import { mkdtemp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { linkFixtureNodeModules } from "../../scripts/lib/fixture-node-modules.ts"; /** * A removal or provider fixture runs `pnpm` inside a throwaway copy of the * repository. pnpm does not recognise the modules directory it finds there and * purges it, and with `CI=true` it does so without a prompt. When * `/node_modules` was a single directory symlink, that purge followed * the link and deleted the repository's own installed dependencies mid-run — * a test suite uninstalling the workspace it was running in. */ describe("fixture node_modules linking", () => { async function sourceTree() { const source = await mkdtemp(path.join(tmpdir(), "fixture-source-")); const modules = path.join(source, "node_modules"); await mkdir(path.join(modules, "left", "lib"), { recursive: true }); await mkdir(path.join(modules, "@scope", "right"), { recursive: true }); await mkdir(path.join(modules, ".bin"), { recursive: true }); await writeFile(path.join(modules, "left", "lib", "index.js"), "export default 1;\n"); await writeFile(path.join(modules, "@scope", "right", "package.json"), "{}\n"); await writeFile(path.join(modules, ".modules.yaml"), "{}\n"); return source; } it("survives a recursive delete of the fixture's node_modules", async () => { const source = await sourceTree(); const fixture = await mkdtemp(path.join(tmpdir(), "fixture-root-")); try { await linkFixtureNodeModules(fixture, source); // The fixture resolves the same packages... expect( await readFile( path.join(fixture, "node_modules/left/lib/index.js"), "utf8", ), ).toContain("export default 1;"); expect( (await readdir(path.join(fixture, "node_modules"))).sort(), ).toEqual([".bin", ".modules.yaml", "@scope", "left"]); // ...and this is exactly what pnpm's purge does. await rm(path.join(fixture, "node_modules"), { recursive: true, force: true, }); // The repository's real dependencies are untouched. expect( await readFile( path.join(source, "node_modules/left/lib/index.js"), "utf8", ), ).toContain("export default 1;"); expect( await stat(path.join(source, "node_modules/@scope/right/package.json")), ).toBeDefined(); expect( (await readdir(path.join(source, "node_modules"))).sort(), ).toEqual([".bin", ".modules.yaml", "@scope", "left"]); } finally { await rm(source, { recursive: true, force: true }); await rm(fixture, { recursive: true, force: true }); } }); it("creates node_modules as a real directory, never a symlink", async () => { const source = await sourceTree(); const fixture = await mkdtemp(path.join(tmpdir(), "fixture-root-")); try { await linkFixtureNodeModules(fixture, source); const { lstat } = await import("node:fs/promises"); const entry = await lstat(path.join(fixture, "node_modules")); expect(entry.isSymbolicLink()).toBe(false); expect(entry.isDirectory()).toBe(true); expect( (await lstat(path.join(fixture, "node_modules/left"))).isSymbolicLink(), ).toBe(true); } finally { await rm(source, { recursive: true, force: true }); await rm(fixture, { recursive: true, force: true }); } }); });