import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { listProjectWorkspaceFiles, readProjectWorkspaceFile } from "./project-workspace-files.js"; describe("project workspace files", () => { const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) { rmSync(root, { recursive: true, force: true }); } }); function linkedFolderBesideSecret() { const base = mkdtempSync(path.join(tmpdir(), "project-workspace-files-")); roots.push(base); const project = path.join(base, "project"); const outside = path.join(base, "outside"); mkdirSync(path.join(project, "docs"), { recursive: true }); mkdirSync(outside); writeFileSync(path.join(project, "docs", "readme.md"), "inside"); writeFileSync(path.join(outside, "secret.txt"), "outside"); return { project, outside }; } it("lists and reads files inside the linked folder, including symlinks that stay inside", async () => { const { project } = linkedFolderBesideSecret(); symlinkSync(path.join(project, "docs", "readme.md"), path.join(project, "readme-link.md")); const listed = await listProjectWorkspaceFiles(project, "project"); expect(listed.map((entry) => entry.path).sort()).toEqual([ "project/docs", "project/readme-link.md", ]); expect(await listProjectWorkspaceFiles(project, "project/docs")).toEqual([ { path: "project/docs/readme.md", kind: "file", size: 6 }, ]); const read = await readProjectWorkspaceFile(project, "project/readme-link.md"); expect(new TextDecoder().decode(read)).toBe("inside"); }); it("keeps symlinks from leaving the linked folder", async () => { const { project, outside } = linkedFolderBesideSecret(); symlinkSync(outside, path.join(project, "escape")); symlinkSync(path.join(outside, "secret.txt"), path.join(project, "secret-link.txt")); symlinkSync(path.join(project, "missing"), path.join(project, "dangling")); const listed = await listProjectWorkspaceFiles(project, "project"); expect(listed.map((entry) => entry.path)).toEqual(["project/docs"]); await expect(listProjectWorkspaceFiles(project, "project/escape")).rejects.toThrow(/escapes/i); await expect(readProjectWorkspaceFile(project, "project/escape/secret.txt")).rejects.toThrow( /escapes/i, ); await expect(readProjectWorkspaceFile(project, "project/secret-link.txt")).rejects.toThrow( /escapes/i, ); }); });