1288 lines · 48.7 KB · TypeScript
RawDownload
import { assert, it, vi } from "@effect/vitest";import {  CommandId,  DEFAULT_SERVER_SETTINGS,  GitCommandError,  MessageId,  ProjectId,  ProviderDriverKind,  ProviderInstanceId,  type ServerProvider,  ThreadId,} from "@t3tools/contracts";import * as Deferred from "effect/Deferred";import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import * as SqlClient from "effect/unstable/sql/SqlClient";import * as TestClock from "effect/testing/TestClock"; import * as GitWorkflow from "../git/GitWorkflowService.ts";import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";import { ProjectionProjectRepository } from "../persistence/Services/ProjectionProjects.ts";import * as ProjectService from "../project/ProjectService.ts";import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts";import * as ServerSettings from "../serverSettings.ts";import * as TextGeneration from "../textGeneration/TextGeneration.ts";import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";import * as CommandReceiptStore from "./CommandReceiptStore.ts";import * as EffectOutbox from "./EffectOutbox.ts";import * as IdAllocator from "./IdAllocator.ts";import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";import * as ThreadLaunch from "./ThreadLaunchService.ts";import * as ThreadManagement from "./ThreadManagementService.ts";import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts";import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts"; const projectId = ProjectId.make("project:launch-test");const modelSelection = {  instanceId: ProviderInstanceId.make("codex"),  model: "gpt-5.1-codex",} as const;const project = {  id: projectId,  title: "Project",  workspaceRoot: "/repo",  repositoryIdentity: null,  faviconPath: null,  defaultModelSelection: modelSelection,  defaultThreadEnvMode: null,  scripts: [],  createdAt: "2026-06-20T00:00:00.000Z",  updatedAt: "2026-06-20T00:00:00.000Z",  deletedAt: null,} as const; const adapter = {  instanceId: modelSelection.instanceId,  driver: ProviderDriverKind.make("codex"),  getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),  planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),  openSession: () => Effect.die("provider execution is disabled in launch tests"),} as ProviderAdapterV2Shape; interface HarnessOptions {  readonly createWorktree?: GitWorkflow.GitWorkflowService["Service"]["createWorktree"];  readonly fetchRemote?: GitWorkflow.GitWorkflowService["Service"]["fetchRemote"];  readonly renameBranch?: GitWorkflow.GitWorkflowService["Service"]["renameBranch"];  readonly runSetup?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"];  readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"];  readonly generateBranchName?: TextGeneration.TextGeneration["Service"]["generateBranchName"];  readonly serverSettings?: Parameters<typeof ServerSettings.layerTest>[0];  readonly providers?: ReadonlyArray<ServerProvider>;} function makeHarness(options: HarnessOptions = {}) {  const database = SqlitePersistenceMemory;  const registry = ProviderAdapterRegistry.makeLayer([adapter]);  const orchestrator = makeOrchestratorV2ReplayLayerWithRegistry(    { name: "thread-launch" },    registry,    { databaseLayer: database, runEffectWorker: false },  );  const threadManagement = ThreadManagement.layer.pipe(Layer.provide(orchestrator));  const receipts = CommandReceiptStore.layer.pipe(Layer.provide(database));  const outbox = EffectOutbox.layer.pipe(Layer.provide(database));  const createWorktree = vi.fn(    options.createWorktree ??      ((input) =>        Effect.succeed({          worktree: { path: "/repo-worktrees/feature", refName: input.newRefName, headSha: "abc" },        } as never)),  );  const renameBranch = vi.fn(    options.renameBranch ?? ((input) => Effect.succeed({ branch: input.newBranch })),  );  const runSetup = vi.fn(    options.runSetup ?? (() => Effect.succeed({ status: "no-script" as const })),  );  const generateBranchName = vi.fn(    options.generateBranchName ?? (() => Effect.succeed({ branch: "generated-branch" })),  );  const generateThreadTitle = vi.fn(    options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })),  );  const externalServices = Layer.mergeAll(    Layer.succeed(ProjectService.ProjectService, {      create: () => Effect.die("unused"),      bootstrap: () => Effect.die("unused"),      update: () => Effect.die("unused"),      delete: () => Effect.die("unused"),      getById: (id) => Effect.succeed(id === projectId ? Option.some(project) : Option.none()),      getByWorkspaceRoot: () => Effect.succeed(Option.some(project)),      snapshot: Effect.die("unused"),    }),    Layer.mock(GitWorkflow.GitWorkflowService)({      createWorktree,      renameBranch,      fetchRemote: options.fetchRemote ?? (() => Effect.void),      remoteExists: () => Effect.succeed(true),      remoteBranchExists: () => Effect.succeed(true),      removeWorktree: () => Effect.void,      resolveRemoteTrackingCommit: () =>        Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" }),    }),    Layer.succeed(ProjectSetupScriptRunner.ProjectSetupScriptRunner, {      runForThread: runSetup,    }),    Layer.mock(TextGeneration.TextGeneration)({      generateThreadTitle,      generateBranchName,    }),    ServerSettings.layerTest(options.serverSettings),    makeProviderRegistryLayer(options.providers),  );  const launch = ThreadLaunch.layer.pipe(    Layer.provide(Layer.mergeAll(externalServices, threadManagement, receipts, IdAllocator.layer)),  );  const projectedProjects = Layer.mock(ProjectionProjectRepository)({    getById: ({ projectId: requestedProjectId }) =>      Effect.succeed(        requestedProjectId === projectId          ? Option.some({              projectId,              title: project.title,              workspaceRoot: project.workspaceRoot,              defaultModelSelection: project.defaultModelSelection,              defaultThreadEnvMode: null,              autoPull: false,              scripts: project.scripts,              createdAt: project.createdAt,              updatedAt: project.updatedAt,              deletedAt: project.deletedAt,            })          : Option.none(),      ),  });  const titleRegeneration = ThreadTitleRegeneration.layer.pipe(    Layer.provide(Layer.mergeAll(threadManagement, projectedProjects, externalServices)),  );  return {    layer: Layer.mergeAll(launch, threadManagement, titleRegeneration, outbox, database),    createWorktree,    renameBranch,    generateBranchName,    generateThreadTitle,    runSetup,  };} function launchInput(input: {  readonly command: string;  readonly thread: string;  readonly message?: string;  readonly workspace?: ThreadLaunch.ThreadLaunchWorkspaceStrategy;}) {  return {    commandId: CommandId.make(input.command),    threadId: ThreadId.make(input.thread),    projectId,    title: "New thread",    modelSelection,    runtimeMode: "full-access" as const,    interactionMode: "default" as const,    workspaceStrategy: input.workspace ?? { type: "root" as const },    ...(input.message === undefined      ? {}      : {          initialMessage: {            messageId: MessageId.make(`${input.message}:id`),            text: input.message,            attachments: [],          },        }),    createdBy: "user" as const,    creationSource: "web" as const,  };} function waitUntil<E, R>(predicate: () => Effect.Effect<boolean, E, R>): Effect.Effect<void, E, R> {  return Effect.gen(function* () {    for (let attempt = 0; attempt < 200; attempt += 1) {      if (yield* predicate()) return;      yield* Effect.promise(        () =>          new Promise<void>((resolve) => {            setImmediate(resolve);          }),      );    }    assert.fail("Condition was not reached before timeout.");  });} it.effect("returns a visible preparing message while provisioning is still blocked", () =>  Effect.gen(function* () {    const worktreeEntered = yield* Deferred.make<void>();    const allowWorktree = yield* Deferred.make<void>();    const setupEntered = yield* Deferred.make<void>();    const allowSetup = yield* Deferred.make<void>();    const harness = makeHarness({      createWorktree: () =>        Deferred.succeed(worktreeEntered, undefined).pipe(          Effect.andThen(Deferred.await(allowWorktree)),          Effect.as({            worktree: { path: "/repo-worktrees/feature", refName: "feature", headSha: "abc" },          } as never),        ),      runSetup: () =>        Deferred.succeed(setupEntered, undefined).pipe(          Effect.andThen(Deferred.await(allowSetup)),          Effect.as({ status: "no-script" as const }),        ),    });    yield* Effect.gen(function* () {      const launches = yield* ThreadLaunch.ThreadLaunchService;      const outbox = yield* EffectOutbox.EffectOutboxV2;      const threads = yield* ThreadManagement.ThreadManagementService;      const input = launchInput({        command: "command:launch:blocked",        thread: "thread:launch:blocked",        message: "Build the feature",        workspace: { type: "worktree", baseRef: "main" },      });      const launched = yield* launches.launch(input);      assert.equal(launched.projection.messages[0]?.text, "Build the feature");      assert.equal(launched.projection.runs[0]?.status, "preparing");      assert.equal(        launched.projection.turnItems.find((item) => item.type === "command_execution")?.status,        "running",      );      yield* Deferred.await(worktreeEntered);      let current = yield* threads.getThreadProjection(launched.threadId);      assert.equal(        current.turnItems.find((item) => item.type === "command_execution")?.title,        "Preparing worktree",      );      yield* Deferred.succeed(allowWorktree, undefined);      const entered = yield* Deferred.await(setupEntered).pipe(        Effect.timeoutOption(Duration.seconds(2)),      );      if (Option.isNone(entered)) {        current = yield* threads.getThreadProjection(launched.threadId);        assert.fail(          `Setup was not reached; run=${current.runs[0]?.status ?? "missing"}, worklog=${current.turnItems.find((item) => item.type === "command_execution")?.title ?? "missing"}.`,        );      }      current = yield* threads.getThreadProjection(launched.threadId);      assert.equal(        current.turnItems.find((item) => item.type === "command_execution")?.title,        "Starting setup script",      );      const prematureEffects = yield* outbox.listByCommandId(        CommandId.make("command:launch:blocked:initial-message"),      );      assert.isEmpty(prematureEffects);      yield* Deferred.succeed(allowSetup, undefined);    }).pipe(Effect.provide(harness.layer));  }),); it.effect("provisions independent launches concurrently instead of behind a global semaphore", () =>  Effect.gen(function* () {    const setupCount = yield* Ref.make(0);    const bothEntered = yield* Deferred.make<void>();    const allowSetup = yield* Deferred.make<void>();    const harness = makeHarness({      runSetup: () =>        Ref.updateAndGet(setupCount, (count) => count + 1).pipe(          Effect.tap((count) =>            count === 2 ? Deferred.succeed(bothEntered, undefined) : Effect.void,          ),          Effect.andThen(Deferred.await(allowSetup)),          Effect.as({ status: "no-script" as const }),        ),    });    yield* Effect.gen(function* () {      const launches = yield* ThreadLaunch.ThreadLaunchService;      const results = yield* Effect.all(        [          launches.launch(            launchInput({              command: "command:launch:concurrent-a",              thread: "thread:launch:concurrent-a",              message: "First",            }),          ),          launches.launch(            launchInput({              command: "command:launch:concurrent-b",              thread: "thread:launch:concurrent-b",              message: "Second",            }),          ),        ],        { concurrency: "unbounded" },      );      assert.deepEqual(        results.map((result) => result.projection.runs[0]?.status),        ["preparing", "preparing"],      );      yield* Deferred.await(bothEntered);      assert.equal(yield* Ref.get(setupCount), 2);      yield* Deferred.succeed(allowSetup, undefined);    }).pipe(Effect.provide(harness.layer));  }),); it.effect("enqueues provider work only after setup has been initiated", () =>  Effect.gen(function* () {    const setupEntered = yield* Deferred.make<void>();    const allowSetup = yield* Deferred.make<void>();    const harness = makeHarness({      runSetup: () =>        Deferred.succeed(setupEntered, undefined).pipe(          Effect.andThen(Deferred.await(allowSetup)),          Effect.as({            status: "started" as const,            scriptId: "setup",            scriptName: "Setup",            terminalId: "setup",            cwd: "/repo",          }),        ),    });    yield* Effect.gen(function* () {      const launches = yield* ThreadLaunch.ThreadLaunchService;      const outbox = yield* EffectOutbox.EffectOutboxV2;      const threads = yield* ThreadManagement.ThreadManagementService;      const input = launchInput({        command: "command:launch:release",        thread: "thread:launch:release",        message: "Start after setup",        workspace: { type: "worktree", baseRef: "main" },      });      const launched = yield* launches.launch(input);      yield* Deferred.await(setupEntered);      assert.isEmpty(        yield* outbox.listByCommandId(CommandId.make("command:launch:release:release")),      );      yield* Deferred.succeed(allowSetup, undefined);      yield* waitUntil(() =>        outbox          .listByCommandId(CommandId.make("command:launch:release:release"))          .pipe(Effect.map((effects) => effects.length === 1)),      );      const projection = yield* threads.getThreadProjection(launched.threadId);      assert.equal(projection.runs[0]?.status, "starting");      assert.equal(projection.checkpointScopes[0]?.cwd, "/repo-worktrees/feature");      assert.equal(        projection.turnItems.find((item) => item.type === "command_execution")?.status,        "completed",      );    }).pipe(Effect.provide(harness.layer));  }),); it.effect(  "queues follow-up messages behind preparation and checkpoints them in the final workspace",  () =>    Effect.gen(function* () {      const setupEntered = yield* Deferred.make<void>();      const failSetup = yield* Deferred.make<void>();      const harness = makeHarness({        runSetup: () =>          Deferred.succeed(setupEntered, undefined).pipe(            Effect.andThen(Deferred.await(failSetup)),            Effect.andThen(Effect.fail(new Error("setup failed") as never)),          ),      });      yield* Effect.gen(function* () {        const launches = yield* ThreadLaunch.ThreadLaunchService;        const threads = yield* ThreadManagement.ThreadManagementService;        const launched = yield* launches.launch(          launchInput({            command: "command:launch:queued-during-preparation",            thread: "thread:launch:queued-during-preparation",            message: "Prepare the workspace",            workspace: { type: "worktree", baseRef: "main" },          }),        );        yield* Deferred.await(setupEntered);         const followUp = yield* threads.sendToThread({          projectId,          commandId: CommandId.make("command:launch:queued-follow-up"),          threadId: launched.threadId,          messageId: MessageId.make("message:launch:queued-follow-up"),          text: "Run after preparation",          attachments: [],          mode: "auto",          createdBy: "user",          creationSource: "web",        });        assert.equal(followUp.delivery, "queued");        assert.equal(followUp.run.status, "queued");        assert.equal(          followUp.projection.nodes.find(            (node) => node.runId === followUp.run.id && node.kind === "root_turn",          )?.checkpointScopeId,          null,        );         yield* Deferred.succeed(failSetup, undefined);        yield* waitUntil(() =>          threads            .getThreadProjection(launched.threadId)            .pipe(              Effect.map(                (projection) =>                  projection.runs.find((run) => run.id === followUp.run.id)?.status === "starting",              ),            ),        );         const projection = yield* threads.getThreadProjection(launched.threadId);        const rootNode = projection.nodes.find(          (node) => node.runId === followUp.run.id && node.kind === "root_turn",        );        assert.isNotNull(rootNode?.checkpointScopeId);        assert.equal(          projection.checkpointScopes.find((scope) => scope.id === rootNode?.checkpointScopeId)            ?.cwd,          "/repo-worktrees/feature",        );      }).pipe(Effect.provide(harness.layer));    }),); for (const nativeCommand of [" /COMPACT ", "/logout"]) {  it.effect(`uses the first conversation message for a title after ${nativeCommand}`, () =>    Effect.gen(function* () {      const harness = makeHarness();      yield* Effect.gen(function* () {        const launches = yield* ThreadLaunch.ThreadLaunchService;        const threads = yield* ThreadManagement.ThreadManagementService;        const outbox = yield* EffectOutbox.EffectOutboxV2;        const launched = yield* launches.launch({          ...launchInput({            command: "compact-title",            thread: "compact-title-thread",            message: nativeCommand,          }),          generateTitle: true,        });        assert.isUndefined(          (yield* threads.getThreadProjection(launched.threadId)).thread.titleRegeneration,        );        assert.isFalse(          (yield* outbox.listByCommandId(CommandId.make("compact-title:initial-message"))).some(            (effect) => effect.request.type === "thread-title.generate",          ),        );        const commandId = CommandId.make("compact-title-conversation");        const messageId = MessageId.make("compact-title-conversation-message");        yield* threads.dispatch({          type: "message.dispatch",          commandId,          threadId: launched.threadId,          messageId,          createdBy: "user",          creationSource: "web",          text: "Fix the failing parser",          attachments: [],          dispatchMode: { type: "defer_start" },        });        assert.equal(          (yield* threads.getThreadProjection(launched.threadId)).thread.titleRegeneration            ?.requestId,          commandId,        );        assert.deepEqual(          (yield* outbox.listByCommandId(commandId))            .filter((effect) => effect.request.type === "thread-title.generate")

Lines 1–500 of 1288.