1// @effect-diagnostics nodeBuiltinImport:off - Owns isolated filesystem fixtures.
2import { afterEach, beforeEach, describe, expect, vi } from "vite-plus/test";
3import { it } from "@effect/vitest";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { ProjectId } from "@t3tools/contracts";
6import * as HostProcess from "@t3tools/shared/HostProcess";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
10import * as NodeFSP from "node:fs/promises";
11import * as NodeOS from "node:os";
12import * as NodePath from "node:path";
13
14import * as ServerConfig from "../config.ts";
15import * as ProcessRunner from "../processRunner.ts";
16import * as ManagedWorkspaces from "./ManagedWorkspaces.ts";
17import * as Orchestrator from "../orchestration-v2/Orchestrator.ts";
18import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
19import * as PullRequestService from "../pullRequest/PullRequestService.ts";
20
21let home: string;
22let repository: string;
23let platform: NodeJS.Platform;
24let sourceRegistered: boolean;
25const launches = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();
26const created = {
27 workspacePath: "/test/workspaces/1",
28 projectId: "new-project",
29 threadId: "new-thread",
30 environmentId: "current-host",
31};
32const output = (stdout = JSON.stringify(created), code = 0) => ({
33 stdout,
34 stderr: "",
35 code: ChildProcessSpawner.ExitCode(code),
36 timedOut: false,
37 stdoutTruncated: false,
38 stderrTruncated: false,
39 stdoutInvalidUtf8: false,
40 stderrInvalidUtf8: false,
41});
42
43beforeEach(async () => {
44 home = await NodeFSP.realpath(
45 await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-managed-ws-")),
46 );
47 repository = NodePath.join(home, "source");
48 platform = "darwin";
49 sourceRegistered = true;
50 await NodeFSP.mkdir(repository, { recursive: true });
51 await NodeFSP.mkdir(NodePath.join(home, ".config"));
52 await NodeFSP.mkdir(NodePath.join(home, "bin"));
53 await NodeFSP.writeFile(
54 NodePath.join(home, ".config/workspaces.json"),
55 JSON.stringify({ repository }),
56 );
57 await NodeFSP.writeFile(NodePath.join(home, "bin/ws"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
58 launches.mockReset();
59 launches.mockImplementation(() => Effect.succeed(output()));
60});
61
62afterEach(async () => {
63 await NodeFSP.rm(home, { recursive: true, force: true });
64});
65
66const layer = () =>
67 ManagedWorkspaces.layer.pipe(
68 Layer.provide(
69 Layer.mergeAll(
70 NodeServices.layer,
71 Layer.succeed(HostProcess.HomeDirectory, home),
72 Layer.succeed(HostProcess.Platform, platform),
73 Layer.succeed(HostProcess.Environment, { PATH: NodePath.join(home, "bin") }),
74 ServerConfig.layerTest(repository, NodePath.join(home, "isolated-t3")).pipe(
75 Layer.provide(NodeServices.layer),
76 ),
77 Layer.succeed(ProcessRunner.ProcessRunner, { run: launches }),
78 Layer.mock(Orchestrator.OrchestratorV2)({}),
79 Layer.mock(ProjectStore.ProjectStoreV2)({
80 listShells: () =>
81 Effect.succeed(
82 sourceRegistered
83 ? [
84 {
85 id: ProjectId.make("source-project"),
86 title: "my work",
87 workspaceRoot: repository,
88 defaultModelSelection: null,
89 scripts: [],
90 createdAt: "2026-10-13T12:00:00.000Z",
91 updatedAt: "2026-10-13T12:00:00.000Z",
92 },
93 ]
94 : [],
95 ),
96 }),
97 Layer.mock(PullRequestService.PullRequestService)({}),
98 ),
99 ),
100 );
101
102const use = <A, E>(effect: Effect.Effect<A, E, ManagedWorkspaces.ManagedWorkspaces>) =>
103 effect.pipe(Effect.provide(layer()));
104
105describe("managed workspace creation", () => {
106 it.effect(
107 "reuses the installed workflow and targets this server without opening a host window",
108 () =>
109 use(
110 Effect.gen(function* () {
111 const service = yield* ManagedWorkspaces.ManagedWorkspaces;
112 expect(yield* service.status()).toEqual({ projectId: ProjectId.make("source-project") });
113 expect(launches).not.toHaveBeenCalled();
114 expect(yield* service.create()).toEqual(created);
115 expect(launches).toHaveBeenCalledOnce();
116 expect(launches.mock.calls[0]?.[0]).toMatchObject({
117 command: NodePath.join(home, "bin/ws"),
118 args: ["new", "--background", "--no-open", "--json"],
119 cwd: repository,
120 env: { T3CODE_HOME: NodePath.join(home, "isolated-t3") },
121 });
122 }),
123 ),
124 );
125
126 it.effect.each([
127 "missing tool",
128 "missing configuration",
129 "missing repository",
130 "unregistered source",
131 "Windows",
132 ] as const)("does not launch when %s makes creation unavailable", (reason) => {
133 if (reason === "Windows") platform = "win32";
134 if (reason === "unregistered source") sourceRegistered = false;
135 return use(
136 Effect.gen(function* () {
137 const service = yield* ManagedWorkspaces.ManagedWorkspaces;
138 if (reason === "missing tool")
139 yield* Effect.promise(() => NodeFSP.rm(NodePath.join(home, "bin/ws")));
140 if (reason === "missing configuration")
141 yield* Effect.promise(() => NodeFSP.rm(NodePath.join(home, ".config/workspaces.json")));
142 if (reason === "missing repository")
143 yield* Effect.promise(() => NodeFSP.rm(repository, { recursive: true }));
144 expect(yield* service.status()).toEqual({ projectId: null });
145 const error = yield* service.create().pipe(Effect.flip);
146 expect(error.reason).toBe("unavailable");
147 expect(launches).not.toHaveBeenCalled();
148 }),
149 );
150 });
151
152 it.effect("rejects unreadable and incomplete CLI replies", () =>
153 use(
154 Effect.gen(function* () {
155 const service = yield* ManagedWorkspaces.ManagedWorkspaces;
156 for (const stdout of ["not JSON", JSON.stringify({ workspacePath: "/created" })]) {
157 launches.mockImplementation(() => Effect.succeed(output(stdout)));
158 expect((yield* service.create().pipe(Effect.flip)).reason).toBe("reply");
159 }
160 }),
161 ),
162 );
163
164 it.effect("propagates a stopped creation instead of navigating to an incomplete workspace", () =>
165 use(
166 Effect.gen(function* () {
167 launches.mockImplementation(() => Effect.succeed(output("", 1)));
168 const service = yield* ManagedWorkspaces.ManagedWorkspaces;
169 expect((yield* service.create().pipe(Effect.flip)).reason).toBe("create");
170 }),
171 ),
172 );
173});