import { assert, it, vi } from "@effect/vitest"; import { CommandId, MessageId, ProjectId, ProviderDriverKind, ProviderInstanceId, ThreadId, } from "@t3tools/contracts"; import * as Deferred from "effect/Deferred"; 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 SqlClient from "effect/unstable/sql/SqlClient"; import * as GitWorkflow from "../git/GitWorkflowService.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import * as ProjectService from "../project/ProjectService.ts"; import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.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 { 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, 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 runSetup?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]; readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"]; } 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 ?? (() => Effect.succeed({ worktree: { path: "/repo-worktrees/feature", refName: "feature", headSha: "abc" }, } as never)), ); const runSetup = vi.fn( options.runSetup ?? (() => Effect.succeed({ status: "no-script" as const })), ); 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, fetchRemote: () => Effect.void, removeWorktree: () => Effect.void, resolveRemoteTrackingCommit: () => Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" }), }), Layer.succeed(ProjectSetupScriptRunner.ProjectSetupScriptRunner, { runForThread: runSetup, }), Layer.mock(TextGeneration.TextGeneration)({ generateThreadTitle: options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })), generateBranchName: () => Effect.succeed({ branch: "generated-branch" }), }), ); const launch = ThreadLaunch.layer.pipe( Layer.provide(Layer.mergeAll(externalServices, threadManagement, receipts, IdAllocator.layer)), ); return { layer: Layer.mergeAll(launch, threadManagement, outbox, database), createWorktree, 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(predicate: () => Effect.Effect): Effect.Effect { return Effect.gen(function* () { for (let attempt = 0; attempt < 200; attempt += 1) { if (yield* predicate()) return; yield* Effect.promise( () => new Promise((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(); const allowWorktree = yield* Deferred.make(); const setupEntered = yield* Deferred.make(); const allowSetup = yield* Deferred.make(); 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(); const allowSetup = yield* Deferred.make(); 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(); const allowSetup = yield* Deferred.make(); 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(); const failSetup = yield* Deferred.make(); 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)); }), ); it.effect("does not put optional title generation on the provisioning critical path", () => Effect.gen(function* () { const titleStarted = yield* Deferred.make(); const allowTitle = yield* Deferred.make(); const setupEntered = yield* Deferred.make(); const harness = makeHarness({ generateTitle: () => Deferred.succeed(titleStarted, undefined).pipe( Effect.andThen(Deferred.await(allowTitle)), Effect.as({ title: "Generated later" }), ), runSetup: () => Deferred.succeed(setupEntered, undefined).pipe(Effect.as({ status: "no-script" as const })), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const threads = yield* ThreadManagement.ThreadManagementService; const input = launchInput({ command: "command:launch:title-independent", thread: "thread:launch:title-independent", message: "Generate my title slowly", }); const launched = yield* launches.launch(input); yield* Deferred.await(titleStarted); yield* Deferred.await(setupEntered); yield* waitUntil(() => threads .getThreadProjection(launched.threadId) .pipe(Effect.map((projection) => projection.runs[0]?.status === "starting")), ); assert.equal( (yield* threads.getThreadProjection(launched.threadId)).thread.title, "New thread", ); yield* Deferred.succeed(allowTitle, undefined); yield* waitUntil(() => threads .getThreadProjection(launched.threadId) .pipe(Effect.map((projection) => projection.thread.title === "Generated later")), ); }).pipe(Effect.provide(harness.layer)); }), ); for (const failurePoint of ["worktree", "setup"] as const) { it.effect( `${failurePoint} failure keeps the thread and message visible and emits failure items`, () => Effect.gen(function* () { const failure = new Error(`${failurePoint} failed`); const harness = makeHarness( failurePoint === "worktree" ? { createWorktree: () => Effect.fail(failure as never) } : { runSetup: () => Effect.fail(failure as never) }, ); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const threads = yield* ThreadManagement.ThreadManagementService; const input = launchInput({ command: `command:launch:${failurePoint}-failure`, thread: `thread:launch:${failurePoint}-failure`, message: `Fail during ${failurePoint}`, workspace: { type: "worktree", baseRef: "main" }, }); const launched = yield* launches.launch(input); yield* waitUntil(() => threads .getThreadProjection(launched.threadId) .pipe(Effect.map((projection) => projection.runs[0]?.status === "failed")), ); const projection = yield* threads.getThreadProjection(launched.threadId); assert.equal(projection.messages[0]?.text, `Fail during ${failurePoint}`); assert.equal(projection.runs[0]?.status, "failed"); assert.equal( projection.turnItems.find((item) => item.type === "command_execution")?.status, "failed", ); assert.match( projection.turnItems.find((item) => item.type === "error")?.failure.message ?? "", new RegExp(`${failurePoint} failed`, "u"), ); }).pipe(Effect.provide(harness.layer)); }), ); } it.effect("deduplicates retried launch side effects in-process", () => Effect.gen(function* () { const setupEntered = yield* Deferred.make(); const allowSetup = yield* Deferred.make(); const harness = makeHarness({ 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 input = launchInput({ command: "command:launch:retry", thread: "thread:launch:retry", message: "Only once", }); const [first, retry] = yield* Effect.all([launches.launch(input), launches.launch(input)], { concurrency: "unbounded", }); yield* Deferred.await(setupEntered); assert.equal(first.threadId, retry.threadId); assert.isFalse(first.resumed); assert.isTrue(retry.resumed); assert.equal(harness.runSetup.mock.calls.length, 1); yield* Deferred.succeed(allowSetup, undefined); }).pipe(Effect.provide(harness.layer)); }), ); it.effect("does not let a failing same-command caller strand a concurrent durable launch", () => Effect.gen(function* () { const setupEntered = yield* Deferred.make(); const allowSetup = yield* Deferred.make(); const harness = makeHarness({ 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 command = "command:launch:failed-owner-race"; const [failed, launched] = yield* Effect.all( [ launches .launch({ ...launchInput({ command, thread: "thread:launch:failed-owner-race", message: "This invalid reuse fails", }), reuseExistingThread: true, }) .pipe(Effect.exit), launches.launch( launchInput({ command, thread: "thread:launch:successful-peer", message: "This peer persists", }), ), ], { concurrency: "unbounded" }, ); assert.isTrue(Exit.isFailure(failed)); assert.equal(launched.projection.runs[0]?.status, "preparing"); const entered = yield* Deferred.await(setupEntered).pipe( Effect.timeoutOption(Duration.seconds(2)), ); assert.isTrue(Option.isSome(entered)); assert.equal(harness.runSetup.mock.calls.length, 1); yield* Deferred.succeed(allowSetup, undefined); }).pipe(Effect.provide(harness.layer)); }), ); it.effect("schedules an accepted preparing message exactly once across concurrent retries", () => Effect.gen(function* () { const setupEntered = yield* Deferred.make(); const allowSetup = yield* Deferred.make(); const harness = makeHarness({ 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 threads = yield* ThreadManagement.ThreadManagementService; const input = launchInput({ command: "command:launch:accepted-before-fork", thread: "thread:launch:accepted-before-fork", message: "Resume preparation", }); const messageId = MessageId.make("message:launch:accepted-before-fork"); yield* threads.dispatch({ type: "thread.create", commandId: input.commandId, threadId: input.threadId, projectId: input.projectId, title: input.title, modelSelection: input.modelSelection, runtimeMode: input.runtimeMode, interactionMode: input.interactionMode, branch: null, worktreePath: null, createdBy: input.createdBy, creationSource: input.creationSource, }); yield* threads.dispatch({ type: "message.dispatch", commandId: CommandId.make(`${input.commandId}:initial-message`), threadId: input.threadId, messageId, text: "Resume preparation", attachments: [], modelSelection: input.modelSelection, dispatchMode: { type: "defer_start" }, createdBy: input.createdBy, creationSource: input.creationSource, }); const preparing = yield* threads.getThreadProjection(input.threadId); assert.equal(preparing.runs[0]?.status, "preparing"); const [first, second] = yield* Effect.all([launches.launch(input), launches.launch(input)], { concurrency: "unbounded", }); yield* Deferred.await(setupEntered); assert.isTrue(first.resumed); assert.isTrue(second.resumed); assert.equal(harness.runSetup.mock.calls.length, 1); yield* Deferred.succeed(allowSetup, undefined); }).pipe(Effect.provide(harness.layer)); }), ); it.effect("does not depend on the legacy launch workflow table", () => { const harness = makeHarness(); return Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const launches = yield* ThreadLaunch.ThreadLaunchService; yield* sql`DROP TABLE orchestration_v2_thread_launch_workflows`; const launched = yield* launches.launch( launchInput({ command: "command:launch:no-workflow-table", thread: "thread:launch:no-workflow-table", message: "No private workflow state", }), ); assert.equal(launched.projection.messages[0]?.text, "No private workflow state"); }).pipe(Effect.provide(harness.layer)); });