379 lines
21 KiB
TypeScript
379 lines
21 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { buildAsset } from '../../src/core/commands/assetCommands';
|
||
import { CommandStack } from '../../src/core/commands/CommandStack';
|
||
import { AddNodeCommand, ConnectCommand, DisconnectCommand, RemoveNodeCommand, SetNodeParamCommand } from '../../src/core/commands/graphCommands';
|
||
import { PlaceAssetCommand, SetParentCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddVariableCommand, RemoveVariableCommand, RenameVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import { type EntityHandle, GraphInterpreter, type NodeContext, allNodeDefs, portsCompatible } from '../../src/core/graph';
|
||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||
import { createEmptyProject } from '../../src/core/model/Project';
|
||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||
|
||
function entity(name: string, x = 0): EntityHandle {
|
||
return { id: name, name, x, y: 0, z: 0, props: {}, parent: null };
|
||
}
|
||
|
||
function context(overrides: Partial<NodeContext> = {}): NodeContext & { logs: string[]; stopped: boolean } {
|
||
const logs: string[] = [];
|
||
const ctx = {
|
||
self: entity('self'),
|
||
owner: entity('owner'),
|
||
variables: { speed: 100 },
|
||
time: 0,
|
||
dt: 0.5,
|
||
logs,
|
||
stopped: false,
|
||
findByName: () => null,
|
||
isKeyDown: () => false,
|
||
log: (m: string) => logs.push(m),
|
||
destroy: () => {},
|
||
stop: () => { ctx.stopped = true; },
|
||
...overrides,
|
||
};
|
||
return ctx;
|
||
}
|
||
|
||
/** Builds "On key held → Move(owner, dx = speed × dt)" through commands, like the editor would. */
|
||
function buildMoverProject() {
|
||
const project = createEmptyProject('p', 'P');
|
||
const behavior = buildAsset('behavior', 'Mover', 'mover') as BehaviorAsset;
|
||
project.assets.push(behavior);
|
||
const stack = new CommandStack(project);
|
||
stack.execute(new AddVariableCommand('mover', 'speed', 'number'));
|
||
const held = new AddNodeCommand('mover', 'OnKeyHeld', 0, 0, { key: 'ArrowRight' });
|
||
const owner = new AddNodeCommand('mover', 'Owner', 0, 100);
|
||
const speed = new AddNodeCommand('mover', 'GetVariable', 0, 200, { variable: 'speed' });
|
||
const mul = new AddNodeCommand('mover', 'Multiply', 200, 150);
|
||
const move = new AddNodeCommand('mover', 'Move', 400, 0);
|
||
for (const c of [held, owner, speed, mul, move]) stack.execute(c);
|
||
stack.execute(new ConnectCommand('mover', { node: held.node.id, port: 'exec' }, { node: move.node.id, port: 'exec' }));
|
||
stack.execute(new ConnectCommand('mover', { node: owner.node.id, port: 'object' }, { node: move.node.id, port: 'object' }));
|
||
stack.execute(new ConnectCommand('mover', { node: speed.node.id, port: 'value' }, { node: mul.node.id, port: 'a' }));
|
||
stack.execute(new ConnectCommand('mover', { node: held.node.id, port: 'dt' }, { node: mul.node.id, port: 'b' }));
|
||
stack.execute(new ConnectCommand('mover', { node: mul.node.id, port: 'result' }, { node: move.node.id, port: 'dx' }));
|
||
return { project, behavior, stack, nodes: { held, owner, speed, mul, move } };
|
||
}
|
||
|
||
describe('node registry', () => {
|
||
it('registers the built-in node set with well-formed ports', () => {
|
||
const defs = allNodeDefs();
|
||
expect(defs.length).toBeGreaterThan(20);
|
||
for (const def of defs) {
|
||
const keys = [...def.inputs, ...def.outputs].map((p) => p.key);
|
||
expect(new Set(def.inputs.map((p) => p.key)).size).toBe(def.inputs.length);
|
||
expect(new Set(def.outputs.map((p) => p.key)).size).toBe(def.outputs.length);
|
||
if (def.category === 'event') expect(def.inputs).toHaveLength(0);
|
||
else expect(def.run).toBeTypeOf('function');
|
||
expect(keys.every((k) => k.length > 0)).toBe(true);
|
||
}
|
||
});
|
||
|
||
it('type-checks port connections', () => {
|
||
expect(portsCompatible('exec', 'exec')).toBe(true);
|
||
expect(portsCompatible('exec', 'number')).toBe(false);
|
||
expect(portsCompatible('number', 'any')).toBe(true);
|
||
expect(portsCompatible('any', 'boolean')).toBe(true);
|
||
expect(portsCompatible('number', 'boolean')).toBe(false);
|
||
expect(portsCompatible('object', 'object')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('GraphInterpreter', () => {
|
||
it('runs an event pulse and pulls data lazily (speed × dt moves the owner)', () => {
|
||
const { behavior } = buildMoverProject();
|
||
const interpreter = new GraphInterpreter(behavior.graph);
|
||
const ctx = context({ variables: { speed: 100 } });
|
||
const fired = interpreter.fire('OnKeyHeld', ctx, { dt: 0.5 }, (n) => n.params.key === 'ArrowRight');
|
||
expect(fired).toBe(1);
|
||
expect(ctx.owner!.x).toBe(50);
|
||
expect(interpreter.fire('OnKeyHeld', ctx, { dt: 0.5 }, (n) => n.params.key === 'ArrowLeft')).toBe(0);
|
||
expect(ctx.owner!.x).toBe(50);
|
||
});
|
||
|
||
it('uses inline values for unconnected inputs', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
const behavior = buildAsset('behavior', 'B', 'b') as BehaviorAsset;
|
||
project.assets.push(behavior);
|
||
const stack = new CommandStack(project);
|
||
const start = new AddNodeCommand('b', 'OnStart', 0, 0);
|
||
const self = new AddNodeCommand('b', 'Self', 0, 0);
|
||
const move = new AddNodeCommand('b', 'Move', 0, 0, { dx: 7, dy: -3 });
|
||
for (const c of [start, self, move]) stack.execute(c);
|
||
stack.execute(new ConnectCommand('b', { node: start.node.id, port: 'exec' }, { node: move.node.id, port: 'exec' }));
|
||
stack.execute(new ConnectCommand('b', { node: self.node.id, port: 'object' }, { node: move.node.id, port: 'object' }));
|
||
const ctx = context();
|
||
new GraphInterpreter(behavior.graph).fire('OnStart', ctx);
|
||
expect(ctx.self.x).toBe(7);
|
||
expect(ctx.self.y).toBe(-3);
|
||
});
|
||
|
||
it('uses port defaults for unconnected inputs (Move scale = 1, Clamp 0…1)', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
const behavior = buildAsset('behavior', 'B', 'b') as BehaviorAsset;
|
||
project.assets.push(behavior);
|
||
const stack = new CommandStack(project);
|
||
const start = new AddNodeCommand('b', 'OnStart', 0, 0);
|
||
const self = new AddNodeCommand('b', 'Self', 0, 0);
|
||
const clamp = new AddNodeCommand('b', 'Clamp', 0, 0, { value: 5 });
|
||
const move = new AddNodeCommand('b', 'Move', 0, 0, { dx: 4 });
|
||
for (const c of [start, self, clamp, move]) stack.execute(c);
|
||
stack.execute(new ConnectCommand('b', { node: start.node.id, port: 'exec' }, { node: move.node.id, port: 'exec' }));
|
||
stack.execute(new ConnectCommand('b', { node: self.node.id, port: 'object' }, { node: move.node.id, port: 'object' }));
|
||
stack.execute(new ConnectCommand('b', { node: clamp.node.id, port: 'result' }, { node: move.node.id, port: 'dy' }));
|
||
const ctx = context();
|
||
new GraphInterpreter(behavior.graph).fire('OnStart', ctx);
|
||
expect(ctx.self.x).toBe(4); // scale defaulted to 1, not 0
|
||
expect(ctx.self.y).toBe(1); // clamp(5, 0, 1)
|
||
});
|
||
|
||
it('five-node mover: Axis input → Move(dt from On update) moves speed × dt, diagonals normalized', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
const behavior = buildAsset('behavior', 'Mover', 'mover') as BehaviorAsset;
|
||
project.assets.push(behavior);
|
||
const stack = new CommandStack(project);
|
||
stack.execute(new AddVariableCommand('mover', 'speed', 'number'));
|
||
const update = new AddNodeCommand('mover', 'OnUpdate', 0, 0);
|
||
const owner = new AddNodeCommand('mover', 'Owner', 0, 0);
|
||
const axis = new AddNodeCommand('mover', 'AxisInput', 0, 0);
|
||
const speed = new AddNodeCommand('mover', 'GetVariable', 0, 0, { variable: 'speed' });
|
||
const move = new AddNodeCommand('mover', 'Move', 0, 0);
|
||
for (const c of [update, owner, axis, speed, move]) stack.execute(c);
|
||
const connect = (from: AddNodeCommand, fp: string, to: AddNodeCommand, tp: string) =>
|
||
stack.execute(new ConnectCommand('mover', { node: from.node.id, port: fp }, { node: to.node.id, port: tp }));
|
||
connect(update, 'exec', move, 'exec');
|
||
connect(update, 'dt', move, 'dt');
|
||
connect(owner, 'object', move, 'object');
|
||
connect(axis, 'x', move, 'dx');
|
||
connect(axis, 'y', move, 'dy');
|
||
connect(speed, 'value', move, 'scale');
|
||
expect(behavior.graph.nodes).toHaveLength(5);
|
||
|
||
const interpreter = new GraphInterpreter(behavior.graph);
|
||
const run = (keys: string[]) => {
|
||
const ctx = context({ variables: { speed: 100 }, dt: 0.5, isKeyDown: (k) => keys.includes(k) });
|
||
interpreter.fire('OnUpdate', ctx, { dt: 0.5 });
|
||
return { x: ctx.owner!.x, y: ctx.owner!.y };
|
||
};
|
||
expect(run([])).toEqual({ x: 0, y: 0 });
|
||
expect(run(['ArrowRight'])).toEqual({ x: 50, y: 0 });
|
||
expect(run(['ArrowLeft'])).toEqual({ x: -50, y: 0 });
|
||
expect(run(['ArrowUp'])).toEqual({ x: 0, y: -50 });
|
||
const diagonal = run(['ArrowLeft', 'ArrowUp']);
|
||
expect(Math.hypot(diagonal.x, diagonal.y)).toBeCloseTo(50, 6); // same distance as a straight move
|
||
expect(diagonal.x).toBeCloseTo(-50 / Math.SQRT2, 6);
|
||
expect(diagonal.y).toBeCloseTo(-50 / Math.SQRT2, 6);
|
||
expect(run(['ArrowLeft', 'ArrowRight'])).toEqual({ x: 0, y: 0 }); // opposite keys cancel
|
||
});
|
||
|
||
it('vector and math helpers', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
const behavior = buildAsset('behavior', 'B', 'b') as BehaviorAsset;
|
||
project.assets.push(behavior);
|
||
const stack = new CommandStack(project);
|
||
const start = new AddNodeCommand('b', 'OnStart', 0, 0);
|
||
const norm = new AddNodeCommand('b', 'Normalize', 0, 0, { x: 3, y: 4 });
|
||
const len = new AddNodeCommand('b', 'Length', 0, 0, { x: 3, y: 4 });
|
||
const sqrt = new AddNodeCommand('b', 'Sqrt', 0, 0, { value: 16 });
|
||
const abs = new AddNodeCommand('b', 'Abs', 0, 0, { value: -2 });
|
||
const min = new AddNodeCommand('b', 'Min', 0, 0, { a: 3, b: -1 });
|
||
const max = new AddNodeCommand('b', 'Max', 0, 0, { a: 3, b: -1 });
|
||
const logs = [norm, len, sqrt, abs, min, max].map(() => new AddNodeCommand('b', 'Log', 0, 0));
|
||
for (const c of [start, norm, len, sqrt, abs, min, max, ...logs]) stack.execute(c);
|
||
const connect = (from: AddNodeCommand, fp: string, to: AddNodeCommand, tp: string) =>
|
||
stack.execute(new ConnectCommand('b', { node: from.node.id, port: fp }, { node: to.node.id, port: tp }));
|
||
let previous = start;
|
||
for (const log of logs) {
|
||
connect(previous, 'exec', log, 'exec');
|
||
previous = log;
|
||
}
|
||
connect(norm, 'x', logs[0]!, 'message');
|
||
connect(len, 'result', logs[1]!, 'message');
|
||
connect(sqrt, 'result', logs[2]!, 'message');
|
||
connect(abs, 'result', logs[3]!, 'message');
|
||
connect(min, 'result', logs[4]!, 'message');
|
||
connect(max, 'result', logs[5]!, 'message');
|
||
const ctx = context();
|
||
new GraphInterpreter(behavior.graph).fire('OnStart', ctx);
|
||
expect(ctx.logs).toEqual(['0.6', '5', '4', '2', '-1', '3']);
|
||
});
|
||
|
||
it('follows Branch, writes variables and logs', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
const behavior = buildAsset('behavior', 'B', 'b') as BehaviorAsset;
|
||
project.assets.push(behavior);
|
||
const stack = new CommandStack(project);
|
||
stack.execute(new AddVariableCommand('b', 'count', 'number'));
|
||
const start = new AddNodeCommand('b', 'OnStart', 0, 0);
|
||
const get = new AddNodeCommand('b', 'GetVariable', 0, 0, { variable: 'count' });
|
||
const cmp = new AddNodeCommand('b', 'Compare', 0, 0, { op: '<', b: 3 });
|
||
const branch = new AddNodeCommand('b', 'Branch', 0, 0);
|
||
const add = new AddNodeCommand('b', 'Add', 0, 0, { b: 1 });
|
||
const set = new AddNodeCommand('b', 'SetVariable', 0, 0, { variable: 'count' });
|
||
const logThen = new AddNodeCommand('b', 'Log', 0, 0, { message: 'then' });
|
||
const logElse = new AddNodeCommand('b', 'Log', 0, 0, { message: 'else' });
|
||
for (const c of [start, get, cmp, branch, add, set, logThen, logElse]) stack.execute(c);
|
||
const connect = (from: AddNodeCommand, fp: string, to: AddNodeCommand, tp: string) =>
|
||
stack.execute(new ConnectCommand('b', { node: from.node.id, port: fp }, { node: to.node.id, port: tp }));
|
||
connect(start, 'exec', branch, 'exec');
|
||
connect(get, 'value', cmp, 'a');
|
||
connect(cmp, 'result', branch, 'condition');
|
||
connect(branch, 'then', set, 'exec');
|
||
connect(get, 'value', add, 'a');
|
||
connect(add, 'result', set, 'value');
|
||
connect(set, 'exec', logThen, 'exec');
|
||
connect(branch, 'else', logElse, 'exec');
|
||
|
||
const interpreter = new GraphInterpreter(behavior.graph);
|
||
const ctx = context({ variables: { count: 0 } });
|
||
for (let i = 0; i < 5; i++) interpreter.fire('OnStart', ctx);
|
||
expect(ctx.variables.count).toBe(3);
|
||
expect(ctx.logs).toEqual(['then', 'then', 'then', 'else', 'else']);
|
||
});
|
||
|
||
it('reports traversed edges and nodes to the trace listener', () => {
|
||
const { behavior, nodes } = buildMoverProject();
|
||
const interpreter = new GraphInterpreter(behavior.graph);
|
||
const edges: string[] = [];
|
||
const visited: string[] = [];
|
||
interpreter.trace = { onEdge: (id) => edges.push(id), onNode: (id) => visited.push(id) };
|
||
interpreter.fire('OnKeyHeld', context(), { dt: 1 });
|
||
expect(edges).toHaveLength(1);
|
||
expect(visited).toContain(nodes.mul.node.id);
|
||
expect(visited).toContain(nodes.move.node.id);
|
||
});
|
||
|
||
it('stops a runaway flow instead of hanging', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
const behavior = buildAsset('behavior', 'B', 'b') as BehaviorAsset;
|
||
project.assets.push(behavior);
|
||
const stack = new CommandStack(project);
|
||
const start = new AddNodeCommand('b', 'OnStart', 0, 0);
|
||
const a = new AddNodeCommand('b', 'Sequence', 0, 0);
|
||
const b = new AddNodeCommand('b', 'Sequence', 0, 0);
|
||
for (const c of [start, a, b]) stack.execute(c);
|
||
stack.execute(new ConnectCommand('b', { node: start.node.id, port: 'exec' }, { node: a.node.id, port: 'exec' }));
|
||
stack.execute(new ConnectCommand('b', { node: a.node.id, port: 'a' }, { node: b.node.id, port: 'exec' }));
|
||
stack.execute(new ConnectCommand('b', { node: b.node.id, port: 'a' }, { node: a.node.id, port: 'exec' }));
|
||
expect(() => new GraphInterpreter(behavior.graph).fire('OnStart', context())).toThrow(/step limit/);
|
||
});
|
||
});
|
||
|
||
describe('graph commands', () => {
|
||
it('rejects incompatible, self and duplicate connections', () => {
|
||
const { stack, nodes } = buildMoverProject();
|
||
expect(() => stack.execute(new ConnectCommand('mover', { node: nodes.held.node.id, port: 'exec' }, { node: nodes.mul.node.id, port: 'a' }))).toThrow(/Cannot connect/);
|
||
expect(() => stack.execute(new ConnectCommand('mover', { node: nodes.mul.node.id, port: 'result' }, { node: nodes.mul.node.id, port: 'a' }))).toThrow(/itself/);
|
||
expect(() => stack.execute(new ConnectCommand('mover', { node: nodes.held.node.id, port: 'exec' }, { node: nodes.move.node.id, port: 'exec' }))).toThrow(/Already/);
|
||
expect(() => stack.execute(new ConnectCommand('mover', { node: nodes.held.node.id, port: 'nope' }, { node: nodes.move.node.id, port: 'exec' }))).toThrow(/Port/);
|
||
});
|
||
|
||
it('replaces the edge of a data input and restores it on undo', () => {
|
||
const { behavior, stack, nodes } = buildMoverProject();
|
||
const literal = new AddNodeCommand('mover', 'Number', 0, 0, { value: 9 });
|
||
stack.execute(literal);
|
||
const before = behavior.graph.edges.length;
|
||
stack.execute(new ConnectCommand('mover', { node: literal.node.id, port: 'value' }, { node: nodes.move.node.id, port: 'dx' }));
|
||
expect(behavior.graph.edges.length).toBe(before); // one replaced, one added
|
||
expect(behavior.graph.edges.find((e) => e.to.node === nodes.move.node.id && e.to.port === 'dx')!.from.node).toBe(literal.node.id);
|
||
stack.undo();
|
||
expect(behavior.graph.edges.find((e) => e.to.node === nodes.move.node.id && e.to.port === 'dx')!.from.node).toBe(nodes.mul.node.id);
|
||
});
|
||
|
||
it('removing a node drops its edges; undo restores both', () => {
|
||
const { behavior, stack, nodes } = buildMoverProject();
|
||
const edgesBefore = behavior.graph.edges.length;
|
||
stack.execute(new RemoveNodeCommand('mover', nodes.mul.node.id));
|
||
expect(behavior.graph.nodes.some((n) => n.id === nodes.mul.node.id)).toBe(false);
|
||
expect(behavior.graph.edges.length).toBe(edgesBefore - 3);
|
||
stack.undo();
|
||
expect(behavior.graph.nodes.some((n) => n.id === nodes.mul.node.id)).toBe(true);
|
||
expect(behavior.graph.edges.length).toBe(edgesBefore);
|
||
stack.execute(new DisconnectCommand('mover', behavior.graph.edges[0]!.id));
|
||
expect(behavior.graph.edges.length).toBe(edgesBefore - 1);
|
||
stack.undo();
|
||
expect(behavior.graph.edges.length).toBe(edgesBefore);
|
||
});
|
||
|
||
it('coerces declared params and keeps inline input values', () => {
|
||
const { behavior, stack, nodes } = buildMoverProject();
|
||
stack.execute(new SetNodeParamCommand('mover', nodes.held.node.id, 'key', 'ArrowLeft'));
|
||
stack.execute(new SetNodeParamCommand('mover', nodes.move.node.id, 'dy', 4));
|
||
const held = behavior.graph.nodes.find((n) => n.id === nodes.held.node.id)!;
|
||
const move = behavior.graph.nodes.find((n) => n.id === nodes.move.node.id)!;
|
||
expect(held.params.key).toBe('ArrowLeft');
|
||
expect(move.params.dy).toBe(4);
|
||
stack.undo();
|
||
expect(move.params.dy).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('variables', () => {
|
||
it('adds, renames (including graph references and overrides) and removes variables', () => {
|
||
const { project, behavior, stack, nodes } = buildMoverProject();
|
||
const scene = buildAsset('scene', 'S', 's');
|
||
project.assets.push(scene);
|
||
const place = new PlaceAssetCommand('s', 'mover', { x: 0, y: 0 });
|
||
stack.execute(place);
|
||
const object = (scene as { objects: { id: string; overrides: Record<string, unknown> }[] }).objects[0]!;
|
||
object.overrides.speed = 5;
|
||
|
||
stack.execute(new RenameVariableCommand('mover', 'speed', 'velocity'));
|
||
expect(behavior.variables[0]!.key).toBe('velocity');
|
||
expect(behavior.props).toEqual({ velocity: 0 });
|
||
expect(object.overrides).toEqual({ velocity: 5 });
|
||
expect(behavior.graph.nodes.find((n) => n.id === nodes.speed.node.id)!.params.variable).toBe('velocity');
|
||
stack.undo();
|
||
expect(behavior.variables[0]!.key).toBe('speed');
|
||
expect(object.overrides).toEqual({ speed: 5 });
|
||
|
||
expect(() => stack.execute(new AddVariableCommand('mover', 'speed', 'number'))).toThrow(/already/);
|
||
expect(() => stack.execute(new AddVariableCommand('mover', '1bad', 'number'))).toThrow(/names/);
|
||
|
||
stack.execute(new RemoveVariableCommand('mover', 'speed'));
|
||
expect(behavior.variables).toHaveLength(0);
|
||
expect(object.overrides).toEqual({});
|
||
stack.undo();
|
||
expect(behavior.variables).toHaveLength(1);
|
||
expect(object.overrides).toEqual({ speed: 5 });
|
||
});
|
||
|
||
it('the owner is the parent: validated, kept across a round trip, dropped when dangling', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
const scene = buildAsset('scene', 'S', 's');
|
||
const sprite = buildAsset('sprite', 'Box', 'box');
|
||
const behavior = buildAsset('behavior', 'B', 'b');
|
||
project.assets.push(scene, sprite, behavior);
|
||
const stack = new CommandStack(project);
|
||
const placeBox = new PlaceAssetCommand('s', 'box', { x: 0, y: 0 });
|
||
const placeB = new PlaceAssetCommand('s', 'b', { x: 0, y: 0 });
|
||
stack.execute(placeBox);
|
||
stack.execute(placeB);
|
||
expect(() => stack.execute(new SetParentCommand('s', placeB.objectId, placeB.objectId))).toThrow(/own parent/);
|
||
stack.execute(new SetParentCommand('s', placeB.objectId, placeBox.objectId));
|
||
const objectsOf = (p: unknown) => ((p as { assets: { objects: { id: string; parentId: string }[] }[] }).assets[0]!).objects;
|
||
expect(objectsOf(project)[1]!.parentId).toBe(placeBox.objectId);
|
||
// Round trip keeps the parent; a dangling parent is dropped by the loader.
|
||
expect(objectsOf(deserializeProject(serializeProject(project)))[1]!.parentId).toBe(placeBox.objectId);
|
||
const raw = serializeProject(project) as { assets: { objects?: { parentId: string }[] }[] };
|
||
raw.assets[0]!.objects![1]!.parentId = 'ghost';
|
||
expect(objectsOf(deserializeProject(raw))[1]!.parentId).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('behavior serialization', () => {
|
||
it('round-trips variables and graph, dropping unknown node types and dangling edges', () => {
|
||
const { project, behavior } = buildMoverProject();
|
||
const loaded = deserializeProject(serializeProject(project)).assets.find((a) => a.id === 'mover') as BehaviorAsset;
|
||
expect(loaded.variables).toEqual(behavior.variables);
|
||
expect(loaded.graph).toEqual(behavior.graph);
|
||
expect(loaded.props).toEqual({ speed: 0 });
|
||
|
||
const raw = serializeProject(project) as { assets: { graph?: { nodes: unknown[]; edges: unknown[] } }[] };
|
||
raw.assets[0]!.graph!.nodes.push({ id: 'zzz', type: 'DoesNotExist', x: 0, y: 0, params: {} });
|
||
raw.assets[0]!.graph!.edges.push({ id: 'e-bad', from: { node: 'zzz', port: 'exec' }, to: { node: behavior.graph.nodes[0]!.id, port: 'exec' } });
|
||
const cleaned = deserializeProject(raw).assets[0] as BehaviorAsset;
|
||
expect(cleaned.graph.nodes.length).toBe(behavior.graph.nodes.length);
|
||
expect(cleaned.graph.edges.length).toBe(behavior.graph.edges.length);
|
||
});
|
||
});
|