Let a nested behavior without a start node run from On start (#230)

A behavior built as a plain behavior, with On start and no Behavior
start, did nothing when another behavior called it through Run
behavior. The node only showed a warning. Such a behavior should run
as a call too, without the author having to rebuild its entry.

- New callEntries() in nested.ts returns the Behavior start node or,
  when there is none, every On start node. callProblem uses it too.
- GraphInterpreter.call() follows all entries one after the other,
  in graph order, in the same frame and with the same variables.
- Behavior end still returns what reaches it. A call through On start
  has no inputs, because only the start node declares them.
- It is a fallback only. With a Behavior start present, On start
  nodes stay out of the call.
- The node warning now reads "No start or On start node - does
  nothing" and only shows when neither exists.
- Keep and local state machines behave as before.

Tests:
- ./check.sh: 163 files, 2396 tests, CHECK EXIT 0 (before 2392).
- 4 new unit tests in tests/core/nested-behavior.test.ts: the flow
  after On start runs, the return value arrives, several On start
  nodes run in graph order, a start node keeps On start out.
- e2e 19: ALL PASSED, 35 checks. Only the expected warning text
  changed, the check sum stays at 1385.
- Counter-checks: without the fallback 3 tests fail. With only the
  first On start exactly the order test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-25 16:13:14 +02:00
1 parent 454d202109
commit 21d7149756
5 files changed
+117 -33

No files matched your search

+73 -1
View File
@@ -198,7 +198,7 @@ describe('a call that cannot do anything says so', () => {
call.params[BEHAVIOR_PARAM] = 'gone';
expect(problem()).toBe('That behavior is gone');
call.params[BEHAVIOR_PARAM] = makeBehavior(fixture, 'Empty').id;
expect(problem()).toBe('No start node – does nothing');
expect(problem()).toBe('No start or On start node – does nothing');
call.params[BEHAVIOR_PARAM] = callee.id;
expect(problem()).toBe(null);
// Only a call can be dead this way; no other node type may be marked.
@@ -245,6 +245,78 @@ describe('a call that cannot do anything says so', () => {
});
});
describe('a called behavior without a start node begins at On start (#230)', () => {
/** A caller whose On start runs `callee` and logs "after" once the call is done. */
function logCaller(fixture: ProjectFixture, callee: BehaviorAsset) {
const caller = makeBehavior(fixture, 'Caller');
const edit = graphEditor(fixture, caller);
const onStart = edit.addNode('OnStart');
const call = edit.addNode(RUN_BEHAVIOR, { [BEHAVIOR_PARAM]: callee.id });
edit.wire([
[onStart, 'exec', call, 'exec'],
[call, 'exec', edit.addNode('Log', { message: 'after' }), 'exec'],
]);
return { caller, call };
}
it('runs the flow behind its On start, and the call is not marked dead', () => {
const fixture = makeProject();
const callee = makeBehavior(fixture, 'Plain');
const cEdit = graphEditor(fixture, callee);
cEdit.connect(cEdit.addNode('OnStart'), 'exec', cEdit.addNode('Log', { message: 'sub' }), 'exec');
const { caller, call } = logCaller(fixture, callee);
expect(callProblem(call, graphLookup(fixture.project), caller.id)).toBe(null);
expect(runOnStart(fixture, caller).ctx.logs).toEqual(['sub', 'after']);
});
it('still hands back what its end node was given', () => {
const fixture = makeProject();
const callee = makeBehavior(fixture, 'Seven');
const cEdit = graphEditor(fixture, callee);
const onStart = cEdit.addNode('OnStart');
const end = cEdit.addNode(BEHAVIOR_END);
fixture.stack.execute(new AddPortCommand(callee.id, end.id, { key: 'out1', kind: 'number' }));
cEdit.wire([
[onStart, 'exec', end, 'exec'],
[cEdit.addNode('Number', { value: 7 }), 'value', end, 'out1'],
]);
const caller = makeBehavior(fixture, 'Caller', ['result']);
const edit = graphEditor(fixture, caller);
const call = edit.addNode(RUN_BEHAVIOR, { [BEHAVIOR_PARAM]: callee.id });
const set = edit.variables(['result'], { write: true });
edit.wire([
[edit.addNode('OnStart'), 'exec', call, 'exec'],
[call, 'exec', set, 'exec'],
[call, 'out1', set, 'result'],
]);
expect(runOnStart(fixture, caller, { result: 0 }).ctx.variables.result).toBe(7);
});
it('runs every On start, in graph order, as the behavior would on its own', () => {
const fixture = makeProject();
const callee = makeBehavior(fixture, 'Twice');
const cEdit = graphEditor(fixture, callee);
cEdit.connect(cEdit.addNode('OnStart'), 'exec', cEdit.addNode('Log', { message: 'one' }), 'exec');
cEdit.connect(cEdit.addNode('OnStart'), 'exec', cEdit.addNode('Log', { message: 'two' }), 'exec');
const { caller } = logCaller(fixture, callee);
expect(runOnStart(fixture, caller).ctx.logs).toEqual(['one', 'two', 'after']);
});
it('is only a fallback: with a start node, its On start stays out of the call', () => {
const fixture = makeProject();
const callee = makeBehavior(fixture, 'Both');
const cEdit = graphEditor(fixture, callee);
cEdit.connect(cEdit.addNode(BEHAVIOR_START), 'exec', cEdit.addNode('Log', { message: 'start' }), 'exec');
cEdit.connect(cEdit.addNode('OnStart'), 'exec', cEdit.addNode('Log', { message: 'on start' }), 'exec');
const { caller } = logCaller(fixture, callee);
expect(runOnStart(fixture, caller).ctx.logs).toEqual(['start', 'after']);
});
});
describe('declared ports', () => {
it('renaming one rewrites the edges in this graph and in every calling one', () => {
const fixture = makeProject();