import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it, describe } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as ServerConfig from "../config.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as WorkspaceEntries from "./WorkspaceEntries.ts"; import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts"; import * as WorkspacePaths from "./WorkspacePaths.ts"; const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; const ProjectLayer = WorkspaceFileSystem.layer.pipe( Layer.provide(WorkspacePaths.layer), Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))), ); const TestLayer = Layer.empty.pipe( Layer.provideMerge(ProjectLayer), Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))), Layer.provideMerge(WorkspacePaths.layer), Layer.provideMerge(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer))), Layer.provide( ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-workspace-files-test-", }), ), Layer.provideMerge(NodeServices.layer), ); const makeTempDir = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; return yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-workspace-files-", }); }); const writeTextFile = Effect.fn("writeTextFile")(function* ( cwd: string, relativePath: string, contents = "", ) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const absolutePath = path.join(cwd, relativePath); yield* fileSystem .makeDirectory(path.dirname(absolutePath), { recursive: true }) .pipe(Effect.orDie); yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie); }); const writeBinaryFile = Effect.fn("writeBinaryFile")(function* ( cwd: string, relativePath: string, contents: Uint8Array, ) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const absolutePath = path.join(cwd, relativePath); yield* fileSystem .makeDirectory(path.dirname(absolutePath), { recursive: true }) .pipe(Effect.orDie); yield* fileSystem.writeFile(absolutePath, contents).pipe(Effect.orDie); }); it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => { describe("readFile", () => { it.effect("reads UTF-8 files relative to the workspace root", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "src/index.ts", "export const answer = 42;\n"); const result = yield* workspaceFileSystem.readFile({ cwd, relativePath: "src/index.ts", }); assert.deepStrictEqual(result, { relativePath: "src/index.ts", contents: "export const answer = 42;\n", byteLength: 26, truncated: false, }); }), ); it.effect("rejects reads outside the workspace root", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; const error = yield* workspaceFileSystem .readFile({ cwd, relativePath: "../escape.md" }) .pipe(Effect.flip); assert.match( error.message, /Workspace file path must be relative to the project root: \.\.\/escape\.md/, ); }), ); it.effect("truncates text files larger than the preview byte limit", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "large.txt", `${"a".repeat(PROJECT_READ_FILE_MAX_BYTES)}tail`); const result = yield* workspaceFileSystem.readFile({ cwd, relativePath: "large.txt", }); assert.strictEqual(result.byteLength, PROJECT_READ_FILE_MAX_BYTES + 4); assert.strictEqual(result.contents.length, PROJECT_READ_FILE_MAX_BYTES); assert.strictEqual(result.contents, "a".repeat(PROJECT_READ_FILE_MAX_BYTES)); assert.strictEqual(result.truncated, true); }), ); it.effect("rejects binary files before decoding them as text", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; yield* writeBinaryFile(cwd, "image.bin", new Uint8Array([0x66, 0x6f, 0x00, 0x6f])); const error = yield* workspaceFileSystem .readFile({ cwd, relativePath: "image.bin" }) .pipe(Effect.flip); assert.strictEqual( error.message, `Workspace file operation 'workspaceFileSystem.readFile' failed for 'image.bin' in '${cwd}'.`, ); assert.ok(error.cause instanceof Error); assert.strictEqual((error.cause as Error).message, "Binary files cannot be previewed as text."); }), ); it.effect("rejects symlinks that resolve outside the workspace root", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const cwd = yield* makeTempDir; const outsideDir = yield* makeTempDir; yield* writeTextFile(outsideDir, "secret.txt", "outside\n"); yield* fileSystem.symlink( path.join(outsideDir, "secret.txt"), path.join(cwd, "linked-secret.txt"), ); const error = yield* workspaceFileSystem .readFile({ cwd, relativePath: "linked-secret.txt" }) .pipe(Effect.flip); assert.strictEqual( error.message, `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`, ); assert.ok(error.cause instanceof Error); assert.strictEqual( (error.cause as Error).message, "Workspace file path resolves outside the project root.", ); }), ); }); describe("writeFile", () => { it.effect("writes files relative to the workspace root", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const result = yield* workspaceFileSystem.writeFile({ cwd, relativePath: "plans/effect-rpc.md", contents: "# Plan\n", }); const saved = yield* fileSystem .readFileString(path.join(cwd, "plans/effect-rpc.md")) .pipe(Effect.orDie); assert.deepStrictEqual(result, { relativePath: "plans/effect-rpc.md" }); assert.strictEqual(saved, "# Plan\n"); }), ); it.effect("invalidates workspace entry search cache after writes", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); const beforeWrite = yield* workspaceEntries.list({ cwd }); assert.strictEqual( beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"), false, ); yield* workspaceFileSystem.writeFile({ cwd, relativePath: "plans/effect-rpc.md", contents: "# Plan\n", }); const afterWrite = yield* workspaceEntries.list({ cwd }); assert.strictEqual( afterWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"), true, ); assert.strictEqual(afterWrite.truncated, false); }), ); it.effect("rejects writes outside the workspace root", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const error = yield* workspaceFileSystem .writeFile({ cwd, relativePath: "../escape.md", contents: "# nope\n", }) .pipe(Effect.flip); assert.match( error.message, /Workspace file path must be relative to the project root: \.\.\/escape\.md/, ); const escapedPath = path.resolve(cwd, "..", "escape.md"); const escapedExists = yield* fileSystem.exists(escapedPath); assert.strictEqual(escapedExists, false); }), ); }); });