import assert from "node:assert/strict"; import { mkdtemp, mkdir, writeFile, readFile, readdir, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { safeReplaceDir } from "../lib/safe-replace-dir.mjs"; // ── Happy path ──────────────────────────────────────────────────────────── test("safeReplaceDir copies source content into the target", async () => { const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-copy-")); try { const safetyRoot = path.join(dir, "root"); const source = path.join(dir, "source"); const target = path.join(safetyRoot, "target"); await mkdir(source, { recursive: true }); await writeFile(path.join(source, "file.txt"), "hello"); await mkdir(safetyRoot, { recursive: true }); await safeReplaceDir(source, target, safetyRoot); const content = await readFile(path.join(target, "file.txt"), "utf8"); assert.equal(content, "hello"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("safeReplaceDir removes existing content before replacing", async () => { const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-stale-")); try { const safetyRoot = path.join(dir, "root"); const source = path.join(dir, "source"); const target = path.join(safetyRoot, "target"); await mkdir(target, { recursive: true }); await writeFile(path.join(target, "old.txt"), "stale"); await mkdir(source, { recursive: true }); await writeFile(path.join(source, "new.txt"), "fresh"); await safeReplaceDir(source, target, safetyRoot); const files = await readdir(target); assert.deepEqual(files.sort(), ["new.txt"]); } finally { await rm(dir, { recursive: true, force: true }); } }); test("safeReplaceDir creates target parent directories if they do not exist", async () => { const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-mkdir-")); try { const safetyRoot = path.join(dir, "root"); const source = path.join(dir, "source"); const target = path.join(safetyRoot, "nested", "target"); await mkdir(source, { recursive: true }); await writeFile(path.join(source, "data.txt"), "data"); await mkdir(safetyRoot, { recursive: true }); // nested parent does NOT exist yet await safeReplaceDir(source, target, safetyRoot); const content = await readFile(path.join(target, "data.txt"), "utf8"); assert.equal(content, "data"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("safeReplaceDir creates deeply nested parent directories (2+ levels missing)", async () => { const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-deep-")); try { const safetyRoot = path.join(dir, "root"); const source = path.join(dir, "source"); // two parent levels (a/b) do NOT exist under safetyRoot const target = path.join(safetyRoot, "a", "b", "target"); await mkdir(source, { recursive: true }); await writeFile(path.join(source, "deep.txt"), "deep"); await mkdir(safetyRoot, { recursive: true }); // a/ and a/b/ intentionally NOT created await safeReplaceDir(source, target, safetyRoot); const content = await readFile(path.join(target, "deep.txt"), "utf8"); assert.equal(content, "deep"); } finally { await rm(dir, { recursive: true, force: true }); } }); // ── Safety checks ───────────────────────────────────────────────────────── test("safeReplaceDir refuses when target is outside the safety root", async () => { const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-outside-")); try { const safetyRoot = path.join(dir, "root"); const source = path.join(dir, "source"); const outside = path.join(dir, "outside"); await mkdir(source, { recursive: true }); await mkdir(safetyRoot, { recursive: true }); await assert.rejects( () => safeReplaceDir(source, outside, safetyRoot), /outside safety root/, ); } finally { await rm(dir, { recursive: true, force: true }); } }); test("safeReplaceDir refuses when target equals the safety root", async () => { const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-same-")); try { const safetyRoot = path.join(dir, "root"); const source = path.join(dir, "source"); await mkdir(source, { recursive: true }); await mkdir(safetyRoot, { recursive: true }); await assert.rejects( () => safeReplaceDir(source, safetyRoot, safetyRoot), /outside safety root/, ); } finally { await rm(dir, { recursive: true, force: true }); } }); test("safeReplaceDir refuses an empty target string", async () => { await assert.rejects( () => safeReplaceDir("/any", "", "/root"), /unsafe target/, ); });