import assert from "node:assert/strict"; 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 repoRoot = path.resolve(import.meta.dirname, "../../.."); const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "compositor-unsupported-unit-")); const sourcePath = path.join(repoRoot, "web/protocol/compositor.ts"); 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(path.join(temporary, "compositor.mjs"), transpiled.outputText.replaceAll('from "./capability-gates"', 'from "./capability-gates.mjs"')); const gates = ts.transpileModule(fs.readFileSync(path.join(repoRoot, "web/protocol/capability-gates.ts"), "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: path.join(repoRoot, "web/protocol/capability-gates.ts"), reportDiagnostics: true, }); assert.deepEqual(gates.diagnostics, []); fs.writeFileSync(path.join(temporary, "capability-gates.mjs"), gates.outputText); const compositor = await import(pathToFileURL(path.join(temporary, "compositor.mjs"))); const node = (id, type, properties = {}, blenderType) => ({ id, type, name: id, properties, ...(blenderType ? { blenderType } : {}) }); const graph = () => ({ schemaVersion: 1, id: "compositor:unsupported", name: "Unsupported", outputNodeId: "out", resources: [], nodes: [ node("color", "CONSTANT_COLOR", { color: [0.125, 0.25, 0.5, 0.75] }), node("out", "COMPOSITE"), node("glare", "UNSUPPORTED", {}, "CompositorNodeGlare"), ], links: [{ fromNodeId: "color", fromSocket: "Image", toNodeId: "out", toSocket: "Image" }], }); test("M11-08 preserves the unsupported graph and blocks CPU execution before evaluation", async () => { const value = graph(); const before = structuredClone(value); const gate = compositor.gateCompositorGraph(value, new Set()); assert.equal(gate.status, "BLOCKED"); assert.deepEqual(gate.issues.map((issue) => issue.code), ["COMPOSITOR_NODE_UNSUPPORTED"]); let cancellationChecks = 0; assert.throws( () => compositor.executeCompositorGraph(value, new Map(), { width: 2, height: 2, cancelled: () => { cancellationChecks++; return false; } }), { code: "COMPOSITOR_NODE_UNSUPPORTED" }, ); assert.equal(cancellationChecks, 0); assert.deepEqual(value, before); }); test("M11-08 blocks a cached unsupported graph before a cache hit", async () => { const value = graph(); const before = structuredClone(value); const cache = new compositor.CompositorFrameCache(512); const key = await compositor.compositorFrameCacheKey(value, new Map(), 1, 2, 2); cache.set(key, { composite: { width: 2, height: 2, data: new Float32Array(16), colorSpace: "LINEAR_SRGB" }, viewers: new Map(), evaluatedNodeIds: ["cached"], }); await assert.rejects( () => compositor.executeCompositorGraphCached(value, new Map(), cache, { frame: 1, width: 2, height: 2 }), { code: "COMPOSITOR_NODE_UNSUPPORTED" }, ); assert.equal(cache.size, 1); assert.deepEqual(value, before); }); test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));