A machine that arrives as a value can still be commanded, so a name may name its state (#58)
game-maker / suite (push) Successful in 6m12s
game-maker / suite (push) Successful in 6m12s
Set state and State branch derived their ports from the graph, so a machine that came out of a variable or a list had no ports at all: nothing to set, nowhere to branch. It could be passed around but not operated. stateByName() resolves id first, then name; a name two states carry answers with nothing, the same rule transitionTo already follows for two transitions of one name — without something in the graph that decides between them, any pick would be a guess. Set state by name takes the machine and a text, and has a second exec output not found, so a typo is not swallowed in silence. State branch gains a derived else that fires whenever no state output matches — no state set, a state of another machine, or a machine that arrives as a value and therefore has no state outputs at all. A flow that enters State branch now always leaves it somewhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7e5003b590
commit
1135205d69
7 files changed
+218
-14
No files matched your search
@@ -122,7 +122,12 @@ const CASES: Record<string, Case[]> = {
|
||||
},
|
||||
],
|
||||
StateBranch: [
|
||||
{ name: 'in no state yet, the flow ends', inputs: { machine: 'm' }, derivedOutputs: ['s1'], next: null },
|
||||
{
|
||||
name: 'in no state yet, the flow leaves along else',
|
||||
inputs: { machine: 'm' },
|
||||
derivedOutputs: ['s1'],
|
||||
next: 'else',
|
||||
},
|
||||
{
|
||||
name: 'follows the output of the current state',
|
||||
inputs: { machine: 'm' },
|
||||
@@ -131,11 +136,58 @@ const CASES: Record<string, Case[]> = {
|
||||
next: 's1',
|
||||
},
|
||||
{
|
||||
name: 'a state that is not among its outputs (another machine, a deleted state) ends the flow',
|
||||
name: 'a state that is not among its outputs (another machine, a deleted state) takes else',
|
||||
inputs: { machine: 'm' },
|
||||
context: { states: { m: 'gone' } },
|
||||
derivedOutputs: ['s1'],
|
||||
next: null,
|
||||
next: 'else',
|
||||
},
|
||||
{
|
||||
// The point of #58: a machine arriving as a value has no state outputs at all.
|
||||
name: 'a machine the graph cannot name has no state outputs, so else is the whole node',
|
||||
inputs: { machine: 'm' },
|
||||
context: { states: { m: 's1' } },
|
||||
next: 'else',
|
||||
},
|
||||
],
|
||||
SetStateByName: [
|
||||
{
|
||||
name: 'moves the machine to the state of that name',
|
||||
inputs: { machine: 'm', state: 'Open' },
|
||||
lookup: machineWithStates([{ id: 's1', name: 'Open' }]),
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(ctx.states).toEqual({ m: 's1' }),
|
||||
},
|
||||
{
|
||||
name: 'a state id names the same state, so a rename cannot break a graph that used the id',
|
||||
inputs: { machine: 'm', state: 's1' },
|
||||
lookup: machineWithStates([{ id: 's1', name: 'Open' }]),
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(ctx.states).toEqual({ m: 's1' }),
|
||||
},
|
||||
{
|
||||
name: 'a name no state answers to moves nothing and takes "not found"',
|
||||
inputs: { machine: 'm', state: 'Ajar' },
|
||||
lookup: machineWithStates([{ id: 's1', name: 'Open' }]),
|
||||
next: 'notFound',
|
||||
after: (ctx) => expect(ctx.states).toEqual({}),
|
||||
},
|
||||
{
|
||||
name: 'a name two states share is no answer: the graph never picked between them',
|
||||
inputs: { machine: 'm', state: 'Open' },
|
||||
lookup: machineWithStates([
|
||||
{ id: 's1', name: 'Open' },
|
||||
{ id: 's2', name: 'Open' },
|
||||
]),
|
||||
next: 'notFound',
|
||||
after: (ctx) => expect(ctx.states).toEqual({}),
|
||||
},
|
||||
{
|
||||
name: 'without a machine there is nothing to set',
|
||||
inputs: { machine: '', state: 'Open' },
|
||||
lookup: machineWithStates([{ id: 's1', name: 'Open' }]),
|
||||
next: 'notFound',
|
||||
after: (ctx) => expect(ctx.states).toEqual({}),
|
||||
},
|
||||
],
|
||||
SetState: [
|
||||
|
||||
@@ -169,7 +169,8 @@ describe('the state ports of State branch and Set state', () => {
|
||||
const branch = edit.addNode('StateBranch', { [MACHINE_PORT]: machine.id });
|
||||
const set = edit.addNode('SetState', { [MACHINE_PORT]: machine.id });
|
||||
const expected = [`${machine.ids[0]}:Open`, `${machine.ids[1]}:Closed`];
|
||||
expect(ports(branch, 'outputs')).toEqual(expected);
|
||||
// The branch carries one more: the way out for everything no state covers.
|
||||
expect(ports(branch, 'outputs')).toEqual([...expected, 'else:else']);
|
||||
expect(ports(set, 'inputs')).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -177,9 +178,13 @@ describe('the state ports of State branch and Set state', () => {
|
||||
const { machine, edit, ports } = setup();
|
||||
const literal = edit.addNode('StateMachine', { value: machine.id });
|
||||
const branch = edit.addNode('StateBranch');
|
||||
expect(ports(branch, 'outputs')).toEqual([]);
|
||||
expect(ports(branch, 'outputs')).toEqual(['else:else']);
|
||||
edit.connect(literal, 'value', branch, MACHINE_PORT);
|
||||
expect(ports(branch, 'outputs')).toEqual([`${machine.ids[0]}:Open`, `${machine.ids[1]}:Closed`]);
|
||||
expect(ports(branch, 'outputs')).toEqual([
|
||||
`${machine.ids[0]}:Open`,
|
||||
`${machine.ids[1]}:Closed`,
|
||||
'else:else',
|
||||
]);
|
||||
});
|
||||
|
||||
it('cannot follow a variable: a port is derived from the graph, not from a running value', () => {
|
||||
@@ -188,12 +193,17 @@ describe('the state ports of State branch and Set state', () => {
|
||||
findAsset(f.project, behavior.id)!.props.doors = machine.id;
|
||||
const variables = edit.variables(['doors']);
|
||||
const branch = edit.addNode('StateBranch', { [MACHINE_PORT]: machine.id });
|
||||
const set = edit.addNode('SetState', { [MACHINE_PORT]: machine.id });
|
||||
edit.connect(variables, 'doors', branch, MACHINE_PORT);
|
||||
expect(ports(branch, 'outputs')).toEqual([]);
|
||||
edit.connect(variables, 'doors', set, MACHINE_PORT);
|
||||
// `else` is all that is left of the branch, and Set state has nothing at all – which is why
|
||||
// "Set state by name" exists: it names the state instead of wiring it.
|
||||
expect(ports(branch, 'outputs')).toEqual(['else:else']);
|
||||
expect(ports(set, 'inputs')).toEqual([]);
|
||||
// Only the State machine literal is followed – a text holding the very same id is not.
|
||||
const text = edit.addNode('String', { value: machine.id });
|
||||
edit.connect(text, 'value', branch, MACHINE_PORT);
|
||||
expect(ports(branch, 'outputs')).toEqual([]);
|
||||
expect(ports(branch, 'outputs')).toEqual(['else:else']);
|
||||
});
|
||||
|
||||
it('follow a rename, and lose the edges of a state that is deleted', () => {
|
||||
@@ -291,6 +301,42 @@ describe('a running state machine', () => {
|
||||
expect(ctx.logs).toEqual([]);
|
||||
});
|
||||
|
||||
it('is driven by name where the machine arrives as a value, and branches through else', () => {
|
||||
const f = makeProject();
|
||||
const machine = makeMachine(f);
|
||||
const behavior = makeBehavior(f, 'Door');
|
||||
f.stack.execute(new AddVariableCommand(behavior.id, 'doors', 'statemachine'));
|
||||
findAsset(f.project, behavior.id)!.props.doors = machine.id;
|
||||
const edit = graphEditor(f, behavior);
|
||||
const variables = edit.variables(['doors']);
|
||||
const start = edit.addNode('OnStart');
|
||||
const set = edit.addNode('SetStateByName', { state: 'Closed' });
|
||||
const missing = edit.addNode('SetStateByName', { state: 'Ajar' });
|
||||
const logMissing = edit.addNode('Log', { message: 'no such state' });
|
||||
const update = edit.addNode('OnUpdate');
|
||||
const branch = edit.addNode('StateBranch');
|
||||
const logElse = edit.addNode('Log', { message: 'unknown to the graph' });
|
||||
edit.wire([
|
||||
[variables, 'doors', set, MACHINE_PORT],
|
||||
[variables, 'doors', missing, MACHINE_PORT],
|
||||
[variables, 'doors', branch, MACHINE_PORT],
|
||||
[start, 'exec', set, 'exec'],
|
||||
[set, 'exec', missing, 'exec'],
|
||||
[missing, 'notFound', logMissing, 'exec'],
|
||||
[update, 'exec', branch, 'exec'],
|
||||
[branch, 'else', logElse, 'exec'],
|
||||
]);
|
||||
const ctx = fakeContext({ variables: { doors: machine.id } });
|
||||
const interpreter = new GraphInterpreter(behavior.graph, graphLookup(f.project));
|
||||
interpreter.fire('OnStart', ctx);
|
||||
// The name moved the machine; the one nothing answers to took "not found" instead of ending.
|
||||
expect(ctx.states).toEqual({ [machine.id]: machine.ids[1] });
|
||||
expect(ctx.logs).toEqual(['no such state']);
|
||||
// The branch has no state outputs at all here, so even a state that is set leaves through else.
|
||||
interpreter.fire('OnUpdate', ctx);
|
||||
expect(ctx.logs).toEqual(['no such state', 'unknown to the graph']);
|
||||
});
|
||||
|
||||
it('refuses a connection to a state port the machine does not have', () => {
|
||||
const f = makeProject();
|
||||
const machine = makeMachine(f);
|
||||
|
||||
Reference in new issue
Block a user