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 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 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 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| @@ -18,6 +18,8 @@ const ProjectLayer = WorkspaceFileSystemLive.pipe( · 12 unmodified lines | |||
| 18 | Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePathsLive))), | 18 | Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePathsLive))), |
| 19 | ); | 19 | ); |
| 20 | 20 | ||
| 21 | const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; | ||
| 22 | |||
| 21 | const TestLayer = Layer.empty.pipe( | 23 | const TestLayer = Layer.empty.pipe( |
| 22 | Layer.provideMerge(ProjectLayer), | 24 | Layer.provideMerge(ProjectLayer), |
| 23 | Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePathsLive))), | 25 | Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePathsLive))), |
| @@ -52,6 +54,20 @@ const writeTextFile = Effect.fn("writeTextFile")(function* ( · 28 unmodified lines | |||
| 52 | yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie); | 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 | it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => { | 71 | it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => { |
| 56 | describe("readFile", () => { | 72 | describe("readFile", () => { |
| 57 | it.effect("reads UTF-8 files relative to the workspace root", () => | 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 | relativePath: "src/index.ts", | 81 | relativePath: "src/index.ts", |
| 66 | }); | 82 | }); |
| 67 | 83 | ||
| 68 | expect(result).toEqual({ | 84 | assert.deepStrictEqual(result, { |
| 69 | relativePath: "src/index.ts", | 85 | relativePath: "src/index.ts", |
| 70 | contents: "export const answer = 42;\n", | 86 | contents: "export const answer = 42;\n", |
| 71 | byteLength: 26, | 87 | byteLength: 26, |
| @@ -83,7 +99,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 11 unmodified lines | |||
| 83 | .readFile({ cwd, relativePath: "../escape.md" }) | 99 | .readFile({ cwd, relativePath: "../escape.md" }) |
| 84 | .pipe(Effect.flip); | 100 | .pipe(Effect.flip); |
| 85 | 101 | ||
| 86 | expect(error.message).toContain( | 102 | assert.include( |
| 103 | error.message, | ||
| 87 | "Workspace file path must be relative to the project root: ../escape.md", | 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 | .readFile({ cwd, relativePath: "linked-secret.txt" }) | 123 | .readFile({ cwd, relativePath: "linked-secret.txt" }) |
| 107 | .pipe(Effect.flip); | 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 | .readFileString(path.join(cwd, "plans/effect-rpc.md")) | 193 | .readFileString(path.join(cwd, "plans/effect-rpc.md")) |
| 128 | .pipe(Effect.orDie); | 194 | .pipe(Effect.orDie); |
| 129 | 195 | ||
| 130 | expect(result).toEqual({ relativePath: "plans/effect-rpc.md" }); | 196 | assert.deepStrictEqual(result, { relativePath: "plans/effect-rpc.md" }); |
| 131 | expect(saved).toBe("# Plan\n"); | 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 | yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); | 206 | yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); |
| 141 | 207 | ||
| 142 | const beforeWrite = yield* workspaceEntries.list({ cwd }); | 208 | const beforeWrite = yield* workspaceEntries.list({ cwd }); |
| 143 | expect(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")).toBe( | 209 | assert.isFalse(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")); |
| 144 | false, | ||
| 145 | ); | ||
| 146 | 210 | ||
| 147 | yield* workspaceFileSystem.writeFile({ | 211 | yield* workspaceFileSystem.writeFile({ |
| 148 | cwd, | 212 | cwd, |
| @@ -151,10 +215,10 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 2 unmodified lines | |||
| 151 | }); | 215 | }); |
| 152 | 216 | ||
| 153 | const afterWrite = yield* workspaceEntries.list({ cwd }); | 217 | const afterWrite = yield* workspaceEntries.list({ cwd }); |
| 154 | expect(afterWrite.entries).toEqual( | 218 | assert.isTrue( |
| 155 | expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]), | 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 | .pipe(Effect.flip); | 238 | .pipe(Effect.flip); |
| 175 | 239 | ||
| 176 | expect(error.message).toContain( | 240 | assert.include( |
| 241 | error.message, | ||
| 177 | "Workspace file path must be relative to the project root: ../escape.md", | 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 | const escapedStat = yield* fileSystem | 246 | const escapedStat = yield* fileSystem |
| 182 | .stat(escapedPath) | 247 | .stat(escapedPath) |
| 183 | .pipe(Effect.orElseSucceed(() => null)); | 248 | .pipe(Effect.orElseSucceed(() => null)); |
| 184 | expect(escapedStat).toBeNull(); | 249 | assert.isNull(escapedStat); |
| 185 | }), | 250 | }), |
| 186 | ); | 251 | ); |
| 187 | }); | 252 | }); |