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-06C/manifest.json"); const reportPath = path.join(root, "tests/golden/M12-06C/desktop-main-report.json"); const fixtureReportPath = path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"); const fixtureRoot = path.join(root, "tests/files/web/m12_glb_desktop_v1"); const generator = path.join(root, "tools/web/generate-glb-main-persistence-fixtures.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 fixtureReport = readJson(fixtureReportPath); assert.deepEqual( { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M12-06C", parentTask: "M12-06B", nextTask: "M12-06D" }, ); assert.deepEqual( { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, fixtureCount: report.fixtureCount, nextTask: report.nextTask }, { schemaVersion: 1, task: "M12-06C", operation: "DESKTOP_GLB_IMPORT_MAIN_PERSISTENCE_BASELINE", fixtureCount: 5, nextTask: "M12-06D" }, ); assert.equal(fileHash(path.join(root, "tests/golden/M12-06B/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 M12-06C artifact ${artifact.path}`); assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); } const fixtureById = new Map(fixtureReport.fixtures.map((fixture) => [fixture.id, fixture])); assert.deepEqual(report.fixtures.map((fixture) => fixture.id), ["mesh", "pbr", "uv", "skin", "animation"]); for (const fixture of report.fixtures) { const source = fixtureById.get(fixture.id); assert.ok(source, `${fixture.id} is absent from M12-06A`); assert.equal(fixture.glb.sha256, source.sha256, `${fixture.id} GLB source drift`); assert.equal(fileHash(path.join(fixtureRoot, fixture.glb.file)), fixture.glb.sha256, `${fixture.id} GLB hash`); const blend = path.join(fixtureRoot.replace("m12_glb_desktop_v1", "m12_glb_main_v1"), fixture.blend.file); assert.equal(fs.statSync(blend).size, fixture.blend.byteLength, `${fixture.id} desktop Main fixture byte length`); assert.equal(fileHash(blend), fixture.blend.sha256, `${fixture.id} desktop Main fixture hash`); assert.deepEqual(fixture.graph.stableIds, { objects: [...fixture.graph.stableIds.objects].sort(), meshes: [...fixture.graph.stableIds.meshes].sort(), materials: [...fixture.graph.stableIds.materials].sort(), images: [...fixture.graph.stableIds.images].sort(), armatures: [...fixture.graph.stableIds.armatures].sort(), actions: [...fixture.graph.stableIds.actions].sort(), }); for (const [kind, ids] of Object.entries(fixture.graph.stableIds)) { assert.equal(new Set(ids).size, ids.length, `${fixture.id} duplicate ${kind} stable ID`); const prefix = { objects: "object:", meshes: "mesh:", materials: "material:", images: "image:", armatures: "armature:", actions: "action:" }[kind]; assert.ok(ids.every((id) => id.startsWith(prefix)), `${fixture.id} malformed ${kind} stable ID`); } } const normalizeReport = (value) => ({ blenderVersion: value.blenderVersion, fixtureCount: value.fixtureCount, nextTask: value.nextTask, operation: value.operation, schemaVersion: value.schemaVersion, fixtures: value.fixtures.map((fixture) => ({ id: fixture.id, glb: fixture.glb, graph: fixture.graph, })), }); const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-06c-main-persistence-")); try { const outputRoot = path.join(temporary, "main"); const regeneratedReport = path.join(temporary, "desktop-main-report.json"); const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", fixtureRoot, outputRoot, regeneratedReport], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024, }); assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); const regenerated = readJson(regeneratedReport); assert.deepEqual(normalizeReport(regenerated), normalizeReport(report), "desktop Main semantic report is not deterministic"); for (const fixture of regenerated.fixtures) { assert.ok(fs.statSync(path.join(outputRoot, fixture.blend.file)).size > 0, `${fixture.id} regenerated .blend is empty`); assert.deepEqual(fixture.graph.stableIds, report.fixtures.find((item) => item.id === fixture.id).graph.stableIds, `${fixture.id} stable IDs drifted`); } } finally { fs.rmSync(temporary, { recursive: true, force: true }); } process.stdout.write(`glb-main-persistence-ok fixtures=${report.fixtureCount} stableIds=exact desktopReopen=exact deterministic=true next=${manifest.nextTask}\n`);