Showing the changes to apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts (1 of 4 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"; |
| @@ -18,6 +18,8 @@ const ProjectLayer = WorkspaceFileSystemLive.pipe( · 12 unmodified lines | ||
| 18 | 18 | Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePathsLive))), |
| 19 | 19 | ); |
| 20 | 20 | |
| 21 | +const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; | |
| 22 | + | |
| 21 | 23 | const TestLayer = Layer.empty.pipe( |
| 22 | 24 | Layer.provideMerge(ProjectLayer), |
| 23 | 25 | Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePathsLive))), |
| @@ -52,6 +54,20 @@ const writeTextFile = Effect.fn("writeTextFile")(function* ( · 28 unmodified lines | ||
| 52 | 54 | yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie); |
| 53 | 55 | }); |
| 54 | 56 | |
| 57 | +const writeBinaryFile = Effect.fn("writeBinaryFile")(function* ( | |
| 58 | + cwd: string, | |
| 59 | + relativePath: string, | |
| 60 | + contents: Uint8Array, | |
| 61 | +) { | |
| 62 | + const fileSystem = yield* FileSystem.FileSystem; | |
| 63 | + const path = yield* Path.Path; | |
| 64 | + const absolutePath = path.join(cwd, relativePath); | |
| 65 | + yield* fileSystem | |
| 66 | + .makeDirectory(path.dirname(absolutePath), { recursive: true }) | |
| 67 | + .pipe(Effect.orDie); | |
| 68 | + yield* fileSystem.writeFile(absolutePath, contents).pipe(Effect.orDie); | |
| 69 | +}); | |
| 70 | + | |
| 55 | 71 | it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => { |
| 56 | 72 | describe("readFile", () => { |
| 57 | 73 | it.effect("reads UTF-8 files relative to the workspace root", () => |
| @@ -65,7 +81,7 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 7 unmodified lines | ||
| 65 | 81 | relativePath: "src/index.ts", |
| 66 | 82 | }); |
| 67 | 83 | |
| 68 | − expect(result).toEqual({ | |
| 84 | + assert.deepStrictEqual(result, { | |
| 69 | 85 | relativePath: "src/index.ts", |
| 70 | 86 | contents: "export const answer = 42;\n", |
| 71 | 87 | byteLength: 26, |
| @@ -83,7 +99,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 11 unmodified lines | ||
| 83 | 99 | .readFile({ cwd, relativePath: "../escape.md" }) |
| 84 | 100 | .pipe(Effect.flip); |
| 85 | 101 | |
| 86 | − expect(error.message).toContain( | |
| 102 | + assert.include( | |
| 103 | + error.message, | |
| 87 | 104 | "Workspace file path must be relative to the project root: ../escape.md", |
| 88 | 105 | ); |
| 89 | 106 | }), |
| @@ -106,7 +123,56 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 16 unmodified lines | ||
| 106 | 123 | .readFile({ cwd, relativePath: "linked-secret.txt" }) |
| 107 | 124 | .pipe(Effect.flip); |
| 108 | 125 | |
| 109 | − expect(error.message).toContain("resolves outside the project root"); | |
| 126 | + assert.include(error.message, "resolves outside the project root"); | |
| 127 | + }), | |
| 128 | + ); | |
| 129 | + | |
| 130 | + it.effect("truncates files larger than the preview byte limit", () => | |
| 131 | + Effect.gen(function* () { | |
| 132 | + const workspaceFileSystem = yield* WorkspaceFileSystem; | |
| 133 | + const cwd = yield* makeTempDir; | |
| 134 | + const contents = `${"a".repeat(PROJECT_READ_FILE_MAX_BYTES)}tail`; | |
| 135 | + yield* writeTextFile(cwd, "large.txt", contents); | |
| 136 | + | |
| 137 | + const result = yield* workspaceFileSystem.readFile({ | |
| 138 | + cwd, | |
| 139 | + relativePath: "large.txt", | |
| 140 | + }); | |
| 141 | + | |
| 142 | + assert.equal(result.contents.length, PROJECT_READ_FILE_MAX_BYTES); | |
| 143 | + assert.equal(result.contents, "a".repeat(PROJECT_READ_FILE_MAX_BYTES)); | |
| 144 | + assert.equal(result.byteLength, PROJECT_READ_FILE_MAX_BYTES + 4); | |
| 145 | + assert.isTrue(result.truncated); | |
| 146 | + }), | |
| 147 | + ); | |
| 148 | + | |
| 149 | + it.effect("rejects binary files", () => | |
| 150 | + Effect.gen(function* () { | |
| 151 | + const workspaceFileSystem = yield* WorkspaceFileSystem; | |
| 152 | + const cwd = yield* makeTempDir; | |
| 153 | + yield* writeBinaryFile(cwd, "binary.bin", new Uint8Array([65, 0, 66])); | |
| 154 | + | |
| 155 | + const error = yield* workspaceFileSystem | |
| 156 | + .readFile({ cwd, relativePath: "binary.bin" }) | |
| 157 | + .pipe(Effect.flip); | |
| 158 | + | |
| 159 | + assert.include(error.message, "Binary files cannot be previewed as text."); | |
| 160 | + }), | |
| 161 | + ); | |
| 162 | + | |
| 163 | + it.effect("rejects directories", () => | |
| 164 | + Effect.gen(function* () { | |
| 165 | + const workspaceFileSystem = yield* WorkspaceFileSystem; | |
| 166 | + const fileSystem = yield* FileSystem.FileSystem; | |
| 167 | + const path = yield* Path.Path; | |
| 168 | + const cwd = yield* makeTempDir; | |
| 169 | + yield* fileSystem.makeDirectory(path.join(cwd, "src"), { recursive: true }); | |
| 170 | + | |
| 171 | + const error = yield* workspaceFileSystem | |
| 172 | + .readFile({ cwd, relativePath: "src" }) | |
| 173 | + .pipe(Effect.flip); | |
| 174 | + | |
| 175 | + assert.include(error.message, "Workspace path is not a file."); | |
| 110 | 176 | }), |
| 111 | 177 | ); |
| 112 | 178 | }); |
| @@ -127,8 +193,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 14 unmodified lines | ||
| 127 | 193 | .readFileString(path.join(cwd, "plans/effect-rpc.md")) |
| 128 | 194 | .pipe(Effect.orDie); |
| 129 | 195 | |
| 130 | − expect(result).toEqual({ relativePath: "plans/effect-rpc.md" }); | |
| 131 | − expect(saved).toBe("# Plan\n"); | |
| 196 | + assert.deepStrictEqual(result, { relativePath: "plans/effect-rpc.md" }); | |
| 197 | + assert.equal(saved, "# Plan\n"); | |
| 132 | 198 | }), |
| 133 | 199 | ); |
| 134 | 200 | |
| @@ -140,9 +206,7 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 5 unmodified lines | ||
| 140 | 206 | yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); |
| 141 | 207 | |
| 142 | 208 | const beforeWrite = yield* workspaceEntries.list({ cwd }); |
| 143 | − expect(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")).toBe( | |
| 144 | − false, | |
| 145 | − ); | |
| 209 | + assert.isFalse(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")); | |
| 146 | 210 | |
| 147 | 211 | yield* workspaceFileSystem.writeFile({ |
| 148 | 212 | cwd, |
| @@ -151,10 +215,10 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 2 unmodified lines | ||
| 151 | 215 | }); |
| 152 | 216 | |
| 153 | 217 | const afterWrite = yield* workspaceEntries.list({ cwd }); |
| 154 | − expect(afterWrite.entries).toEqual( | |
| 155 | − expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]), | |
| 218 | + assert.isTrue( | |
| 219 | + afterWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"), | |
| 156 | 220 | ); |
| 157 | − expect(afterWrite.truncated).toBe(false); | |
| 221 | + assert.isFalse(afterWrite.truncated); | |
| 158 | 222 | }), |
| 159 | 223 | ); |
| 160 | 224 | |
| @@ -173,7 +237,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 12 unmodified lines | ||
| 173 | 237 | }) |
| 174 | 238 | .pipe(Effect.flip); |
| 175 | 239 | |
| 176 | − expect(error.message).toContain( | |
| 240 | + assert.include( | |
| 241 | + error.message, | |
| 177 | 242 | "Workspace file path must be relative to the project root: ../escape.md", |
| 178 | 243 | ); |
| 179 | 244 | |
| @@ -181,7 +246,7 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 1 unmodified line | ||
| 181 | 246 | const escapedStat = yield* fileSystem |
| 182 | 247 | .stat(escapedPath) |
| 183 | 248 | .pipe(Effect.orElseSucceed(() => null)); |
| 184 | − expect(escapedStat).toBeNull(); | |
| 249 | + assert.isNull(escapedStat); | |
| 185 | 250 | }), |
| 186 | 251 | ); |
| 187 | 252 | }); |