341 lines · 12.5 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:off - FileSystem cannot create a FIFO.import * as NodeChildProcess from "node:child_process"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it, describe, expect } 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";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; 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);}); 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",        });         expect(result).toEqual({          relativePath: "src/index.ts",          contents: "export const answer = 42;\n",          byteLength: 26,          truncated: false,        });      }),    );     it.effect("reads host files outside the workspace root by absolute path", () =>      Effect.gen(function* () {        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;        const path = yield* Path.Path;        const cwd = yield* makeTempDir;        const outsideDir = yield* makeTempDir;        yield* writeTextFile(outsideDir, "cleanup-report.md", "# Report\n");        const absolutePath = path.join(outsideDir, "cleanup-report.md");         const result = yield* workspaceFileSystem.readFile({          cwd,          relativePath: absolutePath,        });         expect(result).toEqual({          relativePath: absolutePath,          contents: "# Report\n",          byteLength: 9,          truncated: false,        });      }),    );     // Needs mkfifo; Windows has no FIFOs to reject.    it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(      "rejects a FIFO without blocking on open",      () =>        Effect.gen(function* () {          const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;          const path = yield* Path.Path;          const cwd = yield* makeTempDir;          const outsideDir = yield* makeTempDir;          const fifoPath = path.join(outsideDir, "pipe");          yield* Effect.promise(            () =>              new Promise<void>((resolve, reject) =>                NodeChildProcess.execFile("mkfifo", [fifoPath], (error) =>                  error ? reject(error) : resolve(),                ),              ),          );           const error = yield* workspaceFileSystem            .readFile({ cwd, relativePath: fifoPath })            .pipe(Effect.flip);           expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError);        }),    );     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);         expect(error.message).toContain(          "Workspace file path must be relative to the project root: ../escape.md",        );      }),    );     it.effect.skipIf(!symlinksSupported)(      "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);          const resolvedWorkspaceRoot = yield* fileSystem.realPath(cwd);          const resolvedPath = yield* fileSystem.realPath(path.join(outsideDir, "secret.txt"));           expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceFilePathEscapeError);          expect(error).toMatchObject({            workspaceRoot: cwd,            relativePath: "linked-secret.txt",            resolvedWorkspaceRoot,            resolvedPath,          });          expect("cause" in error).toBe(false);        }),    );     it.effect("rejects directories without manufacturing an I/O cause", () =>      Effect.gen(function* () {        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;        const fileSystem = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const cwd = yield* makeTempDir;        yield* fileSystem.makeDirectory(path.join(cwd, "src"));         const error = yield* workspaceFileSystem          .readFile({ cwd, relativePath: "src" })          .pipe(Effect.flip);        const resolvedPath = yield* fileSystem.realPath(path.join(cwd, "src"));         expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError);        expect(error).toMatchObject({          workspaceRoot: cwd,          relativePath: "src",          resolvedPath,        });        expect("cause" in error).toBe(false);      }),    );     it.effect("rejects binary files without leaking their contents into the error", () =>      Effect.gen(function* () {        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;        const fileSystem = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const cwd = yield* makeTempDir;        const absolutePath = path.join(cwd, "asset.bin");        yield* fileSystem.writeFile(absolutePath, Uint8Array.from([0x61, 0, 0x62]));         const error = yield* workspaceFileSystem          .readFile({ cwd, relativePath: "asset.bin" })          .pipe(Effect.flip);        const resolvedPath = yield* fileSystem.realPath(absolutePath);         expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceBinaryFileError);        expect(error).toMatchObject({          workspaceRoot: cwd,          relativePath: "asset.bin",          resolvedPath,        });        expect("cause" in error).toBe(false);        expect("contents" in error).toBe(false);      }),    );     it.effect("preserves the real cause and path for I/O failures", () =>      Effect.gen(function* () {        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;        const path = yield* Path.Path;        const cwd = yield* makeTempDir;        const resolvedPath = path.join(cwd, "missing.txt");         const error = yield* workspaceFileSystem          .readFile({ cwd, relativePath: "missing.txt" })          .pipe(Effect.flip);         expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceFileSystemOperationError);        expect(error).toMatchObject({          workspaceRoot: cwd,          relativePath: "missing.txt",          resolvedPath,          operationPath: resolvedPath,          operation: "realpath-target",        });        expect(error.cause).toBeInstanceOf(Error);        expect((error.cause as NodeJS.ErrnoException).code).toBe("ENOENT");      }),    );  });   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);         expect(result).toEqual({ relativePath: "plans/effect-rpc.md" });        expect(saved).toBe("# Plan\n");      }),    );     it.effect("rejects writes by absolute path", () =>      Effect.gen(function* () {        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;        const path = yield* Path.Path;        const cwd = yield* makeTempDir;        const outsideDir = yield* makeTempDir;        const absolutePath = path.join(outsideDir, "cleanup-report.md");         const error = yield* workspaceFileSystem          .writeFile({ cwd, relativePath: absolutePath, contents: "# Edited\n" })          .pipe(Effect.flip);         expect(error).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError);      }),    );     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 });        expect(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")).toBe(          false,        );         yield* workspaceFileSystem.writeFile({          cwd,          relativePath: "plans/effect-rpc.md",          contents: "# Plan\n",        });         const afterWrite = yield* workspaceEntries.list({ cwd });        expect(afterWrite.entries).toEqual(          expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]),        );        expect(afterWrite.truncated).toBe(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);         expect(error.message).toContain(          "Workspace file path must be relative to the project root: ../escape.md",        );         const escapedPath = path.resolve(cwd, "..", "escape.md");        const escapedStat = yield* fileSystem          .stat(escapedPath)          .pipe(Effect.orElseSucceed(() => null));        expect(escapedStat).toBeNull();      }),    );  });});