Skip to content

Commit 9f1f77e

Browse files
committed
fix(executor): loop sentinel-end wrongly queued
1 parent 1d4d61a commit 9f1f77e

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

apps/sim/executor/execution/edge-manager.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,6 +2478,9 @@ describe('EdgeManager', () => {
24782478
expect(readyNodes).toContain(otherBranchId)
24792479
expect(readyNodes).not.toContain(sentinelStartId)
24802480

2481+
// sentinel_end should NOT be ready - it's on a fully deactivated path
2482+
expect(readyNodes).not.toContain(sentinelEndId)
2483+
24812484
// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
24822485
expect(readyNodes).not.toContain(afterLoopId)
24832486

@@ -2545,6 +2548,84 @@ describe('EdgeManager', () => {
25452548
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
25462549
})
25472550

2551+
it('should not queue loop sentinel-end when upstream condition deactivates entire loop branch', () => {
2552+
// Regression test for: upstream condition → (if) → ... many blocks ... → sentinel_start → body → sentinel_end
2553+
// → (else) → exit_block
2554+
// When condition takes "else", the deep cascade deactivation should NOT queue sentinel_end.
2555+
// Previously, sentinel_end was flagged as a cascadeTarget (terminal control node) and
2556+
// spuriously queued, causing it to attempt loop scope initialization and fail.
2557+
2558+
const conditionId = 'condition'
2559+
const intermediateId = 'intermediate'
2560+
const sentinelStartId = 'sentinel-start'
2561+
const loopBodyId = 'loop-body'
2562+
const sentinelEndId = 'sentinel-end'
2563+
const afterLoopId = 'after-loop'
2564+
const exitBlockId = 'exit-block'
2565+
2566+
const conditionNode = createMockNode(conditionId, [
2567+
{ target: intermediateId, sourceHandle: 'condition-if' },
2568+
{ target: exitBlockId, sourceHandle: 'condition-else' },
2569+
])
2570+
2571+
const intermediateNode = createMockNode(
2572+
intermediateId,
2573+
[{ target: sentinelStartId }],
2574+
[conditionId]
2575+
)
2576+
2577+
const sentinelStartNode = createMockNode(
2578+
sentinelStartId,
2579+
[{ target: loopBodyId }],
2580+
[intermediateId]
2581+
)
2582+
2583+
const loopBodyNode = createMockNode(
2584+
loopBodyId,
2585+
[{ target: sentinelEndId }],
2586+
[sentinelStartId]
2587+
)
2588+
2589+
const sentinelEndNode = createMockNode(
2590+
sentinelEndId,
2591+
[
2592+
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
2593+
{ target: afterLoopId, sourceHandle: 'loop_exit' },
2594+
],
2595+
[loopBodyId]
2596+
)
2597+
2598+
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
2599+
const exitBlockNode = createMockNode(exitBlockId, [], [conditionId])
2600+
2601+
const nodes = new Map<string, DAGNode>([
2602+
[conditionId, conditionNode],
2603+
[intermediateId, intermediateNode],
2604+
[sentinelStartId, sentinelStartNode],
2605+
[loopBodyId, loopBodyNode],
2606+
[sentinelEndId, sentinelEndNode],
2607+
[afterLoopId, afterLoopNode],
2608+
[exitBlockId, exitBlockNode],
2609+
])
2610+
2611+
const dag = createMockDAG(nodes)
2612+
const edgeManager = new EdgeManager(dag)
2613+
2614+
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, {
2615+
selectedOption: 'else',
2616+
})
2617+
2618+
// Only exitBlock should be ready
2619+
expect(readyNodes).toContain(exitBlockId)
2620+
2621+
// Nothing on the deactivated path should be queued
2622+
expect(readyNodes).not.toContain(intermediateId)
2623+
expect(readyNodes).not.toContain(sentinelStartId)
2624+
expect(readyNodes).not.toContain(loopBodyId)
2625+
expect(readyNodes).not.toContain(sentinelEndId)
2626+
expect(readyNodes).not.toContain(afterLoopId)
2627+
})
2628+
25482629
it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
25492630
// When a loop actually executes and exits normally, after_loop should become ready
25502631
const sentinelStartId = 'sentinel-start'

apps/sim/executor/execution/edge-manager.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ export class EdgeManager {
7171

7272
for (const targetId of cascadeTargets) {
7373
if (!readyNodes.includes(targetId) && !activatedTargets.includes(targetId)) {
74-
if (this.isTargetReady(targetId)) {
74+
// Only queue cascade terminal control nodes when ALL outgoing edges from the
75+
// current node were deactivated (dead-end scenario). When some edges are
76+
// activated, terminal control nodes on deactivated branches should NOT be
77+
// queued - they will be reached through the normal activated path's completion.
78+
// This prevents loop/parallel sentinels on fully deactivated paths (e.g., an
79+
// upstream condition took a different branch) from being spuriously executed.
80+
if (activatedTargets.length === 0 && this.isTargetReady(targetId)) {
7581
readyNodes.push(targetId)
7682
}
7783
}

0 commit comments

Comments
 (0)