import assert from "node:assert/strict"; import test from "node:test"; import { compileFxNodeComposition, composeNode, composeSocket, FXNODE_COMPOSITION_LIMITS, FxNodeCompositionError, removeNode, removeSocket, setTheme, validateFxNodeComposition, type FxNodeCompositionSeed, } from "@lib/composition/index.js"; const theme = { background: "#112233", grid: "#112233", frame: "#112233", frameHeader: "#112233", body: "#112233", control: "#112233", controlFill: "#112233", controlEditing: "#112233", textSelection: "#112233", outline: "#112233", text: "#112233", muted: "#112233", shadow: "#112233", nodeSelected: "#112233", nodeActive: "#112233", unknownHeader: "#112233", unknownSocket: "#112233", linkMuted: "#112233", knifeMuted: "#112233", emphasis: "#112233", focus: "#112233", editOutline: "#112233", resize: "#112233", muteOverlay: "#112233", boxSelectionFill: "#112233", checkerLight: "#112233", checkerDark: "#112233", widgetBorder: "#112233", rampBorder: "#112233", resourceBackground: "#112233", } as const; const socket = ( direction: "input" | "output", type: "scalar" | "wide", value: null | { type: "number"; default: { kind: "number"; value: number } } = null, ) => ({ title: direction, direction, type, maxIncomingLinks: direction === "input" ? 1 : 0, visible: true, value: direction === "input" ? value : null, showValue: direction === "input" && value !== null, }); const valid = () => ({ schemaVersion: 2, id: "studio", version: 4, compatibility: { wildcardInputTypes: [] }, theme, socketTypes: { scalar: { title: "Scalar", color: "#abcdef", acceptsFrom: ["scalar"] }, wide: { title: "Wide", color: "#fedcba", acceptsFrom: ["wide", "scalar"] }, }, nodeStyles: { grade: { header: "#123456" }, utility: { header: "#654321" } }, resources: { image: { kind: "image", title: "Image", openTitle: "Open", accept: ["image/png", "image/jpeg"], referencePrefix: "test-image:", maxBytes: 1024, maxWidth: 64, maxHeight: 64, maxPixels: 4096, }, }, nodes: { grade: { version: 3, title: "Grade", behavior: "standard", style: "grade", parameters: { exposure: { type: "number", default: { kind: "number", value: 0 }, minimum: -10, maximum: 10, step: 0.1, precision: 2, }, mode: { type: "string", default: { kind: "string", value: "film" }, enum: ["film", "raw"] }, file: { type: "string", default: { kind: "string", value: "" } }, tint: { type: "color", default: { kind: "color", value: [1, 1, 1, 1] }, minimum: 0, maximum: 1 }, ramp: { type: "json", codec: "color-ramp/v1", default: { kind: "json", value: { colorMode: "rgb", interpolation: "linear", hueInterpolation: "near", stops: [ { id: "a", position: 0, color: [0, 0, 0, 1] }, { id: "b", position: 1, color: [1, 1, 1, 1] }, ], }, }, }, lift: { type: "number", default: { kind: "number", value: 0 } }, gamma: { type: "number", default: { kind: "number", value: 1 } }, gain: { type: "number", default: { kind: "number", value: 1 } }, liftColor: { type: "color", default: { kind: "color", value: [0, 0, 0, 1] } }, gammaColor: { type: "color", default: { kind: "color", value: [0.5, 0.5, 0.5, 1] } }, gainColor: { type: "color", default: { kind: "color", value: [1, 1, 1, 1] } }, }, sockets: { input: socket("input", "wide", { type: "number", default: { kind: "number", value: 0 } }), output: socket("output", "scalar"), secret: socket("input", "scalar"), }, ui: [ { kind: "text", variant: "header", title: "Color", visibleWhen: { all: [ { parameter: "mode", equals: "film" }, { any: [ { parameter: "exposure", in: [0, 1] }, { parameter: "mode", equals: "raw" }, ], }, ], }, }, { kind: "parameter", parameter: "exposure" }, { kind: "parameter", parameter: "mode" }, { kind: "resource", resource: "image", parameter: "file" }, { kind: "parameter", parameter: "tint" }, { kind: "widget", widget: "color-ramp", parameter: "ramp" }, { kind: "widget", widget: "grading-wheels", bindings: [ { title: "Lift", scalar: "lift", color: "liftColor" }, { title: "Gamma", scalar: "gamma", color: "gammaColor" }, { title: "Gain", scalar: "gain", color: "gainColor" }, ], }, { kind: "socket", socket: "input" }, { kind: "socket", socket: "output" }, { kind: "hidden", target: "socket", socket: "secret" }, ], muteBypass: [["input", "output"]], migrations: [ { fromVersion: 1, toVersion: 3, steps: [ { kind: "materialize-missing", target: "parameter", key: "exposure" }, { kind: "migrate-parameter", parameter: "ramp", codec: "color-ramp/legacy-stops" }, { kind: "rename-socket", from: "source", to: "input" }, ], }, ], }, constant: { version: 1, title: "Constant", behavior: "standard", style: "utility", parameters: { value: { type: "number", default: { kind: "number", value: 1 }, integer: true } }, sockets: { out: socket("output", "scalar") }, ui: [ { kind: "parameter", parameter: "value" }, { kind: "socket", socket: "out" }, ], muteBypass: [], migrations: [], }, internal: { version: 1, title: "Internal", behavior: "reroute", style: "utility", parameters: {}, sockets: {}, ui: [], muteBypass: [], migrations: [], }, }, }) as const; const clone = () => structuredClone(valid()); function issue(value: unknown, code: string, path: string) { const r = validateFxNodeComposition(value); assert.equal(r.ok, false); if (!r.ok) assert( r.issues.some((x) => x.code === code && x.path === path), `${code} ${path}\n${JSON.stringify(r.issues)}`, ); } test("valid fixture compiles without menu projection", () => { const v = valid(); assert.equal(validateFxNodeComposition(v).ok, true); const c = compileFxNodeComposition(v); assert.equal("menuEntries" in c, false); }); test("composition authoring helpers are immutable, replaceable, removable and clone-safe", () => { const seed = { schemaVersion: 2, id: "helpers", version: 1, compatibility: { wildcardInputTypes: [] }, nodeStyles: { utility: { header: "#654321" } }, resources: {}, socketTypes: {}, nodes: {}, } as const satisfies FxNodeCompositionSeed; const themed = setTheme(seed, theme); assert.equal("theme" in seed, false); assert.notEqual(themed, seed); const rethemed = setTheme(themed, { ...theme, grid: "#abcdef" }); assert.equal(themed.theme.grid, "#112233"); assert.equal(rethemed.theme.grid, "#abcdef"); const sockets = composeSocket(themed, "scalar", { title: "Scalar", color: "#abcdef", acceptsFrom: ["scalar"] }); assert.deepEqual(seed.socketTypes, {}); assert.notEqual(sockets.socketTypes, seed.socketTypes); const definition = { version: 1, title: "Constant", behavior: "standard", style: "utility", parameters: {}, sockets: { out: { title: "Out", direction: "output", type: "scalar", maxIncomingLinks: 0, visible: true, value: null, showValue: false, }, }, ui: [{ kind: "socket", socket: "out" }], muteBypass: [], migrations: [], } as const; const nodes = composeNode(sockets, "constant", definition), replacement = composeNode(nodes, "constant", { ...definition, title: "Replacement" }); assert.equal(nodes.nodes.constant.title, "Constant"); assert.equal(replacement.nodes.constant.title, "Replacement"); assert.deepEqual(sockets.nodes, {}); assert.doesNotThrow(() => structuredClone(replacement)); assert.equal(validateFxNodeComposition(replacement).ok, true); const withoutNode = removeNode(replacement, "constant"); assert.equal("constant" in withoutNode.nodes, false); assert.equal("constant" in replacement.nodes, true); const dangling = removeSocket(replacement, "scalar"), checked = validateFxNodeComposition(dangling); assert.equal(checked.ok, false); if (!checked.ok) assert(checked.issues.some((issue) => issue.code === "reference.socketType")); const withSize: any = structuredClone(replacement); withSize.nodes.constant.defaultSize = { x: 1, y: 1 }; issue(withSize, "shape.unknown", "/nodes/constant/defaultSize"); }); test("compilations are independent immutable clones and expose readonly facades", () => { const a: any = clone(), b: any = clone(); a.id = "same"; b.id = "same"; const ca = compileFxNodeComposition(a), cb = compileFxNodeComposition(b); assert.notEqual(ca.source, cb.source); a.nodes.grade.parameters.exposure.default.value = 9; assert.equal(ca.source.nodes.grade.parameters.exposure.default.value, 0); assert.equal(ca.nodes.get("grade")?.typeId, "grade"); assert(Object.isFrozen(ca)); assert(Object.isFrozen(ca.source)); assert(Object.isFrozen(ca.source.nodes.grade.parameters)); for (const facade of [ca.nodes, ca.socketTypes, ca.styles, ca.resources]) for (const method of ["set", "delete", "clear"]) assert.equal(method in facade, false); let leaked: unknown = "unset"; const size = ca.nodes.size; ca.nodes.forEach(function () { leaked = arguments[2]; }); assert.equal(leaked, undefined); assert.equal(ca.nodes.size, size); }); test("compiled image policies preserve requested source limits and expose effective ceilings", () => { const source: any = clone(); source.resources.image = { ...source.resources.image, maxBytes: 100_000_000, maxWidth: 10_000, maxHeight: 3, maxPixels: 100_000_000, }; const compiled = compileFxNodeComposition(source); assert.deepEqual( { maxBytes: compiled.source.resources.image.maxBytes, maxWidth: compiled.source.resources.image.maxWidth, maxHeight: compiled.source.resources.image.maxHeight, maxPixels: compiled.source.resources.image.maxPixels, }, { maxBytes: 100_000_000, maxWidth: 10_000, maxHeight: 3, maxPixels: 100_000_000 }, ); const effective = compiled.resources.get("image")!; assert.deepEqual( { maxBytes: effective.maxBytes, maxWidth: effective.maxWidth, maxHeight: effective.maxHeight, maxPixels: effective.maxPixels, }, { maxBytes: 33_554_432, maxWidth: 8192, maxHeight: 3, maxPixels: 24_576 }, ); }); type Mutator = (x: any) => void; const cases: [string, string, Mutator][] = [ ["reference.style", "/nodes/grade/style", (x) => (x.nodes.grade.style = "no")], ["reference.socketType", "/socketTypes/wide/acceptsFrom/0", (x) => (x.socketTypes.wide.acceptsFrom[0] = "no")], ["reference.socketType", "/nodes/grade/sockets/input/type", (x) => (x.nodes.grade.sockets.input.type = "no")], ["reference.resource", "/nodes/grade/ui/3/resource", (x) => (x.nodes.grade.ui[3].resource = "no")], ["reference.parameter", "/nodes/grade/ui/1/parameter", (x) => (x.nodes.grade.ui[1].parameter = "no")], ["reference.socket", "/nodes/grade/ui/7/socket", (x) => (x.nodes.grade.ui[7].socket = "no")], [ "reference.parameter", "/nodes/grade/ui/0/visibleWhen/all/0/parameter", (x) => (x.nodes.grade.ui[0].visibleWhen.all[0].parameter = "no"), ], [ "schema.bounds", "/nodes/grade/parameters/exposure/minimum", (x) => (x.nodes.grade.parameters.exposure.minimum = 20), ], [ "schema.integer", "/nodes/constant/parameters/value/integer", (x) => (x.nodes.constant.parameters.value.integer = "yes"), ], [ "schema.precision", "/nodes/grade/parameters/exposure/precision", (x) => (x.nodes.grade.parameters.exposure.precision = 21), ], [ "value.duplicate", "/nodes/grade/parameters/mode/enum", (x) => (x.nodes.grade.parameters.mode.enum = ["film", "film"]), ], [ "schema.enum", "/nodes/grade/parameters/mode/default/value", (x) => (x.nodes.grade.parameters.mode.default.value = "no"), ], [ "schema.bounds", "/nodes/grade/parameters/tint/default/value", (x) => (x.nodes.grade.parameters.tint.default.value[0] = 2), ], [ "schema.bounds", "/nodes/grade/parameters/exposure/default/value", (x) => (x.nodes.grade.parameters.exposure.default.value = 20), ], [ "socket.links", "/nodes/grade/sockets/input/maxIncomingLinks", (x) => (x.nodes.grade.sockets.input.maxIncomingLinks = 0), ], ["socket.output", "/nodes/grade/sockets/output", (x) => (x.nodes.grade.sockets.output.showValue = true)], [ "socket.value", "/nodes/grade/sockets/input/value", (x) => { x.nodes.grade.sockets.input.value = null; x.nodes.grade.sockets.input.showValue = true; }, ], ["ui.widget", "/nodes/grade/ui/5/widget", (x) => (x.nodes.grade.ui[5].widget = "dial")], ["ui.widget", "/nodes/grade/ui/5/parameter", (x) => (x.nodes.grade.ui[5].parameter = "exposure")], ["ui.widget", "/nodes/grade/ui/6/bindings/0/scalar", (x) => (x.nodes.grade.ui[6].bindings[0].scalar = "liftColor")], ["ui.widget", "/nodes/grade/ui/6/bindings/0/color", (x) => (x.nodes.grade.ui[6].bindings[0].color = "lift")], ["ui.resource", "/nodes/grade/ui/3/parameter", (x) => (x.nodes.grade.ui[3].parameter = "exposure")], ["ui.placement", "/nodes/grade/parameters/exposure", (x) => x.nodes.grade.ui.splice(1, 1)], ["ui.placement", "/nodes/grade/ui/2", (x) => (x.nodes.grade.ui[2] = { kind: "parameter", parameter: "exposure" })], ["socket.bypass", "/nodes/grade/muteBypass/0", (x) => (x.nodes.grade.muteBypass = [["output", "input"]])], [ "socket.compatibility", "/nodes/grade/muteBypass/0", (x) => { x.nodes.grade.sockets.input.type = "scalar"; x.nodes.grade.sockets.output.type = "wide"; }, ], ["value.duplicate", "/nodes/grade/muteBypass/1", (x) => x.nodes.grade.muteBypass.push(["input", "output"])], ["limit.resource", "/resources/image/maxWidth", (x) => (x.resources.image.maxWidth = 0)], ["shape.string", "/resources/image/referencePrefix", (x) => (x.resources.image.referencePrefix = "bad")], ["migration.edge", "/nodes/grade/migrations/0", (x) => (x.nodes.grade.migrations[0].toVersion = 4)], [ "migration.outgoing", "/nodes/grade/migrations/1/fromVersion", (x) => x.nodes.grade.migrations.push({ fromVersion: 1, toVersion: 2, steps: [] }), ], [ "reference.migrationTarget", "/nodes/grade/migrations/0/steps/0/key", (x) => (x.nodes.grade.migrations[0].steps[0].key = "no"), ], [ "migration.codec", "/nodes/grade/migrations/0/steps/1/codec", (x) => (x.nodes.grade.migrations[0].steps[1].codec = "no"), ], [ "migration.rename", "/nodes/grade/migrations/0/steps/3/from", (x) => x.nodes.grade.migrations[0].steps.push({ kind: "rename-socket", from: "source", to: "output" }), ], ["shape.literal", "/nodes/grade/behavior", (x) => (x.nodes.grade.behavior = ["standard"])], [ "socket.direction", "/nodes/grade/sockets/input/direction", (x) => (x.nodes.grade.sockets.input.direction = ["input"]), ], ["ui.text", "/nodes/grade/ui/0/variant", (x) => (x.nodes.grade.ui[0].variant = ["header"])], ["shape.string", "/nodes/grade/ui/1/parameter", (x) => (x.nodes.grade.ui[1].parameter = 0)], ["shape.string", "/nodes/grade/ui/3/resource", (x) => (x.nodes.grade.ui[3].resource = 0)], ["ui.target", "/nodes/grade/ui/9/target", (x) => (x.nodes.grade.ui[9].target = "bogus")], ["ui.visibility", "/nodes/grade/ui/0/visibleWhen/all", (x) => (x.nodes.grade.ui[0].visibleWhen = { all: [] })], ["ui.visibility", "/nodes/grade/ui/0/visibleWhen/any", (x) => (x.nodes.grade.ui[0].visibleWhen = { any: [] })], [ "migration.codec", "/nodes/grade/migrations/0/steps/1/parameter", (x) => (x.nodes.grade.migrations[0].steps[1].parameter = "exposure"), ], ]; test("semantic invalid cases report precise codes and paths", () => { for (const [code, path, mutate] of cases) { const x: any = clone(); mutate(x); issue(x, code, path); } }); test("hostile structures fail without executing code or throwing", () => { for (const array of [false, true]) { let calls = 0; const x: any = clone(); if (array) Object.defineProperty(x.nodes.grade.ui, 0, { get() { calls++; return {}; }, enumerable: true, }); else Object.defineProperty(x, "id", { get() { calls++; return "x"; }, enumerable: true, }); issue(x, "data.inspect", array ? "/nodes/grade/ui/0" : "/id"); assert.equal(calls, 0); } const hostile: [unknown, string, string][] = [ [Object.assign(clone(), { [Symbol("x")]: 1 }), "data.symbol", ""], [Object.assign(clone(), { id: new (class X {})() }), "data.type", "/id"], [Object.assign(clone(), { id: new Date() }), "data.type", "/id"], [Object.assign(clone(), { id: new Map() }), "data.type", "/id"], [Object.assign(clone(), { id: new ArrayBuffer(1) }), "data.type", "/id"], [Object.assign(clone(), { id: 1n }), "data.type", "/id"], [Object.assign(clone(), { id: undefined }), "data.type", "/id"], ]; for (const [x, c, p] of hostile) issue(x, c, p); const sparse: any = clone(); sparse.nodes.grade.ui.length++; issue(sparse, "data.array", "/nodes/grade/ui/10"); const keyed: any = []; Object.defineProperty(keyed, "x".repeat(FXNODE_COMPOSITION_LIMITS.maxStringCodeUnits + 1), { value: null, enumerable: true, }); issue(keyed, "limit.strings", ""); const shared: any = clone(); shared.extra = shared.theme; issue(shared, "data.identity", "/extra"); const cycle: any = clone(); cycle.extra = cycle; issue(cycle, "data.identity", "/extra"); const revoked = Proxy.revocable({}, {}); revoked.revoke(); for (const proxy of [ new Proxy( {}, { ownKeys() { throw Error("no"); }, }, ), revoked.proxy, ]) assert.doesNotThrow(() => issue(proxy, "data.inspect", "")); const np = Object.assign(Object.create(null), clone()); const r = validateFxNodeComposition(np); assert.equal(r.ok, true); if (r.ok) assert.equal(r.value.id, "studio"); }); test("collection, visibility, and issue caps", () => { const limits: [string, string, (x: any) => void][] = [ [ "limit.collection", "/nodes", (x) => { const n = x.nodes.internal; x.nodes = {}; for (let i = 0; i <= FXNODE_COMPOSITION_LIMITS.maxNodes; i++) x.nodes[`n${i}`] = structuredClone(n); }, ], [ "limit.collection", "/socketTypes", (x) => { for (let i = 0; i <= FXNODE_COMPOSITION_LIMITS.maxSocketTypes; i++) x.socketTypes[`s${i}`] = { title: "S", color: "#abcdef", acceptsFrom: [] }; }, ], [ "limit.ui", "/nodes/grade/ui", (x) => { x.nodes.grade.ui = Array.from({ length: 257 }, () => ({ kind: "text", variant: "header", title: "x" })); }, ], [ "limit.enum", "/nodes/grade/parameters/mode/enum", (x) => (x.nodes.grade.parameters.mode.enum = Array.from({ length: 257 }, (_, i) => `v${i}`)), ], [ "limit.migrations", "/nodes/grade/migrations", (x) => (x.nodes.grade.migrations = Array.from({ length: 65 }, (_, i) => ({ fromVersion: i + 1, toVersion: i + 2, steps: [], }))), ], [ "limit.migrations", "/nodes/grade/migrations/0/steps", (x) => (x.nodes.grade.migrations[0].steps = Array.from({ length: 129 }, () => ({ kind: "materialize-missing", target: "parameter", key: "exposure", }))), ], [ "limit.visibility", "/nodes/grade/ui/0/visibleWhen/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0/all/0", (x) => { let v: any = { parameter: "mode", equals: "film" }; for (let i = 0; i < 17; i++) v = { all: [v] }; x.nodes.grade.ui[0].visibleWhen = v; }, ], ]; for (const [c, p, m] of limits) { const x: any = clone(); m(x); issue(x, c, p); } for (const target of ["top", "node"]) { const x: any = clone(); const o = target === "top" ? x : x.nodes.grade; for (let i = 0; i < 110; i++) o[`unknown${i}`] = i; const r = validateFxNodeComposition(x); assert.equal(r.ok, false); if (!r.ok) { assert.equal(r.issues.length, FXNODE_COMPOSITION_LIMITS.maxIssues); assert(r.issues.every((i) => i.code === "shape.unknown")); } } }); test("compile error carries frozen issues", () => { try { compileFxNodeComposition({ ...valid(), schemaVersion: 1 } as unknown as ReturnType); assert.fail("expected error"); } catch (e) { assert(e instanceof FxNodeCompositionError); assert(Object.isFrozen(e.issues)); assert(Object.isFrozen(e.issues[0])); assert(e.issues.some((i) => i.path === "/schemaVersion")); } });