import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import test from "node:test"; import ts from "typescript"; const root = path.resolve(import.meta.dirname, "../../.."); const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "shader-compiler-unit-")); const sourcePath = path.join(root, "web/protocol/shader-compiler.ts"); const outputPath = path.join(temporary, "shader-compiler.mjs"); const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true, }); assert.deepEqual(transpiled.diagnostics, []); fs.writeFileSync(outputPath, transpiled.outputText); const compiler = await import(pathToFileURL(outputPath)); function material(overrides = {}) { return { id: "material:ShaderUnit", name: "ShaderUnit", baseColor: [0.2, 0.3, 0.4, 1], roughness: 0.5, metallic: 0.1, emissionColor: [0, 0, 0, 1], alpha: 1, ior: 1.45, shaderGraphHash: "a".repeat(64), nodes: [ { id: "rgb", type: "RGB", name: "RGB", defaultValue: [0.1, 0.2, 0.3, 1] }, { id: "value-a", type: "VALUE", name: "A", defaultValue: [0.2] }, { id: "value-b", type: "VALUE", name: "B", defaultValue: [0.22] }, { id: "math", type: "MATH", name: "Add", properties: { operation: "ADD" } }, { id: "image", type: "IMAGE_TEXTURE", name: "Image", imageId: "image:Normal" }, { id: "normal", type: "NORMAL_MAP", name: "Normal" }, { id: "principled", type: "PRINCIPLED", name: "Principled" }, { id: "output", type: "OUTPUT", name: "Output" }, ], links: [ { fromNodeId: "rgb", fromSocket: "Color", toNodeId: "principled", toSocket: "Base Color" }, { fromNodeId: "value-a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" }, { fromNodeId: "value-b", fromSocket: "Value", toNodeId: "math", toSocket: "Value_001" }, { fromNodeId: "math", fromSocket: "Value", toNodeId: "principled", toSocket: "Roughness" }, { fromNodeId: "image", fromSocket: "Color", toNodeId: "normal", toSocket: "Color" }, { fromNodeId: "normal", fromSocket: "Normal", toNodeId: "principled", toSocket: "Normal" }, { fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" }, ], ...overrides, }; } test("M10-07 compiles the declared Principled/Image/Normal/Math closure", () => { const report = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]) }); assert.equal(report.status, "COMPILED"); assert.equal(report.taskId, "M10-07"); assert.equal(report.backend, "WEBGL2_THREE_PHYSICAL"); assert.equal(report.material.baseColor[2], 0.3); assert.ok(Math.abs(report.material.roughness - 0.42) < 1e-8); assert.deepEqual(report.textureBindings, [{ imageId: "image:Normal", usage: "NORMAL" }]); assert.match(report.graphHash, /^[0-9a-f]{64}$/); assert.ok(report.instructions.some((instruction) => instruction.operation === "ADD")); assert.ok(report.nodeOrder.indexOf("value-a") < report.nodeOrder.indexOf("math")); assert.ok(report.nodeOrder.indexOf("math") < report.nodeOrder.indexOf("principled")); assert.ok(report.nodeOrder.indexOf("principled") < report.nodeOrder.indexOf("output")); }); test("M10-07 blocks unknown nodes and preserves the graph metadata", () => { const source = material(); source.nodes.push({ id: "mix", type: "UNSUPPORTED", name: "ShaderNodeMix" }); const report = compiler.compileMaterialGraph(source); assert.equal(report.status, "BLOCKED"); assert.equal(report.issues[0].code, "SHADER_NODE_UNSUPPORTED"); assert.equal(source.nodes.at(-1).type, "UNSUPPORTED"); }); test("M10-07 rejects cycles, duplicate links, and missing image resources", () => { const cyclic = material({ links: [ { fromNodeId: "value-a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" }, { fromNodeId: "math", fromSocket: "Value", toNodeId: "math", toSocket: "Value_001" }, { fromNodeId: "math", fromSocket: "Value", toNodeId: "principled", toSocket: "Roughness" }, { fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" }, { fromNodeId: "image", fromSocket: "Color", toNodeId: "normal", toSocket: "Color" }, { fromNodeId: "normal", fromSocket: "Normal", toNodeId: "principled", toSocket: "Normal" }, ], }); const cyclicReport = compiler.compileMaterialGraph(cyclic); assert.equal(cyclicReport.status, "BLOCKED"); assert.ok(cyclicReport.issues.some((issue) => issue.code === "SHADER_GRAPH_CYCLE")); const missingReport = compiler.compileMaterialGraph(material(), { imageIds: new Set() }); assert.equal(missingReport.status, "BLOCKED"); assert.ok(missingReport.issues.some((issue) => issue.code === "SHADER_EXTERNAL_RESOURCE_MISSING")); }); test("M10-07 keeps the fallback graph fingerprint deterministic", () => { const source = material(); delete source.shaderGraphHash; const first = compiler.compileMaterialGraph(source); const second = compiler.compileMaterialGraph(source); assert.equal(first.graphHash, second.graphHash); assert.match(first.graphHash, /^[0-9a-f]{64}$/); const canonical = JSON.stringify({ schemaVersion: 1, materialId: source.id, nodes: source.nodes.map((node) => ({ id: node.id, type: node.type, name: node.name, imageId: node.imageId ?? null, defaultValue: node.defaultValue ?? null, properties: node.properties ?? null, })), links: source.links.map((link) => ({ fromNodeId: link.fromNodeId, fromSocket: link.fromSocket, toNodeId: link.toNodeId, toSocket: link.toSocket, })), }); assert.equal(first.graphHash, createHash("sha256").update(canonical).digest("hex")); }); test("M10-07 fails closed before oversized topology or forged graph hashes are compiled", () => { const oversized = material({ nodes: Array.from({ length: compiler.SHADER_COMPILE_BUDGET.maxNodes + 1 }, (_value, index) => ({ id: `value:${index}`, type: "VALUE", name: `Value ${index}`, defaultValue: [0], })), links: [], }); const oversizedReport = compiler.compileMaterialGraph(oversized); assert.equal(oversizedReport.status, "BLOCKED"); assert.equal(oversizedReport.issues[0].code, "SHADER_NODE_UNSUPPORTED"); const forged = compiler.compileMaterialGraph(material({ shaderGraphHash: "A".repeat(64) })); assert.equal(forged.status, "BLOCKED"); assert.equal(forged.issues[0].code, "SHADER_INVALID_GRAPH"); }); test("M10-08 binds graph, texture identity, color space and backend into compileKey", () => { const textureIdentities = new Map([["image:Normal", { assetId: "asset:normal-v1", sha256: "b".repeat(64), colorSpace: "NON_COLOR", }]]); const first = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities }); const second = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities: new Map([["image:Normal", { ...textureIdentities.get("image:Normal"), sha256: "c".repeat(64) }]]) }); const linear = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities: new Map([["image:Normal", { ...textureIdentities.get("image:Normal"), colorSpace: "LINEAR" }]]) }); assert.equal(first.status, "COMPILED"); assert.match(first.compileKey, /^[0-9a-f]{64}$/); assert.notEqual(first.compileKey, second.compileKey); assert.notEqual(first.compileKey, linear.compileKey); assert.notEqual( first.compileKey, compiler.createShaderCompileKey({ graphHash: first.graphHash, rendererBackend: "WEBGPU", textures: [{ imageId: "image:Normal", usage: "NORMAL", assetId: "asset:normal-v1", sha256: "b".repeat(64), colorSpace: "NON_COLOR" }], }), ); }); test("M10-08 rejects an unknown renderer backend and malformed texture identity", () => { const backend = compiler.compileMaterialGraph(material(), { rendererBackend: "WEBGPU" }); assert.equal(backend.status, "BLOCKED"); assert.equal(backend.issues[0].code, "CAPABILITY_MISSING"); const malformed = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities: new Map([["image:Normal", { sha256: "not-a-digest" }]]), }); assert.equal(malformed.status, "BLOCKED"); assert.equal(malformed.issues.at(-1).code, "SHADER_INVALID_GRAPH"); });