200 lines · 6.9 KB · TypeScript
RawDownload
import { describe, expect, it, vi } from "@effect/vitest";import { type OrchestrationProject, ProjectId } from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";import * as TerminalManager from "../terminal/Manager.ts";import * as ProjectSetupScriptRunner from "./ProjectSetupScriptRunner.ts"; const isProjectSetupScriptOperationError = Schema.is(  ProjectSetupScriptRunner.ProjectSetupScriptOperationError,); 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.ProjectionSnapshotQuery, {    getCommandReadModel: () => Effect.die("unused"),    getSnapshot: () => Effect.die("unused"),    getShellSnapshot: () => Effect.die("unused"),    getArchivedShellSnapshot: () => 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"),    getFullThreadDiffContext: () => Effect.die("unused"),    getThreadShellById: () => Effect.die("unused"),    getThreadDetailById: () => Effect.die("unused"),    getThreadDetailSnapshot: () => Effect.die("unused"),    searchThreads: () => Effect.succeed({ matches: [] }),  }); const makeTerminalManagerLayer = (  overrides: Pick<TerminalManager.TerminalManager["Service"], "open" | "write">,) =>  Layer.succeed(TerminalManager.TerminalManager, {    ...overrides,    attachStream: () => Effect.die(new Error("unused")),    resize: () => Effect.void,    clear: () => Effect.void,    restart: () => Effect.die(new Error("unused")),    close: () => Effect.void,    subscribe: () => Effect.succeed(() => undefined),    subscribeMetadata: () => Effect.succeed(() => undefined),  }); const testLayer = (  project: OrchestrationProject,  terminal: Pick<TerminalManager.TerminalManager["Service"], "open" | "write">,) =>  ProjectSetupScriptRunner.layer.pipe(    Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)),    Layer.provideMerge(makeTerminalManagerLayer(terminal)),  ); describe("ProjectSetupScriptRunner", () => {  it.effect("returns no-script when no setup script exists", () => {    const open = vi.fn(() => Effect.die("unexpected open"));    const write = vi.fn(() => Effect.die("unexpected write"));    const project = makeProject([]);     return Effect.gen(function* () {      const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;      const result = yield* 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();    }).pipe(Effect.provide(testLayer(project, { open, write })));  });   it.effect(    "opens the deterministic setup terminal with worktree env and writes the command",    () => {      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,          label: "setup-setup",          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,        },      ]);       return Effect.gen(function* () {        const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;        const result = yield* 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",        });      }).pipe(Effect.provide(testLayer(project, { open, write })));    },  );   it.effect("keeps terminal failures as the exact cause of a structured operation error", () => {    const rootCause = new Error("stat failed");    const terminalError = new TerminalManager.TerminalCwdStatError({      cwd: "/repo/worktrees/a",      cause: rootCause,    });    const project = makeProject([      {        id: "setup",        name: "Setup",        command: "bun install",        icon: "configure",        runOnWorktreeCreate: true,      },    ]);     return Effect.gen(function* () {      const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;      const error = yield* runner        .runForThread({          threadId: "thread-1",          projectId: "project-1",          worktreePath: "/repo/worktrees/a",        })        .pipe(Effect.flip);       expect(isProjectSetupScriptOperationError(error)).toBe(true);      if (isProjectSetupScriptOperationError(error)) {        expect(error.operation).toBe("openTerminal");        expect(error.threadId).toBe("thread-1");        expect(error.projectId).toBe("project-1");        expect(error.worktreePath).toBe("/repo/worktrees/a");        expect(error.cause).toBe(terminalError);        expect(terminalError.cause).toBe(rootCause);      }    }).pipe(      Effect.provide(        testLayer(project, {          open: () => Effect.fail(terminalError),          write: () => Effect.die("unexpected write"),        }),      ),    );  });});