import { expect, test } from "@playwright/test"; import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, "../../.."); const expected = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M10-07/shader-compile.json"), "utf8")); const blendBytes = fs.readFileSync(path.join(root, expected.fixture)); function sockets() { return { rgb: [{ id: "color", name: "Color", direction: "OUTPUT", dataType: "COLOR", defaultValue: [0.15, 0.25, 0.35, 1] }], value: [{ id: "value", name: "Value", direction: "OUTPUT", dataType: "VALUE", defaultValue: 0.2 }], math: [ { id: "a", name: "Value", direction: "INPUT", dataType: "VALUE", defaultValue: 0 }, { id: "b", name: "Value_001", direction: "INPUT", dataType: "VALUE", defaultValue: 0 }, { id: "value", name: "Value", direction: "OUTPUT", dataType: "VALUE" }, ], principled: [ { id: "base", name: "Base Color", direction: "INPUT", dataType: "COLOR", defaultValue: [0.2, 0.3, 0.4, 1] }, { id: "roughness", name: "Roughness", direction: "INPUT", dataType: "VALUE", defaultValue: 0.5 }, { id: "bsdf", name: "BSDF", direction: "OUTPUT", dataType: "SHADER" }, ], output: [{ id: "surface", name: "Surface", direction: "INPUT", dataType: "SHADER" }], }; } test("M10-07 produces a bounded compile report from the real Main Shader graph", async ({ page }) => { test.setTimeout(120_000); expect(createHash("sha256").update(blendBytes).digest("hex")).toBe(expected.fixtureSha256); await page.goto("/"); const result = await page.evaluate(async ({ bytes, socketSet }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const client = new WebEngineClient({ timeoutMs: 60_000 }); await client.init(); const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; const opened = await client.openBlend(source); const material = opened.snapshot.materials[0]; if (!material) throw new Error("basic fixture has no editable material"); const graph = { schemaVersion: 1 as const, id: "shader:m10-07", materialId: material.id, outputNodeId: "output", nodes: [ { id: "rgb", type: "RGB" as const, name: "RGB", sockets: socketSet.rgb }, { id: "a", type: "VALUE" as const, name: "A", sockets: socketSet.value }, { id: "b", type: "VALUE" as const, name: "B", sockets: [{ ...socketSet.value[0], defaultValue: 0.22 }] }, { id: "math", type: "MATH" as const, name: "Add", properties: { operation: "ADD" as const }, sockets: socketSet.math }, { id: "principled", type: "PRINCIPLED" as const, name: "Principled", sockets: socketSet.principled }, { id: "output", type: "MATERIAL_OUTPUT" as const, name: "Output", sockets: socketSet.output }, ], links: [ { fromNodeId: "rgb", fromSocketId: "color", toNodeId: "principled", toSocketId: "base" }, { fromNodeId: "a", fromSocketId: "value", toNodeId: "math", toSocketId: "a" }, { fromNodeId: "b", fromSocketId: "value", toNodeId: "math", toSocketId: "b" }, { fromNodeId: "math", fromSocketId: "value", toNodeId: "principled", toSocketId: "roughness" }, { fromNodeId: "principled", fromSocketId: "bsdf", toNodeId: "output", toSocketId: "surface" }, ], }; const applied = await client.applyCommand({ type: "setShaderGraph", materialId: material.id, graph }); const report = applied.shaderCompile; const after = await client.snapshot(); client.terminate(); return { openedHash: material.shaderGraphHash, status: report?.status, taskId: report?.taskId, backend: report?.backend, graphHash: report?.graphHash, roughness: report?.material?.roughness, nodeTypes: report?.compiledNodeTypes, revisionDelta: after.snapshot.revision - opened.snapshot.revision, }; }, { bytes: new Uint8Array(blendBytes), socketSet: sockets() }); expect(result.status).toBe("COMPILED"); expect(result.openedHash).toMatch(/^[0-9a-f]{64}$/); expect(result.taskId).toBe("M10-07"); expect(result.backend).toBe(expected.backend); expect(result.graphHash).toMatch(/^[0-9a-f]{64}$/); expect(result.roughness).toBeCloseTo(expected.expectedMathResult, 5); expect(result.nodeTypes).toEqual(expect.arrayContaining(expected.allowlist.filter((type: string) => !["IMAGE_TEXTURE", "NORMAL_MAP"].includes(type)))); expect(result.revisionDelta).toBe(1); }); test("M10-07 shared viewport material path compiles Image/Normal/Math bindings", async ({ page }) => { await page.goto("/"); const result = await page.evaluate(async () => { const { createPBRMaterial } = await import("/src/three-adapter/pbr.ts"); const material = createPBRMaterial({ id: "material:viewport", name: "Viewport", baseColor: [0.8, 0.8, 0.8, 1], roughness: 0.5, metallic: 0, emissionColor: [0, 0, 0, 1], alpha: 1, ior: 1.45, nodes: [ { id: "a", type: "VALUE", name: "A", defaultValue: [0.2] }, { id: "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: "a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" }, { fromNodeId: "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" }, ], }); const compile = material.userData.shaderCompile; const report = { status: compile.status, graphHash: compile.graphHash, roughness: material.roughness, textures: compile.textureBindings, }; material.dispose(); return report; }); expect(result.status).toBe("COMPILED"); expect(result.graphHash).toMatch(/^[0-9a-f]{64}$/); expect(result.roughness).toBeCloseTo(expected.expectedMathResult, 5); expect(result.textures).toEqual([{ imageId: "image:normal", usage: "NORMAL" }]); }); test("M10-07 compiler blocks a non-declared node without mutating the input graph", async ({ page }) => { await page.goto("/"); const result = await page.evaluate(async () => { const { createPBRMaterial } = await import("/src/three-adapter/pbr.ts"); const material = { id: "material:blocked", name: "Blocked", baseColor: [1, 1, 1, 1], roughness: 0.5, metallic: 0, emissionColor: [0, 0, 0, 1], alpha: 1, ior: 1.45, nodes: [{ id: "mix", type: "UNSUPPORTED", name: "Mix" }, { id: "output", type: "OUTPUT", name: "Output" }], links: [], } as const; const before = JSON.stringify(material); const threeMaterial = createPBRMaterial(material); const report = threeMaterial.userData.shaderCompile; threeMaterial.dispose(); return { status: report.status, code: report.issues[0]?.code, preserved: JSON.stringify(material) === before }; }); expect(result).toEqual({ status: "BLOCKED", code: expected.unsupportedNodeCode, preserved: true }); });