import { expect, test } from "@playwright/test"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, "../../.."); const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-08/compositor-unsupported-gate.json"), "utf8")) as { fixture: { path: string; sha256: string }; scene: string; unsupportedNode: { name: string; type: string; blenderType: string }; expectedErrorCode: string; expectedRevisionDelta: number; }; const fixture = fs.readFileSync(path.join(root, golden.fixture.path)); test("M11-08 keeps the Blender graph intact while blocking unsupported compositor execution", async ({ page }) => { expect(crypto.createHash("sha256").update(fixture).digest("hex")).toBe(golden.fixture.sha256); await page.goto("/"); const result = await page.evaluate(async (input) => { const [{ WebEngineClient }, compositor] = await Promise.all([ import("/src/engine-client/WebEngineClient.ts"), import("/src/compositor/CompositorExecutor.ts"), ]); const client = new WebEngineClient({ timeoutMs: 30_000 }); await client.init(); const opened = await client.openBlend(input.bytes.buffer); const scene = opened.snapshot.scenes.find((candidate) => candidate.name === input.scene); if (!scene?.compositorGraph) throw new Error(`Missing compositor graph ${input.scene}`); const beforeGraph = JSON.stringify(scene.compositorGraph); const beforeRevision = opened.snapshot.revision; const unsupported = scene.compositorGraph.nodes.find((node) => node.type === input.unsupportedNode.type); let gateCode = ""; let executeCode = ""; let cachedCode = ""; try { gateCode = compositor.gateCompositorGraph(scene.compositorGraph, new Set()).issues[0]?.code ?? ""; } catch (error) { gateCode = String(error); } try { compositor.executeCompositorGraph(scene.compositorGraph, new Map(), { width: 2, height: 2 }); } catch (error) { executeCode = error instanceof compositor.CompositorValidationError ? error.code : String(error); } try { await compositor.executeCompositorGraphCached(scene.compositorGraph, new Map(), new compositor.CompositorFrameCache(512), { frame: 1, width: 2, height: 2 }); } catch (error) { cachedCode = error instanceof compositor.CompositorValidationError ? error.code : String(error); } const after = await client.snapshot(); client.terminate(); const afterScene = after.snapshot.scenes.find((candidate) => candidate.name === input.scene); return { beforeGraph, afterGraph: JSON.stringify(afterScene?.compositorGraph), beforeRevision, afterRevision: after.snapshot.revision, gateCode, executeCode, cachedCode, unsupported: unsupported && { name: unsupported.name, type: unsupported.type, blenderType: unsupported.blenderType }, }; }, { bytes: new Uint8Array(fixture), scene: golden.scene, unsupportedNode: golden.unsupportedNode }); expect(result.gateCode).toBe(golden.expectedErrorCode); expect(result.executeCode).toBe(golden.expectedErrorCode); expect(result.cachedCode).toBe(golden.expectedErrorCode); expect(result.unsupported).toEqual(golden.unsupportedNode); expect(result.afterGraph).toBe(result.beforeGraph); expect(result.afterRevision - result.beforeRevision).toBe(golden.expectedRevisionDelta); });