1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it, describe, expect } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as PlatformError from "effect/PlatformError";
8
9import * as WorkspacePaths from "./WorkspacePaths.ts";
10
11const layerTest = Layer.empty.pipe(
12 Layer.provideMerge(WorkspacePaths.layer),
13 Layer.provideMerge(NodeServices.layer),
14);
15
16const makeTempDir = Effect.fn("makeTempDir")(function* () {
17 const fileSystem = yield* FileSystem.FileSystem;
18 return yield* fileSystem.makeTempDirectoryScoped({
19 prefix: "t3code-project-paths-",
20 });
21});
22
23const writeTextFile = Effect.fn("writeTextFile")(function* (
24 cwd: string,
25 relativePath: string,
26 contents = "",
27) {
28 const fileSystem = yield* FileSystem.FileSystem;
29 const path = yield* Path.Path;
30 const absolutePath = path.join(cwd, relativePath);
31 yield* fileSystem
32 .makeDirectory(path.dirname(absolutePath), { recursive: true })
33 .pipe(Effect.orDie);
34 yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);
35});
36
37it.layer(layerTest)("WorkspacePathsLive", (it) => {
38 describe("normalizeWorkspaceRoot", () => {
39 it.effect("resolves an existing directory", () =>
40 Effect.gen(function* () {
41 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
42 const cwd = yield* makeTempDir();
43
44 const resolved = yield* workspacePaths.normalizeWorkspaceRoot(cwd);
45
46 expect(resolved).toBe(cwd);
47 }),
48 );
49
50 it.effect("rejects missing directories", () =>
51 Effect.gen(function* () {
52 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
53 const cwd = yield* makeTempDir();
54 const path = yield* Path.Path;
55
56 const error = yield* workspacePaths
57 .normalizeWorkspaceRoot(path.join(cwd, "missing"))
58 .pipe(Effect.flip);
59
60 expect(error.message).toContain("Workspace root does not exist:");
61 }),
62 );
63
64 it.effect("creates missing directories when createIfMissing is enabled", () =>
65 Effect.gen(function* () {
66 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
67 const fileSystem = yield* FileSystem.FileSystem;
68 const cwd = yield* makeTempDir();
69 const path = yield* Path.Path;
70 const missingPath = path.join(cwd, "nested", "new-project");
71
72 const resolved = yield* workspacePaths.normalizeWorkspaceRoot(missingPath, {
73 createIfMissing: true,
74 });
75 const stat = yield* fileSystem.stat(resolved);
76
77 expect(resolved).toBe(missingPath);
78 expect(stat.type).toBe("Directory");
79 }),
80 );
81
82 it.effect("rejects file paths", () =>
83 Effect.gen(function* () {
84 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
85 const cwd = yield* makeTempDir();
86 const path = yield* Path.Path;
87 const filePath = path.join(cwd, "README.md");
88 yield* writeTextFile(cwd, "README.md", "# hi\n");
89
90 const error = yield* workspacePaths.normalizeWorkspaceRoot(filePath).pipe(Effect.flip);
91
92 expect(error.message).toContain("Workspace root is not a directory:");
93 }),
94 );
95
96 it.effect("preserves non-NotFound stat failures while validating the root", () =>
97 Effect.gen(function* () {
98 const fileSystem = yield* FileSystem.FileSystem;
99 const workspacePaths = yield* WorkspacePaths.make.pipe(
100 Effect.provideService(FileSystem.FileSystem, {
101 ...fileSystem,
102 stat: (path) =>
103 Effect.fail(
104 PlatformError.systemError({
105 _tag: "PermissionDenied",
106 module: "FileSystem",
107 method: "stat",
108 pathOrDescriptor: String(path),
109 description: "Test PermissionDenied stat failure.",
110 }),
111 ),
112 }),
113 );
114 const path = yield* Path.Path;
115 const workspaceRoot = " ./permission-denied ";
116 const normalizedWorkspaceRoot = path.resolve(workspaceRoot.trim());
117
118 const error = yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe(Effect.flip);
119
120 expect(error).toBeInstanceOf(WorkspacePaths.WorkspaceRootStatFailedError);
121 expect(error).toMatchObject({
122 workspaceRoot,
123 normalizedWorkspaceRoot,
124 phase: "validate-existing",
125 });
126 }),
127 );
128
129 it.effect("preserves stat failures while verifying a newly created root", () =>
130 Effect.gen(function* () {
131 const fileSystem = yield* FileSystem.FileSystem;
132 let statCalls = 0;
133 const workspacePaths = yield* WorkspacePaths.make.pipe(
134 Effect.provideService(FileSystem.FileSystem, {
135 ...fileSystem,
136 stat: (path) => {
137 statCalls += 1;
138 const reason = statCalls === 1 ? "NotFound" : "PermissionDenied";
139 return Effect.fail(
140 PlatformError.systemError({
141 _tag: reason,
142 module: "FileSystem",
143 method: "stat",
144 pathOrDescriptor: String(path),
145 description: `Test ${reason} stat failure.`,
146 }),
147 );
148 },
149 makeDirectory: () => Effect.void,
150 }),
151 );
152 const path = yield* Path.Path;
153 const workspaceRoot = " ./created-then-unreadable ";
154 const normalizedWorkspaceRoot = path.resolve(workspaceRoot.trim());
155
156 const error = yield* workspacePaths
157 .normalizeWorkspaceRoot(workspaceRoot, { createIfMissing: true })
158 .pipe(Effect.flip);
159
160 expect(error).toBeInstanceOf(WorkspacePaths.WorkspaceRootStatFailedError);
161 expect(error).toMatchObject({
162 workspaceRoot,
163 normalizedWorkspaceRoot,
164 phase: "verify-created",
165 });
166 }),
167 );
168 });
169
170 describe("resolveRelativePathWithinRoot", () => {
171 it.effect("resolves relative paths inside the workspace root", () =>
172 Effect.gen(function* () {
173 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
174 const cwd = yield* makeTempDir();
175 const path = yield* Path.Path;
176
177 const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
178 workspaceRoot: cwd,
179 relativePath: "plans/effect-rpc.md",
180 });
181
182 expect(resolved).toEqual({
183 absolutePath: path.join(cwd, "plans/effect-rpc.md"),
184 relativePath: "plans/effect-rpc.md",
185 });
186 }),
187 );
188
189 it.effect("rejects paths that escape the workspace root", () =>
190 Effect.gen(function* () {
191 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
192 const cwd = yield* makeTempDir();
193
194 const error = yield* workspacePaths
195 .resolveRelativePathWithinRoot({
196 workspaceRoot: cwd,
197 relativePath: "../escape.md",
198 })
199 .pipe(Effect.flip);
200
201 expect(error.message).toContain(
202 "Workspace file path must be relative to the project root: ../escape.md",
203 );
204 }),
205 );
206 });
207});