// @effect-diagnostics nodeBuiltinImport:off - CLI integration exercises Node HTTP and filesystem boundaries.import * as NodeHttp from "node:http";import * as NodeFS from "node:fs";import * as NodeOS from "node:os";import * as NodePath from "node:path";import * as NodeChildProcess from "node:child_process"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";import * as NodeServices from "@effect/platform-node/NodeServices";import { CommandId, EnvironmentOrchestrationHttpApi, ProviderInstanceId, ThreadId,} from "@t3tools/contracts";import * as NetService from "@t3tools/shared/Net";import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";import { assert, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as DateTime from "effect/DateTime";import * as Layer from "effect/Layer";import * as HttpRouter from "effect/unstable/http/HttpRouter";import * as HttpServer from "effect/unstable/http/HttpServer";import * as HttpApi from "effect/unstable/httpapi/HttpApi";import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";import * as CliError from "effect/unstable/cli/CliError";import * as TestConsole from "effect/testing/TestConsole";import { Command } from "effect/unstable/cli"; import { cli, makeCli } from "./bin.ts";import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts";import { SERVICE_LAUNCHER_CONTEXT_ENV, SERVICE_LAUNCHER_PROTOCOL,} from "./cloud/serviceProtocol.ts";import * as ServerConfig from "./config.ts";import * as ServerEnvironment from "./environment/ServerEnvironment.ts";import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";import { orchestrationHttpApiLayer } from "./orchestration/http.ts";import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts";import { makePersistedServerRuntimeState, persistServerRuntimeState,} from "./serverRuntimeState.ts";import * as WorkspacePaths from "./workspace/WorkspacePaths.ts";import * as ServerSecretStore from "./auth/ServerSecretStore.ts";import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";import { environmentAuthenticatedAuthLayer } from "./auth/http.ts"; import packageJson from "../package.json" with { type: "json" }; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);const DisconnectedLauncherChildLayer = Layer.mergeAll( Layer.succeed(HostProcessEnvironment, { ...process.env, [SERVICE_LAUNCHER_CONTEXT_ENV]: JSON.stringify({ protocol: SERVICE_LAUNCHER_PROTOCOL, childVersion: packageJson.version, }), }), Layer.succeed(ServiceLauncherClient.ServiceLauncherHostProcess, { connected: false, send: () => false, on: () => undefined, off: () => undefined, }),);class ProjectCliHttpApi extends HttpApi.make("environment").add(EnvironmentOrchestrationHttpApi) {} const connectCli = makeCli({ cloudEnabled: true });const noConnectCli = makeCli({ cloudEnabled: false });const runCli = (args: ReadonlyArray<string>, command = cli) => Command.runWith(command, { version: "0.0.0" })(args);const runConnectCli = (args: ReadonlyArray<string>) => runCli(args, connectCli);const runCliWithRuntime = (args: ReadonlyArray<string>) => runCli(args).pipe(Effect.provide(CliRuntimeLayer)); const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.gen(function* () { const result = yield* effect; const output = (yield* TestConsole.logLines).findLast((line): line is string => typeof line === "string") ?? ""; return { result, output }; }).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))); const makeCliTestServerConfig = (baseDir: string) => Effect.gen(function* () { const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined); return { logLevel: "Info", traceMinLevel: "Info", traceTimingEnabled: true, traceBatchWindowMs: 200, traceMaxBytes: 10 * 1024 * 1024, traceMaxFiles: 10, otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpExportIntervalMs: 10_000, otlpServiceName: "t3-server", mode: "web", port: 0, host: "127.0.0.1", cwd: process.cwd(), baseDir, ...derivedPaths, staticDir: undefined, devUrl: undefined, devAllowedOrigins: [], noBrowser: true, startupPresentation: "browser", desktopBootstrapToken: undefined, autoBootstrapProjectFromCwd: false, logWebSocketEvents: false, tailscaleServeEnabled: false, tailscaleServePort: 443, } satisfies ServerConfig.ServerConfig["Service"]; }); const makeProjectPersistenceLayer = (config: ServerConfig.ServerConfig["Service"]) => Layer.mergeAll( OrchestrationLayerLive.pipe( Layer.provideMerge(RepositoryIdentityResolver.layer), Layer.provideMerge(SqlitePersistenceLayerLive), ), WorkspacePaths.layer, ).pipe(Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config))); const readPersistedSnapshot = (baseDir: string) => Effect.gen(function* () { const config = yield* makeCliTestServerConfig(baseDir); return yield* Effect.gen(function* () { const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; return yield* projectionSnapshotQuery.getSnapshot(); }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); }); const makeProjectLookupFixture = Effect.fn("makeProjectLookupFixture")(function* ( withThread: boolean, removeWorkspace: boolean,) { const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-project-lookup-state-"), ); const workspaceRoot = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-project-lookup-git-"), ); NodeChildProcess.execFileSync("git", ["init", "--initial-branch=main", workspaceRoot], { stdio: "ignore", }); yield* runCliWithRuntime(["project", "add", workspaceRoot, "--base-dir", baseDir]); const snapshot = yield* readPersistedSnapshot(baseDir); const project = snapshot.projects.find((candidate) => candidate.workspaceRoot === workspaceRoot)!; assert.isDefined(project); if (withThread) { const config = yield* makeCliTestServerConfig(baseDir); yield* Effect.gen(function* () { const engine = yield* OrchestrationEngine.OrchestrationEngineService; yield* engine.dispatch({ type: "thread.create", commandId: CommandId.make("cmd-project-lookup-thread"), threadId: ThreadId.make("thread-project-lookup"), projectId: project.id, title: "Project lookup test", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex" }, interactionMode: "default", runtimeMode: "approval-required", branch: null, worktreePath: null, createdAt: DateTime.formatIso(yield* DateTime.now), }); }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); } if (removeWorkspace) { NodeFS.renameSync(workspaceRoot, `${workspaceRoot}-removed`); assert.isFalse(NodeFS.existsSync(workspaceRoot)); } return { baseDir, workspaceRoot, project };}); it.layer(NodeServices.layer)("project lookup with unavailable workspaces", (it) => { it.effect("removes an empty project by ID without force after its directory is gone", () => Effect.gen(function* () { const { baseDir, project } = yield* makeProjectLookupFixture(false, true); yield* runCliWithRuntime(["project", "remove", project.id, "--base-dir", baseDir]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull(after.projects.find((candidate) => candidate.id === project.id)!.deletedAt); }), ); it.effect.each([true, false])( "requires force for child threads, then removes by ID; missing=%s", (removeWorkspace) => Effect.gen(function* () { const { baseDir, project } = yield* makeProjectLookupFixture(true, removeWorkspace); const error = yield* runCliWithRuntime([ "project", "remove", project.id, "--base-dir", baseDir, ]).pipe(Effect.flip); assert.include(error.message, "cannot be deleted without force=true"); const retained = yield* readPersistedSnapshot(baseDir); assert.isNull( retained.projects.find((candidate) => candidate.id === project.id)!.deletedAt, ); assert.isNull( retained.threads.find((thread) => thread.id === "thread-project-lookup")!.deletedAt, ); yield* runCliWithRuntime([ "project", "remove", project.id, "--force", "--base-dir", baseDir, ]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull( after.projects.find((candidate) => candidate.id === project.id)!.deletedAt, ); assert.isNotNull( after.threads.find((thread) => thread.id === "thread-project-lookup")!.deletedAt, ); }), ); it.effect("cannot remove the old environment's ID from a replacement empty database", () => Effect.gen(function* () { const { baseDir, project } = yield* makeProjectLookupFixture(true, true); const replacementDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-project-lookup-new-state-"), ); const error = yield* runCliWithRuntime([ "project", "remove", project.id, "--force", "--base-dir", replacementDir, ]).pipe(Effect.flip); assert.include(error.message, "No active project found"); assert.include(String(error.cause), "Workspace root does not exist"); const original = yield* readPersistedSnapshot(baseDir); assert.isNull(original.projects.find((candidate) => candidate.id === project.id)!.deletedAt); const replacement = yield* readPersistedSnapshot(replacementDir); assert.equal(replacement.projects.length, 0); }), ); it.effect("renames by ID and stored path, then force removes after the directory is gone", () => Effect.gen(function* () { const { baseDir, workspaceRoot, project } = yield* makeProjectLookupFixture(true, true); yield* runCliWithRuntime([ "project", "rename", project.id, "Renamed by ID", "--base-dir", baseDir, ]); const afterIdRename = yield* readPersistedSnapshot(baseDir); assert.equal( afterIdRename.projects.find((candidate) => candidate.id === project.id)!.title, "Renamed by ID", ); yield* runCliWithRuntime([ "project", "rename", workspaceRoot, "Renamed by stored path", "--base-dir", baseDir, ]); const afterPathRename = yield* readPersistedSnapshot(baseDir); assert.equal( afterPathRename.projects.find((candidate) => candidate.id === project.id)!.title, "Renamed by stored path", ); const error = yield* runCliWithRuntime([ "project", "remove", workspaceRoot, "--base-dir", baseDir, ]).pipe(Effect.flip); assert.include(error.message, "cannot be deleted without force=true"); yield* runCliWithRuntime([ "project", "remove", workspaceRoot, "--force", "--base-dir", baseDir, ]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull(after.projects.find((candidate) => candidate.id === project.id)!.deletedAt); assert.isNotNull( after.threads.find((thread) => thread.id === "thread-project-lookup")!.deletedAt, ); assert.isFalse(NodeFS.existsSync(workspaceRoot)); }), ); it.effect("preserves normalized paths and distinct symlink project entries", () => Effect.gen(function* () { const { baseDir, workspaceRoot, project } = yield* makeProjectLookupFixture(false, false); const normalizedInput = `${workspaceRoot}${NodePath.sep}.`; yield* runCliWithRuntime([ "project", "rename", normalizedInput, "Normalized", "--base-dir", baseDir, ]); const renamed = yield* readPersistedSnapshot(baseDir); assert.equal( renamed.projects.find((candidate) => candidate.id === project.id)!.title, "Normalized", ); const aliasPath = `${workspaceRoot}-alias`; NodeFS.symlinkSync(workspaceRoot, aliasPath, "junction"); const error = yield* runCliWithRuntime([ "project", "remove", aliasPath, "--force", "--base-dir", baseDir, ]).pipe(Effect.flip); assert.include(error.message, "No active project found"); yield* runCliWithRuntime(["project", "add", aliasPath, "--base-dir", baseDir]); const added = yield* readPersistedSnapshot(baseDir); const aliasProject = added.projects.find( (candidate) => candidate.workspaceRoot === aliasPath, )!; assert.notEqual(aliasProject.id, project.id); yield* runCliWithRuntime([ "project", "remove", `${aliasPath}${NodePath.sep}.`, "--base-dir", baseDir, ]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull( after.projects.find((candidate) => candidate.id === aliasProject.id)!.deletedAt, ); assert.isNull(after.projects.find((candidate) => candidate.id === project.id)!.deletedAt); assert.isTrue(NodeFS.existsSync(workspaceRoot)); }), );}); const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Effect<A, E, R>) => Effect.gen(function* () { const config = yield* makeCliTestServerConfig(baseDir); const routesLayer = HttpApiBuilder.layer(ProjectCliHttpApi).pipe( Layer.provide(orchestrationHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer), ); const appLayer = HttpRouter.serve(routesLayer, { disableListenLog: true, disableLogger: true, }).pipe( Layer.provideMerge( EnvironmentAuth.layer.pipe( Layer.provideMerge(SqlitePersistenceLayerLive), Layer.provide(ServerEnvironment.identityLayer), Layer.provide(ServerSecretStore.layer), ), ), Layer.provideMerge(makeProjectPersistenceLayer(config)), Layer.provideMerge( NodeHttpServer.layer(NodeHttp.createServer, { host: "127.0.0.1", port: 0, }), ), Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config)), ); return yield* Effect.scoped( Effect.gen(function* () { const server = yield* HttpServer.HttpServer; const address = server.address; if (typeof address === "string" || !("port" in address)) { assert.fail(`Expected TCP address, got ${address}`); } yield* persistServerRuntimeState({ path: config.serverRuntimeStatePath, state: yield* makePersistedServerRuntimeState({ config, port: address.port, }), }); return yield* run(); }).pipe(Effect.provide(Layer.mergeAll(appLayer, NodeServices.layer))), ); }); it.layer(NodeServices.layer)("bin cli parsing", (it) => { it.effect("accepts the built-in lowercase log-level flag values", () => Effect.gen(function* () { const { output } = yield* captureStdout(runCli(["--log-level", "debug", "--version"])); assert.include(output, "0.0.0"); }), ); it.effect("accepts canonical --no-<flag> boolean negation", () => Effect.gen(function* () { const { output } = yield* captureStdout(runCli(["--no-log-websocket-events", "--version"])); assert.include(output, "0.0.0"); }), ); it.effect("rejects invalid log-level casing before launching the server", () => Effect.gen(function* () { const error = yield* runCliWithRuntime(["--log-level", "Debug"]).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } if (error._tag !== "InvalidValue") { assert.fail(`Expected InvalidValue, got ${error._tag}`); } assert.equal(error.option, "log-level"); assert.equal(error.value, "Debug"); }), ); it.effect("rejects connect commands when public configuration is missing", () => Effect.gen(function* () { const error = yield* runCli(["connect", "status"], noConnectCli).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } if (error._tag !== "ShowHelp") { assert.fail(`Expected ShowHelp, got ${error._tag}`); } assert.deepEqual(error.commandPath, ["t3", "connect"]); assert.include(error.errors[0]?.message ?? "", "missing T3 Connect public configuration"); const output = (yield* TestConsole.errorLines).join("\n"); assert.include(output, "ERROR"); assert.include(output, "missing T3 Connect public configuration"); }).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))), ); it.effect("exposes service lifecycle commands without T3 Connect configuration", () => Effect.gen(function* () { const { output } = yield* captureStdout(runCli(["service", "--help"], noConnectCli)); assert.include(output, "Manage the T3 Code background service."); assert.include(output, "install"); assert.include(output, "uninstall"); assert.include(output, "update"); assert.include(output, "status"); }), ); it.effect("reports fresh headless connect state without requiring local configuration", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-status-test-"), ); const { output } = yield* captureStdout( runConnectCli(["connect", "status", "--base-dir", baseDir, "--json"]), ); // @effect-diagnostics-next-line preferSchemaOverJson:off - CLI JSON output is decoded as a presentation DTO. const status = JSON.parse(output) as { readonly desired: boolean; readonly authenticated: boolean; readonly linked: boolean; readonly cloudUserId: string | null; readonly relayUrl: string | null; }; assert.equal(status.desired, false); assert.equal(status.authenticated, false); assert.equal(status.linked, false); assert.equal(status.cloudUserId, null); assert.equal(status.relayUrl, null); }).pipe(Effect.provide(DisconnectedLauncherChildLayer)), ); it.effect("reports actionable human-readable headless connect state", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-status-human-test-"), );Lines 1–500 of 855. Load more
// @effect-diagnostics nodeBuiltinImport:off - CLI integration exercises Node HTTP and filesystem boundaries. import * as NodeHttp from "node:http"; import * as NodeFS from "node:fs"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as NodeChildProcess from "node:child_process"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { CommandId, EnvironmentOrchestrationHttpApi, ProviderInstanceId, ThreadId, } from "@t3tools/contracts"; import * as NetService from "@t3tools/shared/Net"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as DateTime from "effect/DateTime"; import * as Layer from "effect/Layer"; import * as HttpRouter from "effect/unstable/http/HttpRouter"; import * as HttpServer from "effect/unstable/http/HttpServer"; import * as HttpApi from "effect/unstable/httpapi/HttpApi"; import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; import * as CliError from "effect/unstable/cli/CliError"; import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; import { cli, makeCli } from "./bin.ts"; import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts"; import { SERVICE_LAUNCHER_CONTEXT_ENV, SERVICE_LAUNCHER_PROTOCOL, } from "./cloud/serviceProtocol.ts"; import * as ServerConfig from "./config.ts"; import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts"; import { orchestrationHttpApiLayer } from "./orchestration/http.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts"; import { makePersistedServerRuntimeState, persistServerRuntimeState, } from "./serverRuntimeState.ts"; import * as WorkspacePaths from "./workspace/WorkspacePaths.ts"; import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; import { environmentAuthenticatedAuthLayer } from "./auth/http.ts"; import packageJson from "../package.json" with { type: "json" }; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); const DisconnectedLauncherChildLayer = Layer.mergeAll( Layer.succeed(HostProcessEnvironment, { ...process.env, [SERVICE_LAUNCHER_CONTEXT_ENV]: JSON.stringify({ protocol: SERVICE_LAUNCHER_PROTOCOL, childVersion: packageJson.version, }), }), Layer.succeed(ServiceLauncherClient.ServiceLauncherHostProcess, { connected: false, send: () => false, on: () => undefined, off: () => undefined, }), ); class ProjectCliHttpApi extends HttpApi.make("environment").add(EnvironmentOrchestrationHttpApi) {} const connectCli = makeCli({ cloudEnabled: true }); const noConnectCli = makeCli({ cloudEnabled: false }); const runCli = (args: ReadonlyArray<string>, command = cli) => Command.runWith(command, { version: "0.0.0" })(args); const runConnectCli = (args: ReadonlyArray<string>) => runCli(args, connectCli); const runCliWithRuntime = (args: ReadonlyArray<string>) => runCli(args).pipe(Effect.provide(CliRuntimeLayer)); const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.gen(function* () { const result = yield* effect; const output = (yield* TestConsole.logLines).findLast((line): line is string => typeof line === "string") ?? ""; return { result, output }; }).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))); const makeCliTestServerConfig = (baseDir: string) => Effect.gen(function* () { const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined); return { logLevel: "Info", traceMinLevel: "Info", traceTimingEnabled: true, traceBatchWindowMs: 200, traceMaxBytes: 10 * 1024 * 1024, traceMaxFiles: 10, otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpExportIntervalMs: 10_000, otlpServiceName: "t3-server", mode: "web", port: 0, host: "127.0.0.1", cwd: process.cwd(), baseDir, ...derivedPaths, staticDir: undefined, devUrl: undefined, devAllowedOrigins: [], noBrowser: true, startupPresentation: "browser", desktopBootstrapToken: undefined, autoBootstrapProjectFromCwd: false, logWebSocketEvents: false, tailscaleServeEnabled: false, tailscaleServePort: 443, } satisfies ServerConfig.ServerConfig["Service"]; }); const makeProjectPersistenceLayer = (config: ServerConfig.ServerConfig["Service"]) => Layer.mergeAll( OrchestrationLayerLive.pipe( Layer.provideMerge(RepositoryIdentityResolver.layer), Layer.provideMerge(SqlitePersistenceLayerLive), ), WorkspacePaths.layer, ).pipe(Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config))); const readPersistedSnapshot = (baseDir: string) => Effect.gen(function* () { const config = yield* makeCliTestServerConfig(baseDir); return yield* Effect.gen(function* () { const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; return yield* projectionSnapshotQuery.getSnapshot(); }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); }); const makeProjectLookupFixture = Effect.fn("makeProjectLookupFixture")(function* ( withThread: boolean, removeWorkspace: boolean, ) { const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-project-lookup-state-"), ); const workspaceRoot = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-project-lookup-git-"), ); NodeChildProcess.execFileSync("git", ["init", "--initial-branch=main", workspaceRoot], { stdio: "ignore", }); yield* runCliWithRuntime(["project", "add", workspaceRoot, "--base-dir", baseDir]); const snapshot = yield* readPersistedSnapshot(baseDir); const project = snapshot.projects.find((candidate) => candidate.workspaceRoot === workspaceRoot)!; assert.isDefined(project); if (withThread) { const config = yield* makeCliTestServerConfig(baseDir); yield* Effect.gen(function* () { const engine = yield* OrchestrationEngine.OrchestrationEngineService; yield* engine.dispatch({ type: "thread.create", commandId: CommandId.make("cmd-project-lookup-thread"), threadId: ThreadId.make("thread-project-lookup"), projectId: project.id, title: "Project lookup test", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex" }, interactionMode: "default", runtimeMode: "approval-required", branch: null, worktreePath: null, createdAt: DateTime.formatIso(yield* DateTime.now), }); }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); } if (removeWorkspace) { NodeFS.renameSync(workspaceRoot, `${workspaceRoot}-removed`); assert.isFalse(NodeFS.existsSync(workspaceRoot)); } return { baseDir, workspaceRoot, project }; }); it.layer(NodeServices.layer)("project lookup with unavailable workspaces", (it) => { it.effect("removes an empty project by ID without force after its directory is gone", () => Effect.gen(function* () { const { baseDir, project } = yield* makeProjectLookupFixture(false, true); yield* runCliWithRuntime(["project", "remove", project.id, "--base-dir", baseDir]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull(after.projects.find((candidate) => candidate.id === project.id)!.deletedAt); }), ); it.effect.each([true, false])( "requires force for child threads, then removes by ID; missing=%s", (removeWorkspace) => Effect.gen(function* () { const { baseDir, project } = yield* makeProjectLookupFixture(true, removeWorkspace); const error = yield* runCliWithRuntime([ "project", "remove", project.id, "--base-dir", baseDir, ]).pipe(Effect.flip); assert.include(error.message, "cannot be deleted without force=true"); const retained = yield* readPersistedSnapshot(baseDir); assert.isNull( retained.projects.find((candidate) => candidate.id === project.id)!.deletedAt, ); assert.isNull( retained.threads.find((thread) => thread.id === "thread-project-lookup")!.deletedAt, ); yield* runCliWithRuntime([ "project", "remove", project.id, "--force", "--base-dir", baseDir, ]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull( after.projects.find((candidate) => candidate.id === project.id)!.deletedAt, ); assert.isNotNull( after.threads.find((thread) => thread.id === "thread-project-lookup")!.deletedAt, ); }), ); it.effect("cannot remove the old environment's ID from a replacement empty database", () => Effect.gen(function* () { const { baseDir, project } = yield* makeProjectLookupFixture(true, true); const replacementDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-project-lookup-new-state-"), ); const error = yield* runCliWithRuntime([ "project", "remove", project.id, "--force", "--base-dir", replacementDir, ]).pipe(Effect.flip); assert.include(error.message, "No active project found"); assert.include(String(error.cause), "Workspace root does not exist"); const original = yield* readPersistedSnapshot(baseDir); assert.isNull(original.projects.find((candidate) => candidate.id === project.id)!.deletedAt); const replacement = yield* readPersistedSnapshot(replacementDir); assert.equal(replacement.projects.length, 0); }), ); it.effect("renames by ID and stored path, then force removes after the directory is gone", () => Effect.gen(function* () { const { baseDir, workspaceRoot, project } = yield* makeProjectLookupFixture(true, true); yield* runCliWithRuntime([ "project", "rename", project.id, "Renamed by ID", "--base-dir", baseDir, ]); const afterIdRename = yield* readPersistedSnapshot(baseDir); assert.equal( afterIdRename.projects.find((candidate) => candidate.id === project.id)!.title, "Renamed by ID", ); yield* runCliWithRuntime([ "project", "rename", workspaceRoot, "Renamed by stored path", "--base-dir", baseDir, ]); const afterPathRename = yield* readPersistedSnapshot(baseDir); assert.equal( afterPathRename.projects.find((candidate) => candidate.id === project.id)!.title, "Renamed by stored path", ); const error = yield* runCliWithRuntime([ "project", "remove", workspaceRoot, "--base-dir", baseDir, ]).pipe(Effect.flip); assert.include(error.message, "cannot be deleted without force=true"); yield* runCliWithRuntime([ "project", "remove", workspaceRoot, "--force", "--base-dir", baseDir, ]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull(after.projects.find((candidate) => candidate.id === project.id)!.deletedAt); assert.isNotNull( after.threads.find((thread) => thread.id === "thread-project-lookup")!.deletedAt, ); assert.isFalse(NodeFS.existsSync(workspaceRoot)); }), ); it.effect("preserves normalized paths and distinct symlink project entries", () => Effect.gen(function* () { const { baseDir, workspaceRoot, project } = yield* makeProjectLookupFixture(false, false); const normalizedInput = `${workspaceRoot}${NodePath.sep}.`; yield* runCliWithRuntime([ "project", "rename", normalizedInput, "Normalized", "--base-dir", baseDir, ]); const renamed = yield* readPersistedSnapshot(baseDir); assert.equal( renamed.projects.find((candidate) => candidate.id === project.id)!.title, "Normalized", ); const aliasPath = `${workspaceRoot}-alias`; NodeFS.symlinkSync(workspaceRoot, aliasPath, "junction"); const error = yield* runCliWithRuntime([ "project", "remove", aliasPath, "--force", "--base-dir", baseDir, ]).pipe(Effect.flip); assert.include(error.message, "No active project found"); yield* runCliWithRuntime(["project", "add", aliasPath, "--base-dir", baseDir]); const added = yield* readPersistedSnapshot(baseDir); const aliasProject = added.projects.find( (candidate) => candidate.workspaceRoot === aliasPath, )!; assert.notEqual(aliasProject.id, project.id); yield* runCliWithRuntime([ "project", "remove", `${aliasPath}${NodePath.sep}.`, "--base-dir", baseDir, ]); const after = yield* readPersistedSnapshot(baseDir); assert.isNotNull( after.projects.find((candidate) => candidate.id === aliasProject.id)!.deletedAt, ); assert.isNull(after.projects.find((candidate) => candidate.id === project.id)!.deletedAt); assert.isTrue(NodeFS.existsSync(workspaceRoot)); }), ); }); const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Effect<A, E, R>) => Effect.gen(function* () { const config = yield* makeCliTestServerConfig(baseDir); const routesLayer = HttpApiBuilder.layer(ProjectCliHttpApi).pipe( Layer.provide(orchestrationHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer), ); const appLayer = HttpRouter.serve(routesLayer, { disableListenLog: true, disableLogger: true, }).pipe( Layer.provideMerge( EnvironmentAuth.layer.pipe( Layer.provideMerge(SqlitePersistenceLayerLive), Layer.provide(ServerEnvironment.identityLayer), Layer.provide(ServerSecretStore.layer), ), ), Layer.provideMerge(makeProjectPersistenceLayer(config)), Layer.provideMerge( NodeHttpServer.layer(NodeHttp.createServer, { host: "127.0.0.1", port: 0, }), ), Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config)), ); return yield* Effect.scoped( Effect.gen(function* () { const server = yield* HttpServer.HttpServer; const address = server.address; if (typeof address === "string" || !("port" in address)) { assert.fail(`Expected TCP address, got ${address}`); } yield* persistServerRuntimeState({ path: config.serverRuntimeStatePath, state: yield* makePersistedServerRuntimeState({ config, port: address.port, }), }); return yield* run(); }).pipe(Effect.provide(Layer.mergeAll(appLayer, NodeServices.layer))), ); }); it.layer(NodeServices.layer)("bin cli parsing", (it) => { it.effect("accepts the built-in lowercase log-level flag values", () => Effect.gen(function* () { const { output } = yield* captureStdout(runCli(["--log-level", "debug", "--version"])); assert.include(output, "0.0.0"); }), ); it.effect("accepts canonical --no-<flag> boolean negation", () => Effect.gen(function* () { const { output } = yield* captureStdout(runCli(["--no-log-websocket-events", "--version"])); assert.include(output, "0.0.0"); }), ); it.effect("rejects invalid log-level casing before launching the server", () => Effect.gen(function* () { const error = yield* runCliWithRuntime(["--log-level", "Debug"]).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } if (error._tag !== "InvalidValue") { assert.fail(`Expected InvalidValue, got ${error._tag}`); } assert.equal(error.option, "log-level"); assert.equal(error.value, "Debug"); }), ); it.effect("rejects connect commands when public configuration is missing", () => Effect.gen(function* () { const error = yield* runCli(["connect", "status"], noConnectCli).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } if (error._tag !== "ShowHelp") { assert.fail(`Expected ShowHelp, got ${error._tag}`); } assert.deepEqual(error.commandPath, ["t3", "connect"]); assert.include(error.errors[0]?.message ?? "", "missing T3 Connect public configuration"); const output = (yield* TestConsole.errorLines).join("\n"); assert.include(output, "ERROR"); assert.include(output, "missing T3 Connect public configuration"); }).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))), ); it.effect("exposes service lifecycle commands without T3 Connect configuration", () => Effect.gen(function* () { const { output } = yield* captureStdout(runCli(["service", "--help"], noConnectCli)); assert.include(output, "Manage the T3 Code background service."); assert.include(output, "install"); assert.include(output, "uninstall"); assert.include(output, "update"); assert.include(output, "status"); }), ); it.effect("reports fresh headless connect state without requiring local configuration", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-status-test-"), ); const { output } = yield* captureStdout( runConnectCli(["connect", "status", "--base-dir", baseDir, "--json"]), ); // @effect-diagnostics-next-line preferSchemaOverJson:off - CLI JSON output is decoded as a presentation DTO. const status = JSON.parse(output) as { readonly desired: boolean; readonly authenticated: boolean; readonly linked: boolean; readonly cloudUserId: string | null; readonly relayUrl: string | null; }; assert.equal(status.desired, false); assert.equal(status.authenticated, false); assert.equal(status.linked, false); assert.equal(status.cloudUserId, null); assert.equal(status.relayUrl, null); }).pipe(Effect.provide(DisconnectedLauncherChildLayer)), ); it.effect("reports actionable human-readable headless connect state", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-status-human-test-"), );