#!/usr/bin/env node /** Validate the RE 5000PBR render matrix, lesson assets and static HTML. */ import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const REPO = path.resolve(import.meta.dirname, "../.."); const OUT = path.join(REPO, "tests/golden/RE-PBR-RENDER-001"); const LESSON = path.join(REPO, "web/app/public/tutorial/pbr"); const GLB_SOURCE = path.join(REPO, "re/猴头.glb"); const GLB_FIXTURE = path.join(REPO, "tests/files/web/pbr-render-lesson/monkey-head.glb"); const MATERIALS = [ "Black_Anti_Slip_grip", "Blackish_Brick_Wall_016", "black_randome_tile", "black_shinning_tile", "black_tiles_wall", "black_worn_wall_brick", ]; const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex"); const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); const pngSize = (file) => { const bytes = fs.readFileSync(file); assert.equal(bytes.subarray(0, 8).toString("hex"), "89504e470d0a1a0a", `not a PNG: ${file}`); return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) }; }; const report = readJson(path.join(OUT, "report.json")); assert.equal(report.status, "PASS"); assert.equal(report.renderer, "WEB_WEBGL2"); assert.equal(report.headed, true); assert.equal(report.renders.length, 20, "expected 20 renders"); assert.equal(report.steps.length, 15, "expected 15 lesson steps"); assert.equal(report.errors.length, 0); assert.equal(sha256(fs.readFileSync(GLB_SOURCE)), sha256(fs.readFileSync(GLB_FIXTURE)), "GLB fixture SHA mismatch"); assert.equal(report.glbSha256, sha256(fs.readFileSync(GLB_FIXTURE))); const materialScene = new Map(); for (const render of report.renders) { const file = path.join(REPO, render.path); assert.ok(fs.existsSync(file), `missing render: ${render.path}`); assert.equal(sha256(fs.readFileSync(file)), render.sha256, `render SHA mismatch: ${render.path}`); assert.equal(fs.statSync(file).size, render.byteLength, `render byte length mismatch: ${render.path}`); const size = pngSize(file); assert.equal(size.width, render.resolution, `render width mismatch: ${render.path}`); assert.equal(size.height, render.resolution, `render height mismatch: ${render.path}`); assert.ok(render.mean > 1 && render.mean < 254, `render appears blank: ${render.path} mean=${render.mean}`); assert.ok(render.textureStatus?.startsWith("ready:"), `texture status not ready: ${render.path} ${render.textureStatus}`); assert.ok(render.textureAssets > 0, `no texture assets: ${render.path}`); if (render.camera === "SCENE" && render.resolution === 1024 && render.material && !render.name.includes("reopen")) { materialScene.set(render.material, render.sha256); } assert.ok(fs.existsSync(path.join(REPO, render.jpg)), `missing lesson JPG: ${render.jpg}`); } assert.equal(materialScene.size, 6, "missing 1024 scene renders for six materials"); assert.ok(report.renders.every((render) => !String(render.compileStatus ?? "").includes("BLOCKED")), "a material compile was BLOCKED"); assert.ok(report.renders.filter((render) => String(render.compileStatus ?? "").includes("COMPILED")).length >= 6, "six RE materials must compile"); assert.equal(new Set(materialScene.values()).size, 6, "six material renders are not distinct"); const baseline = report.renders.find((render) => render.name === "baseline-unassigned-1024-scene"); assert.ok(baseline, "baseline render missing"); assert.ok(![...materialScene.values()].includes(baseline.sha256), "baseline identical to a material render"); assert.ok(report.renders.some((render) => render.name.endsWith("-reopen-1024-scene")), "reopen render missing"); const htmlPath = path.join(LESSON, "render-lesson.html"); const cssPath = path.join(LESSON, "render-lesson.css"); assert.ok(fs.existsSync(htmlPath), "render-lesson.html missing"); assert.ok(fs.existsSync(cssPath), "render-lesson.css missing"); const html = fs.readFileSync(htmlPath, "utf8"); assert.ok(!/ match[1]); for (const source of sources) { if (source.startsWith("#") || source.startsWith("/?") || source.startsWith("mailto:")) continue; assert.ok(source.startsWith("/"), `relative lesson URL is not supported: ${source}`); const file = path.join(REPO, "web/app/public", source.slice(1)); assert.ok(fs.existsSync(file), `lesson asset missing: ${source}`); } for (const match of html.matchAll(/poster="([^"]+)"/g)) { const source = match[1]; assert.ok(source.startsWith("/"), `relative poster is not supported: ${source}`); assert.ok(fs.existsSync(path.join(REPO, "web/app/public", source.slice(1))), `lesson poster missing: ${source}`); } for (const material of MATERIALS) { assert.ok(html.includes(`material-${material}-1024-scene.jpg`), `lesson misses ${material} render`); assert.ok(html.includes(material), `lesson misses ${material} name`); } for (const step of report.steps) { assert.ok(fs.existsSync(path.join(REPO, step.path)), `missing step image: ${step.path}`); } assert.ok(fs.existsSync(path.join(LESSON, "lesson/landing.jpg")), "landing image missing"); console.log(JSON.stringify({ status: "PASS", renders: report.renders.length, steps: report.steps.length, materials: materialScene.size, baselineMean: baseline.mean, lesson: path.relative(REPO, htmlPath), }, null, 2));