import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdirSync, mkdtempSync, writeFileSync, rmSync, symlinkSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { validateRegisteredEvidence } from "../check-trellis-governance.mjs"; function fixture(context) { const root = mkdtempSync(path.join(os.tmpdir(), "hyb-evidence-test-")); context.after(() => rmSync(root, { recursive: true, force: true })); mkdirSync(path.join(root, "output")); const digest = createHash("sha256").update("archive").digest("hex"); const rows = ["first.png", "second.png"].map(name => `| \`${name}\` | \`${digest}\` |`); const document = "| 截图 | SHA-256 |\n| --- | --- |\n" + rows.join("\n") + "\n"; writeFileSync(path.join(root, "evidence.md"), document); for (const name of ["first.png", "second.png"]) writeFileSync(path.join(root, "output", name), "archive"); return { root, document, check: () => validateRegisteredEvidence(root, [{ document: "evidence.md", directory: "output", count: 2 }]) }; } test("归档登记摘要匹配时计数明确", context => { assert.deepEqual(fixture(context).check(), { checked: 2, errors: [] }); }); for (const mutation of ["overwrite", "missing", "symlink", "table-missing", "row-missing", "duplicate", "escape", "invalid-hash"]) test(`证据错误必须被机器发现:${mutation}`, context => { const owned = fixture(context); const image = path.join(owned.root, "output/first.png"); if (mutation === "overwrite") writeFileSync(image, "new run"); if (["missing", "symlink"].includes(mutation)) rmSync(image); if (mutation === "symlink") symlinkSync("second.png", image); const changes = { "table-missing": "history only\n", "row-missing": owned.document.split("\n").slice(0, 3).join("\n"), duplicate: owned.document.replace("second.png", "first.png"), escape: owned.document.replace("first.png", "../first.png"), "invalid-hash": owned.document.replace(/`[a-f0-9]{64}`/, "`unknown`"), }; if (mutation in changes) writeFileSync(path.join(owned.root, "evidence.md"), changes[mutation]); assert.ok(owned.check().errors.length > 0); });