History

[codex] Refactor project and workspace Effect services (#3190)

Julius Marminge committed 4 months ago

Commit b0a3a501 parent 79c571755 files · +1211 −1220Browse files

Showing the changes to apps/server/src/workspace/WorkspaceFileSystem.test.ts (1 of 55 files). Show all

Unified · Split

apps/server/src/workspace/WorkspaceFileSystem.test.ts+193−0
@@ -0,0 +1,193 @@
1+import * as NodeServices from "@effect/platform-node/NodeServices";
2+import { it, describe, expect } from "@effect/vitest";
3+import * as Effect from "effect/Effect";
4+import * as FileSystem from "effect/FileSystem";
5+import * as Layer from "effect/Layer";
6+import * as Path from "effect/Path";
7+
8+import * as ServerConfig from "../config.ts";
9+import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
10+import * as VcsProcess from "../vcs/VcsProcess.ts";
11+import * as WorkspaceEntries from "./WorkspaceEntries.ts";
12+import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts";
13+import * as WorkspacePaths from "./WorkspacePaths.ts";
14+
15+const ProjectLayer = WorkspaceFileSystem.layer.pipe(
16+ Layer.provide(WorkspacePaths.layer),
17+ Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),
18+);
19+
20+const TestLayer = Layer.empty.pipe(
21+ Layer.provideMerge(ProjectLayer),
22+ Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),
23+ Layer.provideMerge(WorkspacePaths.layer),
24+ Layer.provideMerge(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer))),
25+ Layer.provide(
26+ ServerConfig.ServerConfig.layerTest(process.cwd(), {
27+ prefix: "t3-workspace-files-test-",
28+ }),
29+ ),
30+ Layer.provideMerge(NodeServices.layer),
31+);
32+
33+const makeTempDir = Effect.gen(function* () {
34+ const fileSystem = yield* FileSystem.FileSystem;
35+ return yield* fileSystem.makeTempDirectoryScoped({
36+ prefix: "t3code-workspace-files-",
37+ });
38+});
39+
40+const writeTextFile = Effect.fn("writeTextFile")(function* (
41+ cwd: string,
42+ relativePath: string,
43+ contents = "",
44+) {
45+ const fileSystem = yield* FileSystem.FileSystem;
46+ const path = yield* Path.Path;
47+ const absolutePath = path.join(cwd, relativePath);
48+ yield* fileSystem
49+ .makeDirectory(path.dirname(absolutePath), { recursive: true })
50+ .pipe(Effect.orDie);
51+ yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);
52+});
53+
54+it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => {
55+ describe("readFile", () => {
56+ it.effect("reads UTF-8 files relative to the workspace root", () =>
57+ Effect.gen(function* () {
58+ const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
59+ const cwd = yield* makeTempDir;
60+ yield* writeTextFile(cwd, "src/index.ts", "export const answer = 42;\n");
61+
62+ const result = yield* workspaceFileSystem.readFile({
63+ cwd,
64+ relativePath: "src/index.ts",
65+ });
66+
67+ expect(result).toEqual({
68+ relativePath: "src/index.ts",
69+ contents: "export const answer = 42;\n",
70+ byteLength: 26,
71+ truncated: false,
72+ });
73+ }),
74+ );
75+
76+ it.effect("rejects reads outside the workspace root", () =>
77+ Effect.gen(function* () {
78+ const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
79+ const cwd = yield* makeTempDir;
80+
81+ const error = yield* workspaceFileSystem
82+ .readFile({ cwd, relativePath: "../escape.md" })
83+ .pipe(Effect.flip);
84+
85+ expect(error.message).toContain(
86+ "Workspace file path must be relative to the project root: ../escape.md",
87+ );
88+ }),
89+ );
90+
91+ it.effect("rejects symlinks that resolve outside the workspace root", () =>
92+ Effect.gen(function* () {
93+ const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
94+ const fileSystem = yield* FileSystem.FileSystem;
95+ const path = yield* Path.Path;
96+ const cwd = yield* makeTempDir;
97+ const outsideDir = yield* makeTempDir;
98+ yield* writeTextFile(outsideDir, "secret.txt", "outside\n");
99+ yield* fileSystem.symlink(
100+ path.join(outsideDir, "secret.txt"),
101+ path.join(cwd, "linked-secret.txt"),
102+ );
103+
104+ const error = yield* workspaceFileSystem
105+ .readFile({ cwd, relativePath: "linked-secret.txt" })
106+ .pipe(Effect.flip);
107+
108+ expect(error.message).toBe(
109+ `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`,
110+ );
111+ expect(error.cause).toBeInstanceOf(Error);
112+ expect((error.cause as Error).message).toBe(
113+ "Workspace file path resolves outside the project root.",
114+ );
115+ }),
116+ );
117+ });
118+
119+ describe("writeFile", () => {
120+ it.effect("writes files relative to the workspace root", () =>
121+ Effect.gen(function* () {
122+ const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
123+ const cwd = yield* makeTempDir;
124+ const fileSystem = yield* FileSystem.FileSystem;
125+ const path = yield* Path.Path;
126+ const result = yield* workspaceFileSystem.writeFile({
127+ cwd,
128+ relativePath: "plans/effect-rpc.md",
129+ contents: "# Plan\n",
130+ });
131+ const saved = yield* fileSystem
132+ .readFileString(path.join(cwd, "plans/effect-rpc.md"))
133+ .pipe(Effect.orDie);
134+
135+ expect(result).toEqual({ relativePath: "plans/effect-rpc.md" });
136+ expect(saved).toBe("# Plan\n");
137+ }),
138+ );
139+
140+ it.effect("invalidates workspace entry search cache after writes", () =>
141+ Effect.gen(function* () {
142+ const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
143+ const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
144+ const cwd = yield* makeTempDir;
145+ yield* writeTextFile(cwd, "src/existing.ts", "export {};\n");
146+
147+ const beforeWrite = yield* workspaceEntries.list({ cwd });
148+ expect(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")).toBe(
149+ false,
150+ );
151+
152+ yield* workspaceFileSystem.writeFile({
153+ cwd,
154+ relativePath: "plans/effect-rpc.md",
155+ contents: "# Plan\n",
156+ });
157+
158+ const afterWrite = yield* workspaceEntries.list({ cwd });
159+ expect(afterWrite.entries).toEqual(
160+ expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]),
161+ );
162+ expect(afterWrite.truncated).toBe(false);
163+ }),
164+ );
165+
166+ it.effect("rejects writes outside the workspace root", () =>
167+ Effect.gen(function* () {
168+ const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
169+ const cwd = yield* makeTempDir;
170+ const path = yield* Path.Path;
171+ const fileSystem = yield* FileSystem.FileSystem;
172+
173+ const error = yield* workspaceFileSystem
174+ .writeFile({
175+ cwd,
176+ relativePath: "../escape.md",
177+ contents: "# nope\n",
178+ })
179+ .pipe(Effect.flip);
180+
181+ expect(error.message).toContain(
182+ "Workspace file path must be relative to the project root: ../escape.md",
183+ );
184+
185+ const escapedPath = path.resolve(cwd, "..", "escape.md");
186+ const escapedStat = yield* fileSystem
187+ .stat(escapedPath)
188+ .pipe(Effect.orElseSucceed(() => null));
189+ expect(escapedStat).toBeNull();
190+ }),
191+ );
192+ });
193+});