feat: add render graph driven renderer architecture
Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
+327
@@ -0,0 +1,327 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { validCommand, validFxNodeReplayCommand } from "@lib/commands/validate.js";
|
||||
import { commandId, linkId, nodeId, socketId } from "@lib/core/types.js";
|
||||
import { saveCompositionCompatibility } from "@lib/composition/save-compatibility.js";
|
||||
import { createFxNodeHeadless } from "@lib/headless-runtime.js";
|
||||
import { APPLICATION_COMPILED, APPLICATION_HEADLESS } from "./application.js";
|
||||
import type { FxNodeCompositionData } from "@lib/composition/index.js";
|
||||
|
||||
const composition = {
|
||||
...APPLICATION_COMPILED.source,
|
||||
id: "command-log-test",
|
||||
version: 7,
|
||||
nodes: { value: APPLICATION_COMPILED.source.nodes["fxnode.shader.value"]! },
|
||||
};
|
||||
const runtime = createFxNodeHeadless(composition);
|
||||
const save = (commands: readonly unknown[], baseline: unknown = runtime.save(runtime.emptyDocument())) => ({
|
||||
kind: "fxnode.command-log",
|
||||
schemaVersion: 2,
|
||||
composition,
|
||||
baseline,
|
||||
commands,
|
||||
});
|
||||
|
||||
test("command validators are total and replay is forward-only", () => {
|
||||
const hostile = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys() {
|
||||
throw new Error("hostile");
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.doesNotThrow(() => validCommand(hostile));
|
||||
assert.equal(validCommand(hostile), false);
|
||||
assert.equal(validCommand({ type: "undo" }), true);
|
||||
assert.equal(validFxNodeReplayCommand({ type: "undo" }), false);
|
||||
assert.equal(
|
||||
validFxNodeReplayCommand({ type: "batch", commands: [{ type: "node.move", id: "n", position: { x: 1, y: 2 } }] }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("replay commits atomically once, preserves bounded staged history, and accepts empty logs", () => {
|
||||
const original = runtime.createEngine(runtime.emptyDocument(), 1),
|
||||
commands = [
|
||||
{ type: "node.add", nodeId: nodeId("n"), nodeType: "value", position: { x: 0, y: 0 } } as const,
|
||||
{ type: "node.move", id: nodeId("n"), position: { x: 2, y: 3 } } as const,
|
||||
];
|
||||
const result = runtime.replaySaveData(original, save(commands), 0, commandId("restore"));
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.equal(result.status, "committed");
|
||||
if (result.status !== "committed") return;
|
||||
assert.equal(Object.isFrozen(result.saveData), true);
|
||||
assert.notEqual(result.saveData, save(commands));
|
||||
assert.equal(result.state.version, 1);
|
||||
assert.deepEqual(result.state.document.nodes.n!.position, { x: 2, y: 3 });
|
||||
assert.equal(result.state.undo.length, 1);
|
||||
assert.equal(result.state.redo.length, 0);
|
||||
assert.equal(result.mutationEnvelope.cause, "load");
|
||||
assert.equal(result.mutationEnvelope.mutations.length, 1);
|
||||
assert.equal(result.mutationEnvelope.mutations[0]?.kind, "document.replaced");
|
||||
const empty = runtime.replaySaveData(result.state, save([]));
|
||||
assert.equal(empty.ok, true);
|
||||
if (empty.ok) {
|
||||
assert.equal(empty.state.version, 2);
|
||||
assert.equal(empty.state.undo.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test("replay rejection, noop, composition mismatch, and stale source inspection are atomic", () => {
|
||||
const state = runtime.createEngine(runtime.emptyDocument());
|
||||
for (const data of [
|
||||
save([{ type: "node.remove", id: "missing" }]),
|
||||
save([{ type: "batch", commands: [] }]),
|
||||
{ ...save([]), composition: { ...composition, id: "other" } },
|
||||
]) {
|
||||
const result = runtime.replaySaveData(state, data);
|
||||
assert.equal(result.ok, false);
|
||||
if (!result.ok) assert.equal(result.state, state);
|
||||
}
|
||||
const noop = runtime.replaySaveData(state, save([{ type: "batch", commands: [] }]));
|
||||
assert.equal(noop.ok, false);
|
||||
if (!noop.ok) {
|
||||
assert.equal(noop.issues[0]?.code, "replay.noop");
|
||||
assert.equal(noop.issues[0]?.path, "/commands/0");
|
||||
}
|
||||
let inspected = false;
|
||||
const hostile = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys() {
|
||||
inspected = true;
|
||||
throw new Error("inspected");
|
||||
},
|
||||
},
|
||||
);
|
||||
const stale = runtime.replaySaveData(state, hostile, 1);
|
||||
assert.equal(stale.ok, false);
|
||||
assert.equal(inspected, false);
|
||||
if (!stale.ok) {
|
||||
assert.equal(stale.state, state);
|
||||
assert.equal(stale.issues[0]?.code, "version.stale");
|
||||
}
|
||||
const unchanged = runtime.replaySaveData(state, save([]));
|
||||
assert.equal(unchanged.ok, true);
|
||||
if (unchanged.ok) {
|
||||
assert.equal(unchanged.status, "noop");
|
||||
assert.equal(unchanged.state.version, state.version);
|
||||
assert.equal(unchanged.state.document, state.document);
|
||||
}
|
||||
});
|
||||
|
||||
test("persisted commands reject malformed durable values and links atomically", () => {
|
||||
const state = runtime.createEngine(runtime.emptyDocument());
|
||||
const link = {
|
||||
id: "l",
|
||||
fromNodeId: "a",
|
||||
fromSocketId: "a:o",
|
||||
toNodeId: "b",
|
||||
toSocketId: "b:i",
|
||||
muted: false,
|
||||
extensions: {},
|
||||
};
|
||||
const malformed = [
|
||||
{ type: "link.add", link: { ...link, muted: 0 } },
|
||||
{ type: "link.add", link: { ...link, extensions: { bad: undefined } } },
|
||||
{ type: "node.parameter", id: "n", key: "value", value: { kind: "number", value: "1" } },
|
||||
{ type: "node.socket-default", id: "n", socketId: "n:o", value: { kind: "vector", value: [1, 2] } },
|
||||
{ type: "node.add", nodeId: "n", nodeType: "not-bound", position: { x: 0, y: 0 } },
|
||||
];
|
||||
for (const command of malformed) {
|
||||
const result = runtime.replaySaveData(state, save([command]));
|
||||
assert.equal(result.ok, false);
|
||||
if (!result.ok) {
|
||||
assert.equal(result.state, state);
|
||||
assert.match(result.issues[0]?.path ?? "", /^\/commands\/0/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("command and baseline admissions have independent budgets and require V2", () => {
|
||||
const state = runtime.createEngine(runtime.emptyDocument()),
|
||||
baseline = runtime.save(runtime.emptyDocument());
|
||||
const large = { ...baseline, metadata: { values: Array.from({ length: 100_001 }, () => 0) } };
|
||||
assert.equal(runtime.replaySaveData(state, save([], large)).ok, true);
|
||||
const commands = Array.from({ length: 1_001 }, () => ({ type: "node.remove", id: "n" }));
|
||||
const over = runtime.replaySaveData(state, save(commands, baseline));
|
||||
assert.equal(over.ok, false);
|
||||
if (!over.ok) assert.equal(over.state, state);
|
||||
const v1 = runtime.replaySaveData(state, save([], { ...baseline, schemaVersion: 1 }));
|
||||
assert.equal(v1.ok, false);
|
||||
if (!v1.ok) {
|
||||
assert.equal(v1.state, state);
|
||||
assert.equal(v1.issues[0]?.code, "baseline.schema");
|
||||
}
|
||||
});
|
||||
|
||||
test("save schema v1 and future versions have distinct structured errors", () => {
|
||||
const state = runtime.createEngine(runtime.emptyDocument());
|
||||
for (const [schemaVersion, code] of [
|
||||
[1, "save.schema.unsupported"],
|
||||
[3, "save.schema.future"],
|
||||
] as const) {
|
||||
const result = runtime.replaySaveData(state, { ...save([]), schemaVersion });
|
||||
assert.equal(result.ok, false);
|
||||
if (!result.ok) {
|
||||
assert.equal(result.issues[0]?.code, code);
|
||||
assert.equal(result.issues[0]?.path, "/schemaVersion");
|
||||
assert.equal(result.state, state);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("save decode alone normalizes embedded schema-1 composition menus", () => {
|
||||
const state = runtime.createEngine(runtime.emptyDocument());
|
||||
const legacyComposition = {
|
||||
...composition,
|
||||
schemaVersion: 1,
|
||||
menuGroups: { legacy: { title: "Legacy", order: 0 } },
|
||||
nodes: Object.fromEntries(
|
||||
Object.entries(composition.nodes).map(([id, node]) => [
|
||||
id,
|
||||
{ ...node, menu: { kind: "entry", group: "legacy", order: 0, keywords: [] } },
|
||||
]),
|
||||
),
|
||||
};
|
||||
const result = runtime.replaySaveData(state, { ...save([]), composition: legacyComposition });
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.equal(result.saveData.composition.schemaVersion, 2);
|
||||
assert.equal("menuGroups" in result.saveData.composition, false);
|
||||
assert.equal("menu" in result.saveData.composition.nodes.value!, false);
|
||||
assert.equal(validateLegacyRaw(legacyComposition), false);
|
||||
|
||||
for (const invalid of [
|
||||
{ ...legacyComposition, menuGroups: undefined },
|
||||
{ ...legacyComposition, nodes: { ...legacyComposition.nodes, value: composition.nodes.value } },
|
||||
{
|
||||
...legacyComposition,
|
||||
nodes: {
|
||||
...legacyComposition.nodes,
|
||||
value: { ...legacyComposition.nodes.value, menu: { kind: "entry", group: "missing", order: 0, keywords: [] } },
|
||||
},
|
||||
},
|
||||
{ ...legacyComposition, unexpected: true },
|
||||
]) {
|
||||
const rejected = runtime.replaySaveData(state, { ...save([]), composition: invalid });
|
||||
assert.equal(rejected.ok, false);
|
||||
}
|
||||
});
|
||||
|
||||
const validateLegacyRaw = (value: unknown) => {
|
||||
try {
|
||||
createFxNodeHeadless(value as FxNodeCompositionData);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
test("composition compatibility accepts true supersets and rejects changed replay semantics", () => {
|
||||
const extra = APPLICATION_COMPILED.source.nodes["fxnode.shader.color"]!,
|
||||
presentation = {
|
||||
...composition,
|
||||
version: 8,
|
||||
theme: { ...composition.theme, background: "#123456" as const },
|
||||
nodes: { ...composition.nodes, extra },
|
||||
};
|
||||
const reordered = {
|
||||
...presentation,
|
||||
nodes: {
|
||||
...presentation.nodes,
|
||||
value: { ...presentation.nodes.value!, ui: [...presentation.nodes.value!.ui].reverse() },
|
||||
},
|
||||
};
|
||||
const baseline = runtime.save(runtime.emptyDocument());
|
||||
assert.deepEqual(saveCompositionCompatibility(composition, reordered, baseline), []);
|
||||
const supersetRuntime = createFxNodeHeadless(reordered),
|
||||
accepted = supersetRuntime.replaySaveData(supersetRuntime.createEngine(supersetRuntime.emptyDocument()), save([]));
|
||||
assert.equal(accepted.ok, true);
|
||||
const changed = {
|
||||
...presentation,
|
||||
nodes: {
|
||||
...presentation.nodes,
|
||||
value: {
|
||||
...presentation.nodes.value!,
|
||||
parameters: {
|
||||
value: { ...presentation.nodes.value!.parameters.value!, default: { kind: "number" as const, value: 42 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const semantic = saveCompositionCompatibility(composition, changed as FxNodeCompositionData, baseline);
|
||||
assert.equal(semantic[0]?.code, "composition.incompatible");
|
||||
assert.equal(semantic[1]?.code, "composition.node-semantic");
|
||||
assert.equal(semantic[1]?.path, "/composition/nodes/value");
|
||||
const missing = { ...presentation, nodes: { extra } };
|
||||
const absent = saveCompositionCompatibility(composition, missing, baseline);
|
||||
assert.equal(absent[0]?.code, "composition.incompatible");
|
||||
assert.equal(absent[1]?.code, "composition.definition-missing");
|
||||
const forged = {
|
||||
...save([{ type: "node.add", nodeId: "x", nodeType: "extra", position: { x: 0, y: 0 } }]),
|
||||
composition,
|
||||
};
|
||||
const rejected = supersetRuntime.replaySaveData(
|
||||
supersetRuntime.createEngine(supersetRuntime.emptyDocument()),
|
||||
forged,
|
||||
);
|
||||
assert.equal(rejected.ok, false);
|
||||
if (!rejected.ok) assert.equal(rejected.issues[0]?.code, "command.invalid");
|
||||
});
|
||||
|
||||
test("compatibility guards opaque promotion and caps structured issues", () => {
|
||||
const extra = APPLICATION_COMPILED.source.nodes["fxnode.shader.color"]!,
|
||||
current = { ...composition, nodes: { ...composition.nodes, opaque: extra } };
|
||||
const opaque = { ...runtime.save(runtime.emptyDocument()), nodes: [{ id: "o", typeId: "opaque" }] as never };
|
||||
const promoted = saveCompositionCompatibility(composition, current, opaque);
|
||||
assert.equal(promoted[0]?.code, "composition.incompatible");
|
||||
assert.equal(promoted[1]?.code, "composition.opaque-promotion");
|
||||
assert.equal(promoted[1]?.path, "/baseline/nodes/0/typeId");
|
||||
const many = Object.fromEntries(
|
||||
Array.from({ length: 150 }, (_, i) => [`style-${i}`, { header: "#000000" }]),
|
||||
) as unknown as typeof composition.nodeStyles,
|
||||
withMany = { ...composition, nodeStyles: many };
|
||||
const capped = saveCompositionCompatibility(
|
||||
withMany,
|
||||
{ ...composition, nodeStyles: {} },
|
||||
runtime.save(runtime.emptyDocument()),
|
||||
);
|
||||
assert.equal(capped.length, 100);
|
||||
});
|
||||
|
||||
test("trusted journal validation rejects executable commands outside the durable schema", () => {
|
||||
const full = createFxNodeHeadless(APPLICATION_COMPILED.source),
|
||||
from = full.materializeNode("from", "fxnode.shader.value"),
|
||||
to = full.materializeNode("to", "fxnode.shader.math"),
|
||||
baseline = full.save({ ...full.emptyDocument(), nodes: { from, to } }),
|
||||
link = {
|
||||
id: linkId("l"),
|
||||
fromNodeId: nodeId("from"),
|
||||
fromSocketId: socketId("from:value"),
|
||||
toNodeId: nodeId("to"),
|
||||
toSocketId: socketId("to:a"),
|
||||
muted: false,
|
||||
extensions: {},
|
||||
};
|
||||
assert.equal(full.validateReplayJournal(baseline, [{ type: "link.add", link }]), true);
|
||||
assert.equal(
|
||||
full.validateReplayJournal(baseline, [{ type: "link.add", link: { ...link, transient: "not durable" } as never }]),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("replay rejects a command whose resulting aggregate exceeds persistence closure", () => {
|
||||
const node = runtime.materializeNode("n", "value"),
|
||||
baseline = runtime.save({ ...runtime.emptyDocument(), nodes: { n: node } });
|
||||
const baselineText = "b".repeat(8_388_350),
|
||||
commandText = "c".repeat(1_048_554);
|
||||
const result = runtime.replaySaveData(
|
||||
runtime.createEngine(runtime.emptyDocument()),
|
||||
save([{ type: "node.label", id: "n", label: commandText }], { ...baseline, metadata: { text: baselineText } }),
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
if (!result.ok) assert.equal(result.state.document.nodes.n, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user