import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; const doc = readFileSync(new URL("../SKILL.md", import.meta.url), "utf8"); const snippet = doc.match(/## Safe Edit Pattern[\s\S]*?```bash\n([\s\S]*?)```/)[1]; for (const symlink of [false, true]) { test(`backup and rollback preserve ${symlink ? "symlinked" : "regular"} config`, { skip: process.platform !== "linux" }, () => { const dir = mkdtempSync(join(tmpdir(), "linux-userland-backup-")); try { const target = join(dir, "config target"); const file = symlink ? join(dir, "config link") : target; writeFileSync(target, "original\n"); if (symlink) symlinkSync(target, file); const script = snippet .replace("/path/to/file", '"$CONFIG_FILE"') .replace('# edit "$file"', 'printf "changed\\n" > "$file"') .replace("# run validator/reload", 'test "$(cat "$backup")" = original || exit 1') .replace("# rollback if bad: ", ""); const result = spawnSync("sh", ["-eu", "-c", script], { env: { ...process.env, CONFIG_FILE: file }, encoding: "utf8", }); assert.equal(result.status, 0, result.stderr); assert.equal(readFileSync(target, "utf8"), "original\n"); assert.equal(lstatSync(file).isSymbolicLink(), symlink); const backups = readdirSync(dir).filter((name) => name.includes(".bak.")); assert.equal(backups.length, 1); assert.ok(lstatSync(join(dir, backups[0])).isFile()); } finally { rmSync(dir, { recursive: true, force: true }); } }); } for (const kind of ["missing", "dangling symlink", "directory"]) { test(`refuse ${kind} without creating a backup`, { skip: process.platform !== "linux" }, () => { const dir = mkdtempSync(join(tmpdir(), "linux-userland-backup-")); try { const file = kind === "directory" ? dir : join(dir, "config"); if (kind === "dangling symlink") symlinkSync(join(dir, "missing"), file); const before = readdirSync(dir); const result = spawnSync("sh", ["-eu", "-c", snippet.replace("/path/to/file", '"$CONFIG_FILE"')], { env: { ...process.env, CONFIG_FILE: file }, encoding: "utf8", }); assert.equal(result.status, 1, result.stderr); assert.deepEqual(readdirSync(dir), before); } finally { rmSync(dir, { recursive: true, force: true }); } }); }