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 ServerSettings from "../serverSettings.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, { getUserInputActivity: () => Effect.die("unused"), 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"), getEventReplayStats: () => 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"), getImportedAgentSessionSources: () => Effect.die("unused"), getThreadCheckpointContext: () => Effect.die("unused"), getFullThreadDiffContext: () => Effect.die("unused"), getThreadRuntimeContext: () => Effect.die("unused"), getTurnStartMessage: () => 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">, settings = ServerSettings.layerTest(),) => ProjectSetupScriptRunner.layer.pipe( Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)), Layer.provideMerge(makeTerminalManagerLayer(terminal)), Layer.provide(settings), ); describe("ProjectSetupScriptRunner", () => { it.effect("runs the inherited machine setup action in the checkout's worktree", () => { const open = vi.fn(() => Effect.succeed({ threadId: "thread-1", terminalId: "setup-default-setup", cwd: "/repo/worktrees/a", worktreePath: "/repo/worktrees/a", status: "running" as const, pid: 123, history: "", exitCode: null, exitSignal: null, label: "setup-default-setup", updatedAt: "2026-01-01T00:00:00.000Z", }), ); const write = vi.fn(() => Effect.void); 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).toMatchObject({ status: "started", scriptId: "default-setup" }); expect(open).toHaveBeenCalledWith({ threadId: "thread-1", terminalId: "setup-default-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-default-setup", data: "npm install\r", }); }).pipe( Effect.provide( testLayer( makeProject([]), { open, write }, ServerSettings.layerTest({ defaultProjectScripts: [ { id: "default-setup", name: "Setup", command: "npm install", icon: "configure", runOnWorktreeCreate: true, }, ], }), ), ), ); }); 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"), }), ), ); });});
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 ServerSettings from "../serverSettings.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, {
getUserInputActivity: () => Effect.die("unused"),
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"),
getEventReplayStats: () => 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"),
getImportedAgentSessionSources: () => Effect.die("unused"),
getThreadCheckpointContext: () => Effect.die("unused"),
getFullThreadDiffContext: () => Effect.die("unused"),
getThreadRuntimeContext: () => Effect.die("unused"),
getTurnStartMessage: () => 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">,
settings = ServerSettings.layerTest(),
) =>
ProjectSetupScriptRunner.layer.pipe(
Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)),
Layer.provideMerge(makeTerminalManagerLayer(terminal)),
Layer.provide(settings),
);
describe("ProjectSetupScriptRunner", () => {
it.effect("runs the inherited machine setup action in the checkout's worktree", () => {
const open = vi.fn(() =>
Effect.succeed({
threadId: "thread-1",
terminalId: "setup-default-setup",
cwd: "/repo/worktrees/a",
worktreePath: "/repo/worktrees/a",
status: "running" as const,
pid: 123,
history: "",
exitCode: null,
exitSignal: null,
label: "setup-default-setup",
updatedAt: "2026-01-01T00:00:00.000Z",
}),
);
const write = vi.fn(() => Effect.void);
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).toMatchObject({ status: "started", scriptId: "default-setup" });
expect(open).toHaveBeenCalledWith({
threadId: "thread-1",
terminalId: "setup-default-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-default-setup",
data: "npm install\r",
});
}).pipe(
Effect.provide(
testLayer(
makeProject([]),
{ open, write },
ServerSettings.layerTest({
defaultProjectScripts: [
{
id: "default-setup",
name: "Setup",
command: "npm install",
icon: "configure",
runOnWorktreeCreate: true,
},
],
}),
),
),
);
});
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"),
}),
),
);
});
});