Files
harness-tools/skills/develop/develop-workflow/tests/checkpoint-record.test.mjs
T

334 lines
13 KiB
JavaScript

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("does not remove a lock pathname replaced before cleanup", async () => {
const directory = await temporaryDirectory();
const target = join(directory, "record.md");
const lock = `${target}.lock`;
const replacement = "replacement lock";
let replaced = false;
const io = {
...defaultIo,
open: async (path, flags, mode) => {
const handle = await defaultIo.open(path, flags, mode);
if (path !== lock || flags !== "wx") return handle;
return {
stat: (...args) => handle.stat(...args),
close: async () => {
await handle.close();
await fs.unlink(lock);
await fs.writeFile(lock, replacement, { mode: 0o600 });
replaced = true;
},
};
},
};
await checkpointRecord({ target, input: record(), io });
assert.equal(replaced, true);
assert.equal(await fs.readFile(lock, "utf8"), replacement);
assert.equal(await fs.readFile(target, "utf8"), record());
});
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);
});