History

Use Effect FileSystem for workspace reads

Cursor Agent committed 4 months ago

Commit 05760161 parent 97e5cd32 files · +115 −59Browse files

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

Unified · Split

apps/server/src/workspace/WorkspaceFileSystem.test.ts+76−19
@@ -1,5 +1,5 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it, describe, expect } from "@effect/vitest";2import { assert, it, describe } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
@@ -12,6 +12,8 @@ import * as WorkspaceEntries from "./WorkspaceEntries.ts"; · 6 unmodified lines
12import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts";12import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts";
13import * as WorkspacePaths from "./WorkspacePaths.ts";13import * as WorkspacePaths from "./WorkspacePaths.ts";
14 14
15const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;
16
15const ProjectLayer = WorkspaceFileSystem.layer.pipe(17const ProjectLayer = WorkspaceFileSystem.layer.pipe(
16 Layer.provide(WorkspacePaths.layer),18 Layer.provide(WorkspacePaths.layer),
17 Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),19 Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),
@@ -51,6 +53,20 @@ const writeTextFile = Effect.fn("writeTextFile")(function* ( · 33 unmodified lines
51 yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);53 yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);
52});54});
53 55
56const writeBinaryFile = Effect.fn("writeBinaryFile")(function* (
57 cwd: string,
58 relativePath: string,
59 contents: Uint8Array,
60) {
61 const fileSystem = yield* FileSystem.FileSystem;
62 const path = yield* Path.Path;
63 const absolutePath = path.join(cwd, relativePath);
64 yield* fileSystem
65 .makeDirectory(path.dirname(absolutePath), { recursive: true })
66 .pipe(Effect.orDie);
67 yield* fileSystem.writeFile(absolutePath, contents).pipe(Effect.orDie);
68});
69
54it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => {70it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => {
55 describe("readFile", () => {71 describe("readFile", () => {
56 it.effect("reads UTF-8 files relative to the workspace root", () =>72 it.effect("reads UTF-8 files relative to the workspace root", () =>
@@ -64,7 +80,7 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 7 unmodified lines
64 relativePath: "src/index.ts",80 relativePath: "src/index.ts",
65 });81 });
66 82
67 expect(result).toEqual({83 assert.deepStrictEqual(result, {
68 relativePath: "src/index.ts",84 relativePath: "src/index.ts",
69 contents: "export const answer = 42;\n",85 contents: "export const answer = 42;\n",
70 byteLength: 26,86 byteLength: 26,
@@ -82,9 +98,47 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 11 unmodified lines
82 .readFile({ cwd, relativePath: "../escape.md" })98 .readFile({ cwd, relativePath: "../escape.md" })
83 .pipe(Effect.flip);99 .pipe(Effect.flip);
84 100
85 expect(error.message).toContain(101 assert.match(
86 "Workspace file path must be relative to the project root: ../escape.md",102 error.message,
103 /Workspace file path must be relative to the project root: \.\.\/escape\.md/,
104 );
105 }),
106 );
107
108 it.effect("truncates text files larger than the preview byte limit", () =>
109 Effect.gen(function* () {
110 const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
111 const cwd = yield* makeTempDir;
112 yield* writeTextFile(cwd, "large.txt", `${"a".repeat(PROJECT_READ_FILE_MAX_BYTES)}tail`);
113
114 const result = yield* workspaceFileSystem.readFile({
115 cwd,
116 relativePath: "large.txt",
117 });
118
119 assert.strictEqual(result.byteLength, PROJECT_READ_FILE_MAX_BYTES + 4);
120 assert.strictEqual(result.contents.length, PROJECT_READ_FILE_MAX_BYTES);
121 assert.strictEqual(result.contents, "a".repeat(PROJECT_READ_FILE_MAX_BYTES));
122 assert.strictEqual(result.truncated, true);
123 }),
124 );
125
126 it.effect("rejects binary files before decoding them as text", () =>
127 Effect.gen(function* () {
128 const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
129 const cwd = yield* makeTempDir;
130 yield* writeBinaryFile(cwd, "image.bin", new Uint8Array([0x66, 0x6f, 0x00, 0x6f]));
131
132 const error = yield* workspaceFileSystem
133 .readFile({ cwd, relativePath: "image.bin" })
134 .pipe(Effect.flip);
135
136 assert.strictEqual(
137 error.message,
138 `Workspace file operation 'workspaceFileSystem.readFile' failed for 'image.bin' in '${cwd}'.`,
87 );139 );
140 assert.ok(error.cause instanceof Error);
141 assert.strictEqual((error.cause as Error).message, "Binary files cannot be previewed as text.");
88 }),142 }),
89 );143 );
90 144
@@ -105,11 +159,13 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 14 unmodified lines
105 .readFile({ cwd, relativePath: "linked-secret.txt" })159 .readFile({ cwd, relativePath: "linked-secret.txt" })
106 .pipe(Effect.flip);160 .pipe(Effect.flip);
107 161
108 expect(error.message).toBe(162 assert.strictEqual(
163 error.message,
109 `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`,164 `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`,
110 );165 );
111 expect(error.cause).toBeInstanceOf(Error);166 assert.ok(error.cause instanceof Error);
112 expect((error.cause as Error).message).toBe(167 assert.strictEqual(
168 (error.cause as Error).message,
113 "Workspace file path resolves outside the project root.",169 "Workspace file path resolves outside the project root.",
114 );170 );
115 }),171 }),
@@ -132,8 +188,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 16 unmodified lines
132 .readFileString(path.join(cwd, "plans/effect-rpc.md"))188 .readFileString(path.join(cwd, "plans/effect-rpc.md"))
133 .pipe(Effect.orDie);189 .pipe(Effect.orDie);
134 190
135 expect(result).toEqual({ relativePath: "plans/effect-rpc.md" });191 assert.deepStrictEqual(result, { relativePath: "plans/effect-rpc.md" });
136 expect(saved).toBe("# Plan\n");192 assert.strictEqual(saved, "# Plan\n");
137 }),193 }),
138 );194 );
139 195
@@ -145,7 +201,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 5 unmodified lines
145 yield* writeTextFile(cwd, "src/existing.ts", "export {};\n");201 yield* writeTextFile(cwd, "src/existing.ts", "export {};\n");
146 202
147 const beforeWrite = yield* workspaceEntries.list({ cwd });203 const beforeWrite = yield* workspaceEntries.list({ cwd });
148 expect(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")).toBe(204 assert.strictEqual(
205 beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"),
149 false,206 false,
150 );207 );
151 208
@@ -156,10 +213,11 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 4 unmodified lines
156 });213 });
157 214
158 const afterWrite = yield* workspaceEntries.list({ cwd });215 const afterWrite = yield* workspaceEntries.list({ cwd });
159 expect(afterWrite.entries).toEqual(216 assert.strictEqual(
160 expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]),217 afterWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"),
218 true,
161 );219 );
162 expect(afterWrite.truncated).toBe(false);220 assert.strictEqual(afterWrite.truncated, false);
163 }),221 }),
164 );222 );
165 223
@@ -178,15 +236,14 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 12 unmodified lines
178 })236 })
179 .pipe(Effect.flip);237 .pipe(Effect.flip);
180 238
181 expect(error.message).toContain(239 assert.match(
182 "Workspace file path must be relative to the project root: ../escape.md",240 error.message,
241 /Workspace file path must be relative to the project root: \.\.\/escape\.md/,
183 );242 );
184 243
185 const escapedPath = path.resolve(cwd, "..", "escape.md");244 const escapedPath = path.resolve(cwd, "..", "escape.md");
186 const escapedStat = yield* fileSystem245 const escapedExists = yield* fileSystem.exists(escapedPath);
187 .stat(escapedPath)246 assert.strictEqual(escapedExists, false);
188 .pipe(Effect.orElseSucceed(() => null));
189 expect(escapedStat).toBeNull();
190 }),247 }),
191 );248 );
192 });249 });