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(), "geometry-nodes-unit-")); function transpile(sourceName, outputName, replacements = []) { const sourcePath = path.join(repoRoot, "web/protocol", sourceName); 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, []); const output = replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText); fs.writeFileSync(path.join(temporary, outputName), output); } transpile("capability-gates.ts", "capability-gates.mjs"); transpile("geometry-nodes.ts", "geometry-nodes.mjs", [ ['from "./capability-gates"', 'from "./capability-gates.mjs"'], ]); const geometryNodes = await import(pathToFileURL(path.join(temporary, "geometry-nodes.mjs"))); function graph() { return { schemaVersion: 1, id: "node-group:Unit", name: "Unit", interfaceInputs: [{ id: "input:geometry", name: "Geometry", direction: "INPUT", dataType: "GEOMETRY" }], interfaceOutputs: [{ id: "output:geometry", name: "Geometry", direction: "OUTPUT", dataType: "GEOMETRY" }], nodes: [{ id: "geometry-node:7", type: "GeometryNodeTransform", name: "Transform Geometry", sockets: [ { id: "input:mode", name: "Mode", direction: "INPUT", dataType: "MENU", defaultValue: 0 }, { id: "input:rotation", name: "Rotation", direction: "INPUT", dataType: "ROTATION", defaultValue: [0, 0, 0] }, { id: "input:matrix", name: "Transform", direction: "INPUT", dataType: "MATRIX" }, { id: "output:geometry", name: "Geometry", direction: "OUTPUT", dataType: "GEOMETRY" }, ], }], links: [], groupReferences: [], graphHash: "a".repeat(64), }; } test("M10-01 parses Blender Main socket types and stable graph identities", () => { const parsed = geometryNodes.parseGeometryNodeGraph(graph()); assert.equal(parsed.id, "node-group:Unit"); assert.deepEqual(parsed.nodes[0].sockets.map((socket) => socket.dataType), ["MENU", "ROTATION", "MATRIX", "GEOMETRY"]); assert.equal(geometryNodes.validateGeometryNodeGraph(parsed).status, "SUPPORTED"); assert.deepEqual(geometryNodes.GEOMETRY_NODE_GRAPH_BUDGET, { maxGraphs: 4_096, maxNodesPerGraph: 4_096, maxLinksPerGraph: 16_384, maxSocketsPerGraph: 65_536, maxInterfaceSocketsPerGraph: 4_096, maxIdentifierBytes: 256, maxNameBytes: 1_024, }); }); test("M10-01 rejects duplicate stable IDs and malformed graph hashes", () => { const duplicateNode = graph(); duplicateNode.nodes.push(structuredClone(duplicateNode.nodes[0])); assert.throws(() => geometryNodes.parseGeometryNodeGraph(duplicateNode), { code: "GN_INVALID_GRAPH" }); const duplicateSocket = graph(); duplicateSocket.nodes[0].sockets.push(structuredClone(duplicateSocket.nodes[0].sockets[0])); assert.throws(() => geometryNodes.parseGeometryNodeGraph(duplicateSocket), { code: "GN_INVALID_GRAPH" }); assert.throws(() => geometryNodes.parseGeometryNodeGraph({ ...graph(), graphHash: "A".repeat(64) }), { code: "GN_INVALID_GRAPH" }); }); test("M10-01 fails closed before oversized graph topology enters SceneIR", () => { const oversized = graph(); oversized.nodes = Array.from({ length: geometryNodes.GEOMETRY_NODE_GRAPH_BUDGET.maxNodesPerGraph + 1 }, (_value, index) => ({ id: `node:${index}`, type: "NodeGroupInput", name: "Input", sockets: [] })); assert.throws(() => geometryNodes.parseGeometryNodeGraph(oversized), { code: "GN_GRAPH_BUDGET_EXCEEDED" }); const graphSet = Array.from({ length: geometryNodes.GEOMETRY_NODE_GRAPH_BUDGET.maxGraphs + 1 }, graph); assert.deepEqual(geometryNodes.validateGeometryNodeGraphSet(graphSet).issues.map((issue) => issue.code), ["GN_GRAPH_BUDGET_EXCEEDED"]); }); test("M10-02 freezes the allowlist and blocks unsupported nodes without rewriting the graph", () => { const allowed = graph(); const before = structuredClone(allowed); assert.equal(geometryNodes.GEOMETRY_NODE_ALLOWLIST_SCHEMA, 1); assert.deepEqual(geometryNodes.GEOMETRY_NODE_ALLOWLIST, [ "NodeGroupInput", "NodeGroupOutput", "GeometryNodeTransform", "GeometryNodeSetPosition", "GeometryNodeJoinGeometry", "GeometryNodeSeparateGeometry", "GeometryNodeRealizeInstances", "GeometryNodeStoreNamedAttribute", "FunctionNodeInputInt", "FunctionNodeInputVector", "FunctionNodeCompare", "ShaderNodeValue", "ShaderNodeMath", "GeometryNodeObjectInfo", "GeometryNodeCollectionInfo", "GeometryNodeImageInfo", ]); assert.equal(geometryNodes.gateGeometryNodeGraph(allowed).status, "READY"); allowed.nodes.push({ id: "geometry-node:8", type: "GeometryNodeSimulationOutput", name: "Simulation Output", sockets: [], }); const blocked = geometryNodes.gateGeometryNodeGraph(allowed); assert.equal(blocked.status, "BLOCKED"); assert.deepEqual(blocked.issues.map((issue) => issue.code), ["GN_NODE_UNSUPPORTED"]); assert.deepEqual(before, graph()); assert.equal(allowed.nodes.at(-1).type, "GeometryNodeSimulationOutput"); }); function domainCardinality(overrides = {}) { return { POINT: 8, EDGE: 12, FACE: 6, CORNER: 24, CURVE: 0, INSTANCE: 0, LAYER: 0, ...overrides, }; } function field(overrides = {}) { return { schemaVersion: 1, graphId: "node-group:Unit", graphHash: "a".repeat(64), fieldId: "field:position", revision: 7, sourceDomain: "POINT", targetDomain: "CORNER", dataType: "FLOAT", transport: "JSON", domainCardinality: domainCardinality(), ...overrides, }; } test("M10-04 binds field materialization to exact domain cardinality and byte budgets", () => { assert.equal(geometryNodes.GEOMETRY_NODE_FIELD_SCHEMA, 1); assert.deepEqual(geometryNodes.GEOMETRY_NODE_FIELD_DOMAIN_BUDGET, { POINT: 1_000_000, EDGE: 2_000_000, FACE: 2_000_000, CORNER: 4_000_000, CURVE: 100_000, INSTANCE: 100_000, LAYER: 4_096, }); assert.deepEqual(geometryNodes.GEOMETRY_NODE_FIELD_BUDGET, { maxFieldsPerBatch: 64, maxDomainConversionsPerBatch: 32, maxMaterializedElementsPerBatch: 4_000_000, maxMaterializedBytesPerBatch: 64 * 1024 * 1024, maxJsonScalarValuesPerField: 65_536, maxIdentifierBytes: 256, }); const batch = geometryNodes.parseGeometryNodeFieldMaterializationBatch([ field(), field({ fieldId: "field:offset", sourceDomain: "CONSTANT", targetDomain: "POINT", dataType: "VECTOR", }), ]); assert.equal(batch.fieldCount, 2); assert.equal(batch.domainConversionCount, 1); assert.equal(batch.materializedElementCount, 32); assert.equal(batch.materializedByteLength, 192); assert.deepEqual(batch.fields.map((entry) => ({ source: entry.sourceElementCount, target: entry.targetElementCount, scalars: entry.scalarValueCount, bytes: entry.materializedByteLength, conversion: entry.domainConversion, })), [ { source: 8, target: 24, scalars: 24, bytes: 96, conversion: true }, { source: 1, target: 8, scalars: 24, bytes: 96, conversion: false }, ]); }); test("M10-04 blocks unbounded JSON fields, cardinality drift and aggregate overflow", () => { assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterializationBatch({}), { code: "GN_INVALID_GRAPH" }); assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization({ ...field(), values: Array(24).fill(0), }), { code: "GN_FIELD_JSON_BUDGET_EXCEEDED" }); const largeCardinality = domainCardinality({ POINT: 100_000, CORNER: 300_000 }); assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({ targetDomain: "POINT", dataType: "VECTOR", domainCardinality: largeCardinality, })), { code: "GN_FIELD_JSON_BUDGET_EXCEEDED" }); assert.equal(geometryNodes.parseGeometryNodeFieldMaterialization(field({ targetDomain: "POINT", dataType: "VECTOR", transport: "BINARY", domainCardinality: largeCardinality, })).materializedByteLength, 1_200_000); const missingDomain = domainCardinality(); delete missingDomain.LAYER; assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({ domainCardinality: missingDomain, })), { code: "GN_DOMAIN_CARDINALITY_MISMATCH" }); assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({ domainCardinality: { ...domainCardinality(), VOXEL: 1 }, })), { code: "GN_DOMAIN_CARDINALITY_MISMATCH" }); assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({ dataType: "toString", })), { code: "GN_INVALID_GRAPH" }); assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({ domainCardinality: domainCardinality({ EDGE: 2_000_001 }), })), { code: "GN_FIELD_BUDGET_EXCEEDED" }); assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterializationBatch([ field({ fieldId: "field:large-a", targetDomain: "CORNER", dataType: "COLOR", transport: "BINARY", domainCardinality: domainCardinality({ CORNER: 2_500_000 }), }), field({ fieldId: "field:large-b", targetDomain: "CORNER", dataType: "COLOR", transport: "BINARY", domainCardinality: domainCardinality({ CORNER: 2_500_000 }), }), ]), { code: "GN_FIELD_BUDGET_EXCEEDED" }); assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterializationBatch( Array.from({ length: 33 }, (_value, index) => field({ fieldId: `field:conversion-${index}` })), ), { code: "GN_FIELD_BUDGET_EXCEEDED" }); }); test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));