Showing the changes to apps/server/src/workspace/WorkspaceFileSystem.test.ts (1 of 2 files). Show all
Unified · Split
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | −import { it, describe, expect } from "@effect/vitest"; | |
| 2 | +import { assert, it, describe } from "@effect/vitest"; | |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | 5 | import * as Layer from "effect/Layer"; |
| @@ -12,6 +12,8 @@ import * as WorkspaceEntries from "./WorkspaceEntries.ts"; · 6 unmodified lines | ||
| 12 | 12 | import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts"; |
| 13 | 13 | import * as WorkspacePaths from "./WorkspacePaths.ts"; |
| 14 | 14 | |
| 15 | +const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; | |
| 16 | + | |
| 15 | 17 | const ProjectLayer = WorkspaceFileSystem.layer.pipe( |
| 16 | 18 | Layer.provide(WorkspacePaths.layer), |
| 17 | 19 | Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))), |
| @@ -51,6 +53,20 @@ const writeTextFile = Effect.fn("writeTextFile")(function* ( · 33 unmodified lines | ||
| 51 | 53 | yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie); |
| 52 | 54 | }); |
| 53 | 55 | |
| 56 | +const 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 | + | |
| 54 | 70 | it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => { |
| 55 | 71 | describe("readFile", () => { |
| 56 | 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 | 80 | relativePath: "src/index.ts", |
| 65 | 81 | }); |
| 66 | 82 | |
| 67 | − expect(result).toEqual({ | |
| 83 | + assert.deepStrictEqual(result, { | |
| 68 | 84 | relativePath: "src/index.ts", |
| 69 | 85 | contents: "export const answer = 42;\n", |
| 70 | 86 | byteLength: 26, |
| @@ -82,9 +98,47 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 11 unmodified lines | ||
| 82 | 98 | .readFile({ cwd, relativePath: "../escape.md" }) |
| 83 | 99 | .pipe(Effect.flip); |
| 84 | 100 | |
| 85 | − expect(error.message).toContain( | |
| 86 | − "Workspace file path must be relative to the project root: ../escape.md", | |
| 101 | + assert.match( | |
| 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 | 159 | .readFile({ cwd, relativePath: "linked-secret.txt" }) |
| 106 | 160 | .pipe(Effect.flip); |
| 107 | 161 | |
| 108 | − expect(error.message).toBe( | |
| 162 | + assert.strictEqual( | |
| 163 | + error.message, | |
| 109 | 164 | `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`, |
| 110 | 165 | ); |
| 111 | − expect(error.cause).toBeInstanceOf(Error); | |
| 112 | − expect((error.cause as Error).message).toBe( | |
| 166 | + assert.ok(error.cause instanceof Error); | |
| 167 | + assert.strictEqual( | |
| 168 | + (error.cause as Error).message, | |
| 113 | 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 | 188 | .readFileString(path.join(cwd, "plans/effect-rpc.md")) |
| 133 | 189 | .pipe(Effect.orDie); |
| 134 | 190 | |
| 135 | − expect(result).toEqual({ relativePath: "plans/effect-rpc.md" }); | |
| 136 | − expect(saved).toBe("# Plan\n"); | |
| 191 | + assert.deepStrictEqual(result, { relativePath: "plans/effect-rpc.md" }); | |
| 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 | 201 | yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); |
| 146 | 202 | |
| 147 | 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 | 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 | 215 | const afterWrite = yield* workspaceEntries.list({ cwd }); |
| 159 | − expect(afterWrite.entries).toEqual( | |
| 160 | − expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]), | |
| 216 | + assert.strictEqual( | |
| 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 | 237 | .pipe(Effect.flip); |
| 180 | 238 | |
| 181 | − expect(error.message).toContain( | |
| 182 | − "Workspace file path must be relative to the project root: ../escape.md", | |
| 239 | + assert.match( | |
| 240 | + error.message, | |
| 241 | + /Workspace file path must be relative to the project root: \.\.\/escape\.md/, | |
| 183 | 242 | ); |
| 184 | 243 | |
| 185 | 244 | const escapedPath = path.resolve(cwd, "..", "escape.md"); |
| 186 | − const escapedStat = yield* fileSystem | |
| 187 | − .stat(escapedPath) | |
| 188 | − .pipe(Effect.orElseSucceed(() => null)); | |
| 189 | − expect(escapedStat).toBeNull(); | |
| 245 | + const escapedExists = yield* fileSystem.exists(escapedPath); | |
| 246 | + assert.strictEqual(escapedExists, false); | |
| 190 | 247 | }), |
| 191 | 248 | ); |
| 192 | 249 | }); |