Let a Start input or a variable name the state machine it carries
A sub-behavior that gets its machine through a Start input had no state ports on Is in state, Set state and the other state nodes. Their ports came only from an inline pick or a State machine literal, so a machine handed in from outside left them empty. - A statemachine port of a Start node can now name a machine (DeclaredPort.machine). The inspector shows the pick next to the port name. SetPortMachineCommand sets it with undo. - machineIdOf follows an edge from such a port to that machine, so the state nodes it feeds offer its states. - A Run behavior node has the machine picked in advance on that input. - A different pick there is allowed, but the node warns, because the called graph asks for the states of its own machine. - A statemachine variable of a Variables node can name a machine the same way, picked next to the variable's row. - The inspector signature of a node now includes the picked machine. Before, the pick changed the ports but the dropdown kept its text until the node was selected again. - A local machine handed in still starts in no state every call. That rule from #229 is unchanged. Tests: - tests/core/start-machine.test.ts (7): ports with and without a machine and undo, save and load, the preset value moving a shared machine through the call, the warning, a port of another kind, the inspector signature, a Variables node. - Counter checks: without the new branch in machineIdOf 2 tests fail, without the machine in the signature 1 test fails. - Gate on melektaus-atx: 239 files, 3599 tests, CHECK EXIT 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f12a77aea
commit
d80d5cce26
8 files changed
+284
-10
No files matched your search
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* A `statemachine` input of a behavior's start node can say which machine it carries. The state
|
||||
* nodes it feeds then offer that machine's states, and a node running the behavior has it picked
|
||||
* in advance – a different pick there is a problem on that node. A variable a Variables node reads
|
||||
* can say the same.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { AddNodeCommand } from '../../src/core/commands/graphCommands';
|
||||
import { AddPortCommand, SetPortMachineCommand } from '../../src/core/commands/portCommands';
|
||||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import {
|
||||
BEHAVIOR_PARAM,
|
||||
BEHAVIOR_START,
|
||||
callProblem,
|
||||
GraphInterpreter,
|
||||
inlineValue,
|
||||
MACHINE_PORT,
|
||||
resolveDef,
|
||||
RUN_BEHAVIOR,
|
||||
STATE,
|
||||
STATE_NAME_PARAM,
|
||||
} from '../../src/core/graph';
|
||||
import { graphLookup } from '../../src/core/model/graphLookup';
|
||||
import { findAssetOf } from '../../src/core/model/Project';
|
||||
import { nodeSignature } from '../../src/editor/inspector/nodeSection';
|
||||
import {
|
||||
fakeContext,
|
||||
fire,
|
||||
graphEditor,
|
||||
makeBehavior,
|
||||
makeProject,
|
||||
type ProjectFixture,
|
||||
roundTrip,
|
||||
} from '../helpers';
|
||||
|
||||
function makeMachine(f: ProjectFixture, name: string, states: readonly string[]) {
|
||||
const create = new CreateAssetCommand('statemachine', name);
|
||||
f.stack.execute(create);
|
||||
const ids = states.map((state) => {
|
||||
const add = new AddNodeCommand(create.asset.id, STATE, 0, 0, { [STATE_NAME_PARAM]: state });
|
||||
f.stack.execute(add);
|
||||
return add.node.id;
|
||||
});
|
||||
return { id: create.asset.id, ids };
|
||||
}
|
||||
|
||||
/** "Inner": its start hands `machine` to an `Is in state` and to a `Set state`. */
|
||||
function build(f: ProjectFixture) {
|
||||
const doors = makeMachine(f, 'Doors', ['Open', 'Closed']);
|
||||
const lights = makeMachine(f, 'Lights', ['On']);
|
||||
const inner = makeBehavior(f, 'Inner');
|
||||
const edit = graphEditor(f, inner);
|
||||
const start = edit.addNode(BEHAVIOR_START);
|
||||
f.stack.execute(new AddPortCommand(inner.id, start.id, { key: 'machine', kind: 'statemachine' }));
|
||||
const isIn = edit.addNode('IsInState');
|
||||
const set = edit.addNode('SetState');
|
||||
edit.wire([
|
||||
[start, 'machine', isIn, MACHINE_PORT],
|
||||
[start, 'machine', set, MACHINE_PORT],
|
||||
]);
|
||||
const ports = (node = isIn) =>
|
||||
resolveDef(node, inner.graph, graphLookup(f.project))!.outputs.map((p) => p.label);
|
||||
return { doors, lights, inner, start, isIn, set, ports };
|
||||
}
|
||||
|
||||
describe('a machine carried by a start node input', () => {
|
||||
it('gives the nodes it feeds the states of that machine, and none without one', () => {
|
||||
const f = makeProject();
|
||||
const { doors, inner, start, ports } = build(f);
|
||||
expect(ports()).toEqual([]);
|
||||
f.stack.execute(new SetPortMachineCommand(inner.id, start.id, 'machine', doors.id));
|
||||
expect(ports()).toEqual(['Open', 'Closed']);
|
||||
f.stack.undo();
|
||||
expect(ports()).toEqual([]);
|
||||
expect(start.ports).toEqual([{ key: 'machine', kind: 'statemachine' }]);
|
||||
});
|
||||
|
||||
it('survives a save and load', () => {
|
||||
const f = makeProject();
|
||||
const { doors, inner, start } = build(f);
|
||||
f.stack.execute(new SetPortMachineCommand(inner.id, start.id, 'machine', doors.id));
|
||||
const loaded = findAssetOf(roundTrip(f.project), inner.id, 'behavior')!;
|
||||
const port = loaded.graph.nodes.find((n) => n.id === start.id)!.ports;
|
||||
expect(port).toEqual([{ key: 'machine', kind: 'statemachine', machine: doors.id }]);
|
||||
});
|
||||
|
||||
it('is picked in advance on a node running the behavior, and moves that machine', () => {
|
||||
const f = makeProject();
|
||||
const { doors, inner, start, set } = build(f);
|
||||
f.stack.execute(new SetPortMachineCommand(inner.id, start.id, 'machine', doors.id));
|
||||
graphEditor(f, inner).connect(start, 'exec', set, doors.ids[1]!);
|
||||
const outer = makeBehavior(f, 'Outer');
|
||||
const edit = graphEditor(f, outer);
|
||||
const call = edit.addNode(RUN_BEHAVIOR, { [BEHAVIOR_PARAM]: inner.id });
|
||||
edit.connect(edit.addNode('OnStart'), 'exec', call, 'exec');
|
||||
const lookup = graphLookup(f.project);
|
||||
const input = resolveDef(call, outer.graph, lookup)!.inputs.find((p) => p.key === 'machine')!;
|
||||
expect(inlineValue(call, input)).toBe(doors.id);
|
||||
// Shared, so the move outlives the call and can be read here.
|
||||
const ctx = fakeContext({ shared: { variables: {}, states: { [doors.id]: '' } } });
|
||||
fire(new GraphInterpreter(outer.graph, lookup), 'OnStart', ctx);
|
||||
expect(ctx.shared.states[doors.id]).toBe(doors.ids[1]);
|
||||
});
|
||||
|
||||
it('warns on a node that picks a different machine, not on one picking the same', () => {
|
||||
const f = makeProject();
|
||||
const { doors, lights, inner, start } = build(f);
|
||||
f.stack.execute(new SetPortMachineCommand(inner.id, start.id, 'machine', doors.id));
|
||||
const outer = makeBehavior(f, 'Outer');
|
||||
const edit = graphEditor(f, outer);
|
||||
const call = edit.addNode(RUN_BEHAVIOR, { [BEHAVIOR_PARAM]: inner.id });
|
||||
const lookup = () => graphLookup(f.project);
|
||||
expect(callProblem(call, lookup(), outer.id)).toBeNull();
|
||||
call.params.machine = lights.id;
|
||||
expect(callProblem(call, lookup(), outer.id)).toBe('"machine" expects "Doors"');
|
||||
call.params.machine = doors.id;
|
||||
expect(callProblem(call, lookup(), outer.id)).toBeNull();
|
||||
});
|
||||
|
||||
it('is refused on a port of another kind', () => {
|
||||
const f = makeProject();
|
||||
const { doors, inner, start } = build(f);
|
||||
f.stack.execute(new AddPortCommand(inner.id, start.id, { key: 'speed', kind: 'number' }));
|
||||
expect(() => f.stack.execute(new SetPortMachineCommand(inner.id, start.id, 'speed', doors.id))).toThrow(
|
||||
'"speed" is not a state machine',
|
||||
);
|
||||
});
|
||||
|
||||
it('rebuilds the inspector of the start node when it changes', () => {
|
||||
const f = makeProject();
|
||||
const { doors, lights, inner, start } = build(f);
|
||||
const before = nodeSignature(f.project, inner.id, start.id);
|
||||
f.stack.execute(new SetPortMachineCommand(inner.id, start.id, 'machine', doors.id));
|
||||
const picked = nodeSignature(f.project, inner.id, start.id);
|
||||
expect(picked).not.toBe(before);
|
||||
f.stack.execute(new SetPortMachineCommand(inner.id, start.id, 'machine', lights.id));
|
||||
expect(nodeSignature(f.project, inner.id, start.id)).not.toBe(picked);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a machine carried by a variable of a Variables node', () => {
|
||||
it('gives the nodes it feeds the states of that machine, and none without one', () => {
|
||||
const f = makeProject();
|
||||
const doors = makeMachine(f, 'Doors', ['Open', 'Closed']);
|
||||
const behavior = makeBehavior(f, 'Door');
|
||||
f.stack.execute(new AddVariableCommand(behavior.id, 'door', 'statemachine'));
|
||||
const edit = graphEditor(f, behavior);
|
||||
const read = edit.variables(['door']);
|
||||
const isIn = edit.addNode('IsInState');
|
||||
edit.connect(read, 'door', isIn, MACHINE_PORT);
|
||||
const ports = () => resolveDef(isIn, behavior.graph, graphLookup(f.project))!.outputs.map((p) => p.label);
|
||||
expect(ports()).toEqual([]);
|
||||
f.stack.execute(new SetPortMachineCommand(behavior.id, read.id, 'door', doors.id));
|
||||
expect(ports()).toEqual(['Open', 'Closed']);
|
||||
f.stack.undo();
|
||||
expect(ports()).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user