anthonyfitzpatrick_manuscri.../tests/safe-binary-writer.ts
2026-07-13 23:36:18 +02:00

50 lines
14 KiB
TypeScript

/**
* SafeBinaryWriter failure-injection suite.
* Fake backends prove destination preservation, rollback, cancellation boundaries,
* cleanup, and critical recovery guidance without real disk failures.
*/
import assert from "node:assert/strict";
import { createTestDocx } from "./docx-test-fixture";
import { validateDocxBytes } from "../src/docx-validator";
import { SafeBinaryWriteError, SafeBinaryWriter, type BinaryEntry, type SafeBinaryBackend } from "../src/safe-binary-writer";
import { strToU8, zipSync } from "fflate";
type Operation = "exists" | "read" | "write" | "rename" | "remove" | "list";
class FakeBackend implements SafeBinaryBackend {
readonly files = new Map<string, { bytes: Uint8Array; mtime: number }>(); readonly operations: Array<{ operation: Operation; path: string; target?: string }> = [];
fail?: (operation: Operation, path: string, target?: string) => boolean; corruptRead?: (path: string, bytes: Uint8Array) => Uint8Array;
constructor(readonly kind: "filesystem" | "adapter") {}
seed(path: string, bytes: Uint8Array, mtime = Date.now()): void { this.files.set(path, { bytes: bytes.slice(), mtime }); }
async exists(path: string): Promise<boolean> { this.hit("exists", path); return this.files.has(path); }
async read(path: string): Promise<Uint8Array> { this.hit("read", path); const value = this.files.get(path); if (!value) throw new Error(`Missing ${path}`); const bytes = value.bytes.slice(); return this.corruptRead?.(path, bytes) ?? bytes; }
async write(path: string, bytes: Uint8Array): Promise<void> { this.hit("write", path); this.files.set(path, { bytes: bytes.slice(), mtime: Date.now() }); }
async rename(from: string, to: string): Promise<void> { this.hit("rename", from, to); const value = this.files.get(from); if (!value) throw new Error(`Missing ${from}`); if (this.files.has(to)) throw new Error(`Exists ${to}`); this.files.set(to, value); this.files.delete(from); }
async remove(path: string): Promise<void> { this.hit("remove", path); this.files.delete(path); }
async list(folder: string): Promise<BinaryEntry[]> { this.hit("list", folder); const prefix = folder ? `${folder}/` : ""; return [...this.files.entries()].filter(([path]) => path.startsWith(prefix) && !path.slice(prefix.length).includes("/")).map(([path, value]) => ({ path, mtime: value.mtime })); }
private hit(operation: Operation, path: string, target?: string): void { this.operations.push({ operation, path, target }); if (this.fail?.(operation, path, target)) throw new Error(`Injected ${operation} failure for ${path}`); }
}
const tests: Array<[string, () => void | Promise<void>]> = []; const test = (name: string, action: () => void | Promise<void>): void => { tests.push([name, action]); };
const generated = createTestDocx("Validated prose.", "Safe Book"); const original = createTestDocx("Original prose.", "Original"); const destination = "Exports/Safe Book.docx";
const artifacts = (backend: FakeBackend): string[] => [...backend.files.keys()].filter((path) => /manuscript-compiler/.test(path));
test("validator rejects unreadable and structurally incomplete DOCX packages", () => { assert.equal(validateDocxBytes(new Uint8Array([1, 2, 3])).valid, false); const incomplete = zipSync({ "word/document.xml": strToU8("<w:document><w:body/></w:document>") }); const invalid = validateDocxBytes(incomplete); assert.equal(invalid.valid, false); assert.ok(invalid.errors.some((error) => /Content_Types|styles|relationships/i.test(error))); assert.equal(validateDocxBytes(createTestDocx("")).valid, true); });
test("generated validation failure leaves destination unchanged", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, new Uint8Array([1, 2, 3])), /validation|readable ZIP/i); assert.deepEqual(backend.files.get(destination)?.bytes, original); assert.deepEqual(artifacts(backend), []); });
test("temporary write failure leaves destination unchanged", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); backend.fail = (op, path) => op === "write" && path.endsWith(".tmp"); await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "temp-write" })); assert.deepEqual(backend.files.get(destination)?.bytes, original); });
test("temporary readback failure leaves destination unchanged", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); backend.fail = (op, path) => op === "read" && path.endsWith(".tmp"); await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "temp-read" })); assert.deepEqual(backend.files.get(destination)?.bytes, original); assert.deepEqual(artifacts(backend), []); });
test("temporary validation failure leaves destination unchanged", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); backend.corruptRead = (path, bytes) => path.endsWith(".tmp") ? bytes.slice(0, 20) : bytes; await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "temp-invalid" }), /length|validation/i); assert.deepEqual(backend.files.get(destination)?.bytes, original); });
test("new filesystem save verifies final bytes and removes temporary files", async () => { const backend = new FakeBackend("filesystem"); const result = await new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "new-save" }); assert.equal(result.strategy, "same-folder-filesystem"); assert.equal(result.finalValidation.valid, true); assert.deepEqual(backend.files.get(destination)?.bytes, generated); assert.deepEqual(artifacts(backend), []); });
test("filesystem overwrite removes its backup after final verification", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); const result = await new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "overwrite" }); assert.equal(result.replacedExisting, true); assert.deepEqual(backend.files.get(destination)?.bytes, generated); assert.deepEqual(artifacts(backend), []); });
test("filesystem replacement failure restores the original", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); backend.fail = (op, path, target) => op === "rename" && path.endsWith(".tmp") && target === destination; await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "replace-fail" }), (error: unknown) => error instanceof SafeBinaryWriteError && error.restoration === "restored"); assert.deepEqual(backend.files.get(destination)?.bytes, original); assert.deepEqual(artifacts(backend), []); });
test("final validation failure restores the original", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); let corrupt = false; backend.corruptRead = (path, bytes) => path === destination && corrupt ? bytes.slice(0, 10) : bytes; const originalRename = backend.rename.bind(backend); backend.rename = async (from, to) => { await originalRename(from, to); if (from.endsWith(".tmp") && to === destination) corrupt = true; if (from.endsWith(".backup") && to === destination) corrupt = false; }; await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "final-invalid" }), (error: unknown) => error instanceof SafeBinaryWriteError && error.restoration === "restored"); assert.deepEqual(backend.files.get(destination)?.bytes, original); });
test("restoration failure preserves a critical recovery backup and guidance", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); backend.fail = (op, path, target) => op === "rename" && (path.endsWith(".tmp") && target === destination || path.endsWith(".backup") && target === destination); let failure: SafeBinaryWriteError | undefined; try { await new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "restore-fail" }); } catch (error) { failure = error as SafeBinaryWriteError; } assert.equal(failure?.restoration, "failed"); assert.equal(failure?.severity, "critical"); assert.ok(failure?.backupPath && backend.files.has(failure.backupPath)); assert.match(failure?.message ?? "", /recovery file|restore/i); });
test("cancellation before commit cleans temporary output and preserves destination", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); const controller = new AbortController(); await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "cancel", signal: controller.signal, onProgress: (stage) => { if (stage === "Verifying temporary file") controller.abort(); } }), /cancel/i); assert.deepEqual(backend.files.get(destination)?.bytes, original); assert.deepEqual(artifacts(backend), []); assert.equal(backend.operations.some((item) => item.operation === "rename"), false); });
test("cancellation is ignored after final replacement begins", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); const controller = new AbortController(); let committed = false; const result = await new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "commit-cancel", signal: controller.signal, onCommit: () => { committed = true; controller.abort(); } }); assert.equal(committed, true); assert.equal(result.finalValidation.valid, true); assert.deepEqual(backend.files.get(destination)?.bytes, generated); });
test("generic backup preparation remains cancellable before destination replacement", async () => { const backend = new FakeBackend("adapter"); backend.seed(destination, original); const controller = new AbortController(); let committed = false; const write = backend.write.bind(backend); backend.write = async (path, bytes) => { await write(path, bytes); if (path.endsWith(".backup")) controller.abort(); }; await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "adapter-cancel", signal: controller.signal, onCommit: () => { committed = true; } }), /cancel/i); assert.equal(committed, false); assert.deepEqual(backend.files.get(destination)?.bytes, original); assert.deepEqual(artifacts(backend), []); });
test("generic adapter fallback verifies success and cleans its backup", async () => { const backend = new FakeBackend("adapter"); backend.seed(destination, original); const result = await new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "adapter-success" }); assert.equal(result.strategy, "verified-adapter-recovery"); assert.deepEqual(backend.files.get(destination)?.bytes, generated); assert.deepEqual(artifacts(backend), []); });
test("generic adapter fallback restores original after final corruption", async () => { const backend = new FakeBackend("adapter"); backend.seed(destination, original); let finalReads = 0; backend.corruptRead = (path, bytes) => path === destination && ++finalReads === 2 ? bytes.slice(0, 12) : bytes; await assert.rejects(new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "adapter-restore" }), (error: unknown) => error instanceof SafeBinaryWriteError && error.restoration === "restored"); assert.deepEqual(backend.files.get(destination)?.bytes, original); assert.deepEqual(artifacts(backend), []); });
test("filesystem transaction uses same-directory temporary and backup paths", async () => { const backend = new FakeBackend("filesystem"); backend.seed(destination, original); await new SafeBinaryWriter(backend).writeValidated(destination, generated, { token: "same-directory" }); const paths = backend.operations.filter((item) => item.operation === "write" || item.operation === "rename").flatMap((item) => [item.path, item.target].filter((value): value is string => !!value)); assert.ok(paths.every((path) => path.startsWith("Exports/"))); });
test("stale cleanup removes only recognised old temporary files", async () => { const backend = new FakeBackend("adapter"); const now = Date.now(); const old = now - SafeBinaryWriter.STALE_TEMP_AGE_MS - 1; backend.seed("Exports/.Book.docx.manuscript-compiler-old.tmp", generated, old); backend.seed("Exports/.Book.docx.manuscript-compiler-recent.tmp", generated, now); backend.seed("Exports/.Book.docx.manuscript-compiler-old.backup", original, old); backend.seed("Exports/unrelated.tmp", generated, old); backend.seed("Exports/file.bak", generated, old); backend.seed("Exports/.hidden", generated, old); const result = await new SafeBinaryWriter(backend).cleanupStaleArtifacts("Exports", now); assert.deepEqual(result.removed, ["Exports/.Book.docx.manuscript-compiler-old.tmp"]); assert.ok(backend.files.has("Exports/.Book.docx.manuscript-compiler-recent.tmp")); assert.ok(backend.files.has("Exports/.Book.docx.manuscript-compiler-old.backup")); assert.ok(backend.files.has("Exports/unrelated.tmp")); assert.ok(backend.files.has("Exports/file.bak")); assert.ok(backend.files.has("Exports/.hidden")); });
test("history and result actions can be gated on final writer resolution", async () => { const failed = new FakeBackend("filesystem"); failed.seed(destination, original); failed.fail = (op, path, target) => op === "rename" && path.endsWith(".tmp") && target === destination; let failedHistorySuccess = false; let failedActions = false; await new SafeBinaryWriter(failed).writeValidated(destination, generated, { token: "gate-fail" }).then(() => { failedHistorySuccess = true; failedActions = true; }, () => undefined); assert.equal(failedHistorySuccess, false); assert.equal(failedActions, false); const success = new FakeBackend("filesystem"); let historySuccess = false; let actions = false; const result = await new SafeBinaryWriter(success).writeValidated(destination, generated, { token: "gate-success" }); if (result.finalValidation.valid) { historySuccess = true; actions = true; } assert.equal(historySuccess, true); assert.equal(actions, true); });
let failures = 0; for (const [name, action] of tests) { try { await action(); process.stdout.write(`${name}\n`); } catch (error) { failures += 1; process.stderr.write(`${name}\n${error instanceof Error ? error.stack : String(error)}\n`); } } if (failures) process.exitCode = 1; else process.stdout.write(`${tests.length} safe-writer tests passed.\n`);