import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { execFileSync } from "node:child_process"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const generator = path.join(root, "tools/web/generate-blender-parity-map.mjs"); const mapPath = path.join(root, "tests/golden/M15-02A/blender-parity-map.json"); const reportPath = path.join(root, "tests/golden/M15-02A/parity-map-check-report.json"); const manifestPath = path.join(root, "tests/golden/M15-02A/manifest.json"); const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); const fileSha256 = (file) => sha256(fs.readFileSync(file)); const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); const map = JSON.parse(fs.readFileSync(mapPath, "utf8")); assert.equal(map.schemaVersion, 1); assert.equal(map.task, "M15-02A"); assert.equal(map.operation, "BLENDER_PARITY_MAPPING"); assert.equal(map.statusAxis, "INVENTORY_ONLY"); assert.deepEqual(map.implementationClasses, ["INVENTORY_BASELINE"]); assert.equal(map.entries.length, map.summary.mapped); const ids = map.entries.map((entry) => entry.id); assert.deepEqual(ids, [...ids].sort(), "parity map IDs are not sorted"); assert.equal(new Set(ids).size, ids.length, "parity map IDs are not unique"); for (const entry of map.entries) { assert.match(entry.id, /^[\x20-\x7e]+$/); assert.equal(entry.implementationClass, "INVENTORY_BASELINE"); assert.equal(entry.coverage, "INVENTORIED_ONLY"); assert.ok(entry.ownerFamily); assert.equal(entry.tests.length, 1); assert.ok(fs.existsSync(path.join(root, entry.tests[0]))); assert.equal(entry.evidence.length, 1); assert.ok(fs.existsSync(path.join(root, entry.evidence[0]))); } for (const [task, ref] of Object.entries(map.sourceInventories)) assert.equal(fileSha256(path.join(root, ref.path)), ref.sha256, `${task} inventory hash drifted`); const temporary = path.join(root, ".tmp-m15-02a-map.json"); try { execFileSync(process.execPath, [generator, temporary], { cwd: root, stdio: "pipe" }); assert.deepEqual(fs.readFileSync(temporary), fs.readFileSync(mapPath), "parity map is not deterministic"); } finally { fs.rmSync(temporary, { force: true }); } const report = { schemaVersion: 1, task: "M15-02A", operation: "BLENDER_PARITY_MAPPING_CHECK", mapped: map.summary.mapped, byTask: map.summary.byTask, ownerFamilies: map.summary.ownerFamilies, mapSha256: fileSha256(mapPath), nextTask: map.nextTask }; if (process.env.UPDATE_M15_02A_MANIFEST === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); const artifactPaths = { parentManifest: path.join(root, "tests/golden/M15-01F/manifest.json"), generator, checker: path.join(root, "tools/web/check-blender-parity-map.mjs"), map: mapPath, report: reportPath, package: path.join(root, "web/package.json") }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: fileSha256(file) }])); fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M15-02A", parentTask: "M15-01F", enablingTask: false, parityStateChange: false, runtime: "BLENDER_5_2_PARITY", operation: "BLENDER_PARITY_MAPPING", artifacts, nextTask: "M15-02B" }, null, 2)}\n`); } const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M15-02A", parentTask: "M15-01F", nextTask: "M15-02B" }); for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); process.stdout.write(`blender-parity-map-ok mapped=${map.summary.mapped} families=${map.summary.ownerFamilies.length} map=${fileSha256(mapPath)} next=${manifest.nextTask}\n`);