import { ProjectId, type OrchestrationProject } from "@t3tools/contracts"; import { Effect, Layer, Option } from "effect"; import { describe, expect, it, vi } from "vitest"; import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; import { TerminalManager } from "../../terminal/Services/Manager.ts"; import { ProjectSetupScriptRunner } from "../Services/ProjectSetupScriptRunner.ts"; import { ProjectSetupScriptRunnerLive } from "./ProjectSetupScriptRunner.ts"; const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationProject => ({ id: ProjectId.make("project-1"), title: "Project", workspaceRoot: "/repo/project", defaultModelSelection: null, scripts, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", deletedAt: null, }); const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) => Layer.succeed(ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.die("unused"), getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 1 }), getCounts: () => Effect.die("unused"), getActiveProjectByWorkspaceRoot: (workspaceRoot) => Effect.succeed( workspaceRoot === project.workspaceRoot ? Option.some(project) : Option.none(), ), getProjectShellById: (projectId) => Effect.succeed(projectId === project.id ? Option.some(project) : Option.none()), getFirstActiveThreadIdByProjectId: () => Effect.die("unused"), getThreadCheckpointContext: () => Effect.die("unused"), getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), }); describe("ProjectSetupScriptRunner", () => { it("returns no-script when no setup script exists", async () => { const open = vi.fn(); const write = vi.fn(); const project = makeProject([]); const runner = await Effect.runPromise( Effect.service(ProjectSetupScriptRunner).pipe( Effect.provide( ProjectSetupScriptRunnerLive.pipe( Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)), Layer.provideMerge( Layer.succeed(TerminalManager, { open, write, resize: () => Effect.void, clear: () => Effect.void, restart: () => Effect.die(new Error("unused")), close: () => Effect.void, subscribe: () => Effect.succeed(() => undefined), }), ), ), ), ), ); const result = await Effect.runPromise( runner.runForThread({ threadId: "thread-1", projectId: "project-1", worktreePath: "/repo/worktrees/a", }), ); expect(result).toEqual({ status: "no-script" }); expect(open).not.toHaveBeenCalled(); expect(write).not.toHaveBeenCalled(); }); it("opens the deterministic setup terminal with worktree env and writes the command", async () => { const open = vi.fn(() => Effect.succeed({ threadId: "thread-1", terminalId: "setup-setup", cwd: "/repo/worktrees/a", worktreePath: "/repo/worktrees/a", status: "running" as const, pid: 123, history: "", exitCode: null, exitSignal: null, updatedAt: "2026-01-01T00:00:00.000Z", }), ); const write = vi.fn(() => Effect.void); const project = makeProject([ { id: "setup", name: "Setup", command: "bun install", icon: "configure", runOnWorktreeCreate: true, }, ]); const runner = await Effect.runPromise( Effect.service(ProjectSetupScriptRunner).pipe( Effect.provide( ProjectSetupScriptRunnerLive.pipe( Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)), Layer.provideMerge( Layer.succeed(TerminalManager, { open, write, resize: () => Effect.void, clear: () => Effect.void, restart: () => Effect.die(new Error("unused")), close: () => Effect.void, subscribe: () => Effect.succeed(() => undefined), }), ), ), ), ), ); const result = await Effect.runPromise( runner.runForThread({ threadId: "thread-1", projectCwd: "/repo/project", worktreePath: "/repo/worktrees/a", }), ); expect(result).toEqual({ status: "started", scriptId: "setup", scriptName: "Setup", terminalId: "setup-setup", cwd: "/repo/worktrees/a", }); expect(open).toHaveBeenCalledWith({ threadId: "thread-1", terminalId: "setup-setup", cwd: "/repo/worktrees/a", worktreePath: "/repo/worktrees/a", env: { T3CODE_PROJECT_ROOT: "/repo/project", T3CODE_WORKTREE_PATH: "/repo/worktrees/a", }, }); expect(write).toHaveBeenCalledWith({ threadId: "thread-1", terminalId: "setup-setup", data: "bun install\r", }); }); });