855 lines · 31.9 KB · TypeScript
RawDownload
// @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.