/** Ports a node works out for itself (`NodeDef.derivePorts`): from its settings, or from its own edges. */ import { describe, expect, it } from 'vitest'; import { requireNodeDef } from '../../src/core/commands/require'; import { AddPortCommand } from '../../src/core/commands/portCommands'; import { DisconnectCommand, SetNodeParamCommand } from '../../src/core/commands/graphCommands'; import { BEHAVIOR_END, BEHAVIOR_START, type GraphNode, GraphInterpreter, inlineInputs, resolveDef, } from '../../src/core/graph'; import { graphLookup } from '../../src/core/model/graphLookup'; import type { BehaviorAsset } from '../../src/core/model/Asset'; import { fakeContext, graphEditor, makeBehavior, makeProject, type ProjectFixture, fire } from '../helpers'; function sequenceFixture() { const fixture = makeProject(); const behavior = makeBehavior(fixture, 'B'); const edit = graphEditor(fixture, behavior); return { ...fixture, behavior, edit }; } const portsOf = (behavior: BehaviorAsset, node: GraphNode, fixture: ProjectFixture) => resolveDef(node, behavior.graph, graphLookup(fixture.project))!.outputs.map((p) => p.key); const edgeKeys = (behavior: BehaviorAsset) => behavior.graph.edges.map((e) => `${e.from.port}→${e.to.port}`).sort(); describe('a Sequence sized by what is plugged into it', () => { it('starts with one output and grows a spare as each last one is connected', () => { const fixture = sequenceFixture(); const { behavior, edit } = fixture; const node = edit.addNode('Sequence'); expect(portsOf(behavior, node, fixture)).toEqual(['a']); edit.connect(node, 'a', edit.addNode('Log'), 'exec'); expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b']); edit.connect(node, 'b', edit.addNode('Log'), 'exec'); expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b', 'c']); }); it('shrinks again when the bottom edge goes, and never below the last connected one', () => { const fixture = sequenceFixture(); const { stack, behavior, edit } = fixture; const node = edit.addNode('Sequence'); edit.wire(['a', 'b'].map((port) => [node, port, edit.addNode('Log'), 'exec'] as const)); expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b', 'c']); stack.undo(); expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b']); stack.undo(); expect(portsOf(behavior, node, fixture)).toEqual(['a']); }); it('keeps a gap rather than dropping an edge: the last connected port sets the size', () => { const fixture = sequenceFixture(); const { behavior, edit } = fixture; const node = edit.addNode('Sequence'); edit.wire(['a', 'b', 'c'].map((port) => [node, port, edit.addNode('Log'), 'exec'] as const)); const middle = behavior.graph.edges.find((e) => e.from.port === 'b')!; fixture.stack.execute(new DisconnectCommand(behavior.id, middle.id)); expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b', 'c', 'd']); expect(edgeKeys(behavior)).toEqual(['a→exec', 'c→exec']); }); it('runs the outputs it has, in order', () => { const fixture = sequenceFixture(); const { behavior, edit } = fixture; const node = edit.addNode('Sequence'); const onStart = edit.addNode('OnStart'); edit.connect(onStart, 'exec', node, 'exec'); for (const port of ['a', 'b']) edit.connect(node, port, edit.addNode('Log', { message: port.toUpperCase() }), 'exec'); const ctx = fakeContext(); fire(new GraphInterpreter(behavior.graph, graphLookup(fixture.project)), 'OnStart', ctx); expect(ctx.logs).toEqual(['A', 'B']); }); }); /** The point of a struct: a bundle crosses a behavior boundary on ONE edge. */ describe('a struct travelling through a nested behavior', () => { it('goes in as one port and comes back out as its parts', () => { const fixture = makeProject(); const callee = makeBehavior(fixture, 'Sum piece'); const inner = graphEditor(fixture, callee); const start = inner.addNode(BEHAVIOR_START); const end = inner.addNode(BEHAVIOR_END); fixture.stack.execute(new AddPortCommand(callee.id, start.id, { key: 'piece', kind: 'struct' })); fixture.stack.execute(new AddPortCommand(callee.id, end.id, { key: 'total', kind: 'number' })); const expand = inner.addNode('ExpandStruct'); for (const key of ['x', 'y']) fixture.stack.execute(new AddPortCommand(callee.id, expand.id, { key, kind: 'number' })); const add = inner.addNode('Add'); inner.wire([ [start, 'exec', end, 'exec'], [start, 'piece', expand, 'value'], [expand, 'x', add, 'a'], [expand, 'y', add, 'b'], [add, 'result', end, 'total'], ]); const caller = makeBehavior(fixture, 'Caller', ['result']); const outer = graphEditor(fixture, caller); const pack = outer.addNode('PackStruct', { x: 30, y: 12 }); for (const key of ['x', 'y']) fixture.stack.execute(new AddPortCommand(caller.id, pack.id, { key, kind: 'number' })); const onStart = outer.addNode('OnStart'); const call = outer.addNode('RunBehavior', { behavior: callee.id }); const set = outer.variables(['result'], { write: true }); outer.wire([ [onStart, 'exec', call, 'exec'], [pack, 'value', call, 'piece'], [call, 'exec', set, 'exec'], [call, 'total', set, 'result'], ]); // One edge into the call, where two numbers would need two. expect(caller.graph.edges.filter((e) => e.to.node === call.id && e.to.port !== 'exec')).toHaveLength(1); const ctx = fakeContext({ variables: { result: 0 } }); fire(new GraphInterpreter(caller.graph, graphLookup(fixture.project)), 'OnStart', ctx); expect(ctx.variables.result).toBe(42); }); }); describe('a struct input', () => { it('is offered no inline field, because a bundle cannot be typed on one line', () => { const def = requireNodeDef('ExpandStruct'); expect(def.inputs.map((p) => p.type)).toEqual(['struct']); expect(inlineInputs(def)).toEqual([]); }); }); describe('a Run behavior node whose behavior changes', () => { it('drops the edges of the ports the new behavior does not have', () => { const fixture = makeProject(); const called = makeBehavior(fixture, 'Called'); const other = makeBehavior(fixture, 'Other'); const start = graphEditor(fixture, called).addNode(BEHAVIOR_START); fixture.stack.execute(new AddPortCommand(called.id, start.id, { key: 'power', kind: 'number' })); const caller = makeBehavior(fixture, 'Caller'); const edit = graphEditor(fixture, caller); const call = edit.addNode('RunBehavior', { behavior: called.id }); const number = edit.addNode('Number', { value: 3 }); edit.connect(number, 'value', call, 'power'); expect(edgeKeys(caller)).toEqual(['value→power']); fixture.stack.execute(new SetNodeParamCommand(caller.id, call.id, 'behavior', other.id)); expect(edgeKeys(caller)).toEqual([]); fixture.stack.undo(); expect(edgeKeys(caller)).toEqual(['value→power']); expect(start.ports).toEqual([{ key: 'power', kind: 'number' }]); }); });