| @@ -1,9 +1,11 @@ |
| 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, describe, it } 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"; |
| | 6 | import * as Option from "effect/Option"; |
| 6 | import * as Path from "effect/Path"; | 7 | import * as Path from "effect/Path"; |
| | 8 | import * as PlatformError from "effect/PlatformError"; |
| 7 | | 9 | |
| 8 | import * as ServerConfig from "../config.ts"; | 10 | import * as ServerConfig from "../config.ts"; |
| 9 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | 11 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| @@ -64,7 +66,7 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 54 unmodified lines |
| 64 | relativePath: "src/index.ts", | 66 | relativePath: "src/index.ts", |
| 65 | }); | 67 | }); |
| 66 | | 68 | |
| 67 | expect(result).toEqual({ | 69 | assert.deepStrictEqual(result, { |
| 68 | relativePath: "src/index.ts", | 70 | relativePath: "src/index.ts", |
| 69 | contents: "export const answer = 42;\n", | 71 | contents: "export const answer = 42;\n", |
| 70 | byteLength: 26, | 72 | byteLength: 26, |
| @@ -82,7 +84,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 11 unmodified lines |
| 82 | .readFile({ cwd, relativePath: "../escape.md" }) | 84 | .readFile({ cwd, relativePath: "../escape.md" }) |
| 83 | .pipe(Effect.flip); | 85 | .pipe(Effect.flip); |
| 84 | | 86 | |
| 85 | expect(error.message).toContain( | 87 | assert.include( |
| | 88 | error.message, |
| 86 | "Workspace file path must be relative to the project root: ../escape.md", | 89 | "Workspace file path must be relative to the project root: ../escape.md", |
| 87 | ); | 90 | ); |
| 88 | }), | 91 | }), |
| @@ -107,14 +110,14 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 18 unmodified lines |
| 107 | const resolvedWorkspaceRoot = yield* fileSystem.realPath(cwd); | 110 | const resolvedWorkspaceRoot = yield* fileSystem.realPath(cwd); |
| 108 | const resolvedPath = yield* fileSystem.realPath(path.join(outsideDir, "secret.txt")); | 111 | const resolvedPath = yield* fileSystem.realPath(path.join(outsideDir, "secret.txt")); |
| 109 | | 112 | |
| 110 | expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceFilePathEscapeError); | 113 | assert.strictEqual(error._tag, "WorkspaceFilePathEscapeError"); |
| 111 | expect(error).toMatchObject({ | 114 | assert.deepInclude(error, { |
| 112 | workspaceRoot: cwd, | 115 | workspaceRoot: cwd, |
| 113 | relativePath: "linked-secret.txt", | 116 | relativePath: "linked-secret.txt", |
| 114 | resolvedWorkspaceRoot, | 117 | resolvedWorkspaceRoot, |
| 115 | resolvedPath, | 118 | resolvedPath, |
| 116 | }); | 119 | }); |
| 117 | expect("cause" in error).toBe(false); | 120 | assert.notProperty(error, "cause"); |
| 118 | }), | 121 | }), |
| 119 | ); | 122 | ); |
| 120 | | 123 | |
| @@ -131,13 +134,13 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 10 unmodified lines |
| 131 | .pipe(Effect.flip); | 134 | .pipe(Effect.flip); |
| 132 | const resolvedPath = yield* fileSystem.realPath(path.join(cwd, "src")); | 135 | const resolvedPath = yield* fileSystem.realPath(path.join(cwd, "src")); |
| 133 | | 136 | |
| 134 | expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError); | 137 | assert.strictEqual(error._tag, "WorkspacePathNotFileError"); |
| 135 | expect(error).toMatchObject({ | 138 | assert.deepInclude(error, { |
| 136 | workspaceRoot: cwd, | 139 | workspaceRoot: cwd, |
| 137 | relativePath: "src", | 140 | relativePath: "src", |
| 138 | resolvedPath, | 141 | resolvedPath, |
| 139 | }); | 142 | }); |
| 140 | expect("cause" in error).toBe(false); | 143 | assert.notProperty(error, "cause"); |
| 141 | }), | 144 | }), |
| 142 | ); | 145 | ); |
| 143 | | 146 | |
| @@ -155,14 +158,31 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 11 unmodified lines |
| 155 | .pipe(Effect.flip); | 158 | .pipe(Effect.flip); |
| 156 | const resolvedPath = yield* fileSystem.realPath(absolutePath); | 159 | const resolvedPath = yield* fileSystem.realPath(absolutePath); |
| 157 | | 160 | |
| 158 | expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceBinaryFileError); | 161 | assert.strictEqual(error._tag, "WorkspaceBinaryFileError"); |
| 159 | expect(error).toMatchObject({ | 162 | assert.deepInclude(error, { |
| 160 | workspaceRoot: cwd, | 163 | workspaceRoot: cwd, |
| 161 | relativePath: "asset.bin", | 164 | relativePath: "asset.bin", |
| 162 | resolvedPath, | 165 | resolvedPath, |
| 163 | }); | 166 | }); |
| 164 | expect("cause" in error).toBe(false); | 167 | assert.notProperty(error, "cause"); |
| 165 | expect("contents" in error).toBe(false); | 168 | assert.notProperty(error, "contents"); |
| | 169 | }), |
| | 170 | ); |
| | 171 | |
| | 172 | it.effect("reads at most the preview limit", () => |
| | 173 | Effect.gen(function* () { |
| | 174 | const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; |
| | 175 | const cwd = yield* makeTempDir; |
| | 176 | yield* writeTextFile(cwd, "large.txt", "a".repeat(1024 * 1024 + 1)); |
| | 177 | |
| | 178 | const result = yield* workspaceFileSystem.readFile({ |
| | 179 | cwd, |
| | 180 | relativePath: "large.txt", |
| | 181 | }); |
| | 182 | |
| | 183 | assert.strictEqual(result.contents.length, 1024 * 1024); |
| | 184 | assert.strictEqual(result.byteLength, 1024 * 1024 + 1); |
| | 185 | assert.isTrue(result.truncated); |
| 166 | }), | 186 | }), |
| 167 | ); | 187 | ); |
| 168 | | 188 | |
| @@ -177,16 +197,16 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 8 unmodified lines |
| 177 | .readFile({ cwd, relativePath: "missing.txt" }) | 197 | .readFile({ cwd, relativePath: "missing.txt" }) |
| 178 | .pipe(Effect.flip); | 198 | .pipe(Effect.flip); |
| 179 | | 199 | |
| 180 | expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceFileSystemOperationError); | 200 | assert.strictEqual(error._tag, "WorkspaceFileSystemOperationError"); |
| 181 | expect(error).toMatchObject({ | 201 | assert.deepInclude(error, { |
| 182 | workspaceRoot: cwd, | 202 | workspaceRoot: cwd, |
| 183 | relativePath: "missing.txt", | 203 | relativePath: "missing.txt", |
| 184 | resolvedPath, | 204 | resolvedPath, |
| 185 | operationPath: resolvedPath, | 205 | operationPath: resolvedPath, |
| 186 | operation: "realpath-target", | 206 | operation: "realpath-target", |
| 187 | }); | 207 | }); |
| 188 | expect(error.cause).toBeInstanceOf(Error); | 208 | assert.instanceOf(error.cause, PlatformError.PlatformError); |
| 189 | expect((error.cause as NodeJS.ErrnoException).code).toBe("ENOENT"); | 209 | assert.strictEqual((error.cause as PlatformError.PlatformError).reason._tag, "NotFound"); |
| 190 | }), | 210 | }), |
| 191 | ); | 211 | ); |
| 192 | }); | 212 | }); |
| @@ -207,8 +227,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 14 unmodified lines |
| 207 | .readFileString(path.join(cwd, "plans/effect-rpc.md")) | 227 | .readFileString(path.join(cwd, "plans/effect-rpc.md")) |
| 208 | .pipe(Effect.orDie); | 228 | .pipe(Effect.orDie); |
| 209 | | 229 | |
| 210 | expect(result).toEqual({ relativePath: "plans/effect-rpc.md" }); | 230 | assert.deepStrictEqual(result, { relativePath: "plans/effect-rpc.md" }); |
| 211 | expect(saved).toBe("# Plan\n"); | 231 | assert.strictEqual(saved, "# Plan\n"); |
| 212 | }), | 232 | }), |
| 213 | ); | 233 | ); |
| 214 | | 234 | |
| @@ -220,8 +240,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 5 unmodified lines |
| 220 | yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); | 240 | yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); |
| 221 | | 241 | |
| 222 | const beforeWrite = yield* workspaceEntries.list({ cwd }); | 242 | const beforeWrite = yield* workspaceEntries.list({ cwd }); |
| 223 | expect(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")).toBe( | 243 | assert.isFalse( |
| 224 | false, | 244 | beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"), |
| 225 | ); | 245 | ); |
| 226 | | 246 | |
| 227 | yield* workspaceFileSystem.writeFile({ | 247 | yield* workspaceFileSystem.writeFile({ |
| @@ -231,10 +251,10 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 3 unmodified lines |
| 231 | }); | 251 | }); |
| 232 | | 252 | |
| 233 | const afterWrite = yield* workspaceEntries.list({ cwd }); | 253 | const afterWrite = yield* workspaceEntries.list({ cwd }); |
| 234 | expect(afterWrite.entries).toEqual( | 254 | assert.isTrue( |
| 235 | expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]), | 255 | afterWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"), |
| 236 | ); | 256 | ); |
| 237 | expect(afterWrite.truncated).toBe(false); | 257 | assert.isFalse(afterWrite.truncated); |
| 238 | }), | 258 | }), |
| 239 | ); | 259 | ); |
| 240 | | 260 | |
| @@ -253,15 +273,14 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 12 unmodified lines |
| 253 | }) | 273 | }) |
| 254 | .pipe(Effect.flip); | 274 | .pipe(Effect.flip); |
| 255 | | 275 | |
| 256 | expect(error.message).toContain( | 276 | assert.include( |
| | 277 | error.message, |
| 257 | "Workspace file path must be relative to the project root: ../escape.md", | 278 | "Workspace file path must be relative to the project root: ../escape.md", |
| 258 | ); | 279 | ); |
| 259 | | 280 | |
| 260 | const escapedPath = path.resolve(cwd, "..", "escape.md"); | 281 | const escapedPath = path.resolve(cwd, "..", "escape.md"); |
| 261 | const escapedStat = yield* fileSystem | 282 | const escapedStat = yield* fileSystem.stat(escapedPath).pipe(Effect.option); |
| 262 | .stat(escapedPath) | 283 | assert.isTrue(Option.isNone(escapedStat)); |
| 263 | .pipe(Effect.orElseSucceed(() => null)); | | |
| 264 | expect(escapedStat).toBeNull(); | | |
| 265 | }), | 284 | }), |
| 266 | ); | 285 | ); |
| 267 | }); | 286 | }); |