import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const manifestPath = path.join(root, "tests/golden/M12-06E/manifest.json"); const reportPath = path.join(root, "tests/golden/M12-06E/desktop-reimport-report.json"); const generator = path.join(root, "tools/web/generate-glb-web-reimport-report.py"); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const read = (file) => fs.readFileSync(file); const readJson = (file) => JSON.parse(read(file)); const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); const fileHash = (file) => sha256(read(file)); const manifest = readJson(manifestPath); const report = readJson(reportPath); const base = readJson(path.join(root, "tests/golden/M12-06C/desktop-main-report.json")); const exportReport = readJson(path.join(root, "tests/golden/M12-06D/web-loss-report.json")); assert.deepEqual( { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M12-06E", parentTask: "M12-06D", nextTask: "M12-06F" }, ); assert.deepEqual( { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, fixtureCount: report.fixtureCount, nextTask: report.nextTask }, { schemaVersion: 1, task: "M12-06E", operation: "DESKTOP_REIMPORT_WEB_GLB_CANONICAL_REPORT", fixtureCount: 4, nextTask: "M12-06F" }, ); assert.equal(fileHash(path.join(root, "tests/golden/M12-06D/manifest.json")), manifest.artifacts.parentManifest.sha256); for (const artifact of Object.values(manifest.artifacts)) { const file = path.join(root, artifact.path); assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); } const expectedIds = ["pbr", "uv", "skin", "animation"]; assert.deepEqual(report.fixtures.map((fixture) => fixture.id), expectedIds); for (const fixture of report.fixtures) { const exportFixture = exportReport.fixtures.find((candidate) => candidate.fixtureId === fixture.id); assert.ok(exportFixture?.output, `${fixture.id} has no Web GLB output`); assert.equal(fixture.glb.sha256, exportFixture.output.sha256, `${fixture.id} Web GLB hash drift`); assert.equal(fileHash(path.join(root, "tests/files/web/m12_glb_web_v1", fixture.glb.file)), fixture.glb.sha256); } function mismatches(expected, actual, pathName = "graph", output = []) { if (Object.is(expected, actual)) return output; if (Array.isArray(expected) || Array.isArray(actual)) { if (!Array.isArray(expected) || !Array.isArray(actual)) { output.push(pathName); return output; } if (expected.length !== actual.length) output.push(`${pathName}.length`); for (let index = 0; index < Math.max(expected.length, actual.length); index++) { if (index >= expected.length || index >= actual.length) output.push(`${pathName}[${index}]`); else mismatches(expected[index], actual[index], `${pathName}[${index}]`, output); } return output; } if (expected && actual && typeof expected === "object" && typeof actual === "object") { for (const key of new Set([...Object.keys(expected), ...Object.keys(actual)])) mismatches(expected[key], actual[key], `${pathName}.${key}`, output); return output; } output.push(pathName); return output; } const comparisons = report.fixtures.map((fixture) => { const baseline = base.fixtures.find((candidate) => candidate.id === fixture.id); assert.ok(baseline, `${fixture.id} missing M12-06C baseline`); const paths = mismatches(baseline.graph, fixture.graph); return { id: fixture.id, exact: paths.length === 0, mismatchCount: paths.length, mismatchPaths: paths }; }); assert.deepEqual(comparisons.map((comparison) => ({ id: comparison.id, exact: comparison.exact, mismatchCount: comparison.mismatchCount })), [ { id: "pbr", exact: true, mismatchCount: 0 }, { id: "uv", exact: false, mismatchCount: 4 }, { id: "skin", exact: false, mismatchCount: 31 }, { id: "animation", exact: true, mismatchCount: 0 }, ]); const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-06e-web-reimport-")); try { const regenerated = path.join(temporary, "desktop-reimport-report.json"); const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", path.join(root, "tests/files/web/m12_glb_web_v1"), path.join(temporary, "blend"), regenerated], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024, }); assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); assert.deepEqual(readJson(regenerated), report, "desktop Web GLB report is not deterministic"); } finally { fs.rmSync(temporary, { recursive: true, force: true }); } process.stdout.write(`glb-web-reimport-ok fixtures=${report.fixtureCount} exact=${comparisons.filter((comparison) => comparison.exact).length} mismatched=${comparisons.filter((comparison) => !comparison.exact).map((comparison) => comparison.id).join(",")} deterministic=true next=${manifest.nextTask}\n`);