import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { promises as fs } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { once } from "node:events"; import { spawn } from "node:child_process"; import test from "node:test"; import { checkpointRecord, defaultIo, REQUIRED_SECTIONS, } from "../scripts/checkpoint-record.mjs"; const scriptPath = fileURLToPath(new URL("../scripts/checkpoint-record.mjs", import.meta.url)); function record(title = "Workflow checkpoint", sections = REQUIRED_SECTIONS) { return `# ${title}\n\n${sections.map((section) => `## ${section}\n\ncheckpoint data\n`).join("\n")}`; } function digest(value) { return createHash("sha256").update(value).digest("hex"); } async function temporaryDirectory() { return fs.mkdtemp(join(tmpdir(), "checkpoint-record-test-")); } async function exists(path) { try { await fs.access(path); return true; } catch { return false; } } function faultIo({ write, flush, rename, directoryFlush, cleanup } = {}) { return { ...defaultIo, open: async (path, flags, mode) => { const handle = await defaultIo.open(path, flags, mode); const isTemp = flags === "wx" && path.includes(".tmp-"); const isDirectory = flags === "r"; if (!(isTemp && (write || flush)) && !(isDirectory && directoryFlush)) return handle; return { write: async (...args) => { if (isTemp && write) throw new Error("interrupted write"); return handle.write(...args); }, sync: async () => { if ((isTemp && flush) || (isDirectory && directoryFlush)) throw new Error("interrupted flush"); return handle.sync(); }, close: (...args) => handle.close(...args), }; }, rename: async (...args) => { if (rename) throw new Error("rename failed"); return defaultIo.rename(...args); }, unlink: async (path) => { if (cleanup) throw new Error("cleanup failed"); return defaultIo.unlink(path); }, }; } async function assertRejectsCode(promise, code) { await assert.rejects(promise, (error) => { assert.equal(error.code, code); return true; }); } test("creates a missing record and parent directory atomically with restrictive modes", async () => { const directory = await temporaryDirectory(); const target = join(directory, "nested", "record.md"); const input = record(); const result = await checkpointRecord({ target, input }); assert.equal(result.created, true); assert.equal(result.sha256, digest(input)); assert.equal(await fs.readFile(target, "utf8"), input); assert.equal((await fs.stat(target)).mode & 0o777, 0o600); assert.equal((await fs.stat(join(directory, "nested"))).mode & 0o777, 0o700); assert.equal(await exists(`${target}.lock`), false); assert.equal( (await fs.readdir(join(directory, "nested"))).filter((name) => name.includes(".tmp-")).length, 0, ); }); test("replaces a record only with its current SHA-256 digest", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const before = record("Before"); const after = record("After"); await fs.writeFile(target, before, { mode: 0o600 }); const result = await checkpointRecord({ target, input: after, expectedSha256: digest(before) }); assert.equal(result.created, false); assert.equal(await fs.readFile(target, "utf8"), after); assert.equal(result.sha256, digest(after)); }); test("rejects malformed, missing, duplicate, and unknown sections before touching the target", async () => { const cases = [ ["missing H1", record().replace(/^# .*\n\n/, "")], ["missing section", record().replace("## Review\n\ncheckpoint data\n\n", "")], ["duplicate section", `${record()}\n## Status\n\nagain\n`], ["unknown section", `${record()}\n## Unrecognized\n\nnope\n`], ]; for (const [name, input] of cases) { const directory = await temporaryDirectory(); const target = join(directory, `${name.replaceAll(" ", "-")}.md`); await assertRejectsCode(checkpointRecord({ target, input }), "INVALID_RECORD"); assert.equal(await exists(target), false, `${name} created target`); assert.equal(await exists(`${target}.lock`), false, `${name} created lock`); } }); test("requires a digest for replacement and rejects stale digests without changing bytes", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const before = record("Before"); const after = record("After"); await fs.writeFile(target, before, { mode: 0o600 }); await assertRejectsCode(checkpointRecord({ target, input: after }), "EXPECTED_SHA256_REQUIRED"); await assertRejectsCode( checkpointRecord({ target, input: after, expectedSha256: digest(record("Other")) }), "STALE_DIGEST", ); assert.equal(await fs.readFile(target, "utf8"), before); assert.equal(await exists(`${target}.lock`), false); }); test("serializes concurrent updates with an exclusive per-target lock", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const lock = `${target}.lock`; const first = record("First"); const second = record("Second"); let firstLockOpened; const lockOpened = new Promise((resolve) => { firstLockOpened = resolve; }); let releaseFirstLock; const firstLockHeld = new Promise((resolve) => { releaseFirstLock = resolve; }); let firstOpen = true; const io = { ...defaultIo, open: async (path, flags, mode) => { const handle = await defaultIo.open(path, flags, mode); if (path === lock && flags === "wx" && firstOpen) { firstOpen = false; firstLockOpened(); await firstLockHeld; } return handle; }, }; const firstPromise = checkpointRecord({ target, input: first, io }); await lockOpened; const secondPromise = checkpointRecord({ target, input: second, io }); await assertRejectsCode(secondPromise, "LOCK_EXISTS"); releaseFirstLock(); await firstPromise; assert.equal(await fs.readFile(target, "utf8"), first); assert.equal(await exists(`${target}.lock`), false); }); test("fails closed on a pre-existing stale lock and leaves it for recovery", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const lock = `${target}.lock`; const input = record(); await fs.writeFile(lock, "stale lock", { mode: 0o600 }); await assertRejectsCode(checkpointRecord({ target, input }), "LOCK_EXISTS"); assert.equal(await fs.readFile(lock, "utf8"), "stale lock"); assert.equal(await exists(target), false); }); test("preserves pre-existing stale temporary files", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const stale = `${target}.tmp-stale-recovery`; await fs.writeFile(stale, "recover me", { mode: 0o600 }); await checkpointRecord({ target, input: record() }); assert.equal(await fs.readFile(stale, "utf8"), "recover me"); assert.equal(await exists(`${target}.lock`), false); }); test("cleans its own temporary file after an interrupted write and preserves the prior target", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const before = record("Before"); await fs.writeFile(target, before, { mode: 0o600 }); await assertRejectsCode( checkpointRecord({ target, input: record("After"), expectedSha256: digest(before), io: faultIo({ write: true }) }), "WRITE_FAILED", ); assert.equal(await fs.readFile(target, "utf8"), before); assert.equal(await exists(`${target}.lock`), false); assert.deepEqual((await fs.readdir(directory)).filter((name) => name.includes(".tmp-")), []); }); test("cleans its own temporary file after a flush failure and does not mask the primary error", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const before = record("Before"); await fs.writeFile(target, before, { mode: 0o600 }); const cleanupFailingIo = faultIo({ flush: true, cleanup: true }); await assertRejectsCode( checkpointRecord({ target, input: record("After"), expectedSha256: digest(before), io: cleanupFailingIo }), "TEMP_SYNC_FAILED", ); assert.equal(await fs.readFile(target, "utf8"), before); // Remove artifacts made intentionally unremovable by this fault adapter. for (const name of await fs.readdir(directory)) await fs.unlink(join(directory, name)); }); test("rename failure leaves the previous target byte-for-byte intact", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const before = record("Before"); await fs.writeFile(target, before, { mode: 0o600 }); // The adapter's rename failure is wrapped as an atomic operation error. await assert.rejects( checkpointRecord({ target, input: record("After"), expectedSha256: digest(before), io: faultIo({ rename: true }) }), (error) => { assert.equal(error.code, "RENAME_FAILED"); assert.match(error.message, /rename failed/); return true; }, ); assert.equal(await fs.readFile(target, "utf8"), before); assert.equal(await exists(`${target}.lock`), false); }); test("directory flush failure is reported after rename and leaves no lock or owned temp", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const before = record("Before"); const after = record("After"); await fs.writeFile(target, before, { mode: 0o600 }); await assertRejectsCode( checkpointRecord({ target, input: after, expectedSha256: digest(before), io: faultIo({ directoryFlush: true }) }), "DIRECTORY_SYNC_FAILED", ); assert.equal(await fs.readFile(target, "utf8"), after); assert.equal(await exists(`${target}.lock`), false); assert.deepEqual((await fs.readdir(directory)).filter((name) => name.includes(".tmp-")), []); }); test("cleanup errors never mask a lock or write failure", async () => { const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const io = faultIo({ write: true, cleanup: true }); await assert.rejects(checkpointRecord({ target, input: record(), io }), /interrupted write/); for (const name of await fs.readdir(directory)) await fs.unlink(join(directory, name)); }); test("strict CLI reports usage and operation errors with exit code 2", async () => { const missingTarget = spawn(process.execPath, [scriptPath], { stdio: ["ignore", "pipe", "pipe"] }); const [missingResult] = await once(missingTarget, "close"); assert.equal(missingResult, 2); const directory = await temporaryDirectory(); const target = join(directory, "record.md"); const child = spawn(process.execPath, [scriptPath, target], { stdio: ["pipe", "pipe", "pipe"] }); child.stdin.end(record()); const [result] = await once(child, "close"); assert.equal(result, 0); assert.equal(await fs.readFile(target, "utf8"), record()); const inputPath = join(directory, "input.md"); const inputRecord = record("From input file"); await fs.writeFile(inputPath, inputRecord); const inputTarget = join(directory, "input-target.md"); const inputChild = spawn(process.execPath, [scriptPath, inputTarget, "--input", inputPath], { stdio: ["ignore", "pipe", "pipe"] }); const [inputResult] = await once(inputChild, "close"); assert.equal(inputResult, 0); assert.equal(await fs.readFile(inputTarget, "utf8"), inputRecord); const bad = spawn(process.execPath, [scriptPath, target, "--expected-sha256", "bad"], { stdio: ["pipe", "pipe", "pipe"] }); bad.stdin.end(record("Changed")); const [badResult] = await once(bad, "close"); assert.equal(badResult, 2); });