Skip to content

Commit 69ab63b

Browse files
committed
fix cascade targets to only have terminal nodes
1 parent 28b7366 commit 69ab63b

File tree

2 files changed

+63
-4
lines changed

2 files changed

+63
-4
lines changed

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,65 @@ describe('EdgeManager', () => {
16481648
// sentinel_end should be ready because all paths to it are deactivated
16491649
expect(ready2).toContain(sentinelEndId)
16501650
})
1651+
1652+
it('should NOT execute intermediate nodes in long cascade chains (2+ hops)', () => {
1653+
// Regression test: When condition hits dead-end with 2+ intermediate nodes,
1654+
// only sentinel_end should be ready, NOT the intermediate nodes.
1655+
//
1656+
// Structure: sentinel_start → condition → funcA → funcB → sentinel_end
1657+
// When condition hits dead-end, funcA and funcB should NOT execute.
1658+
1659+
const sentinelStartId = 'sentinel-start'
1660+
const sentinelEndId = 'sentinel-end'
1661+
const conditionId = 'condition'
1662+
const funcAId = 'funcA'
1663+
const funcBId = 'funcB'
1664+
1665+
const sentinelStartNode = createMockNode(sentinelStartId, [{ target: conditionId }])
1666+
const conditionNode = createMockNode(
1667+
conditionId,
1668+
[{ target: funcAId, sourceHandle: 'condition-if' }],
1669+
[sentinelStartId]
1670+
)
1671+
const funcANode = createMockNode(funcAId, [{ target: funcBId }], [conditionId])
1672+
const funcBNode = createMockNode(funcBId, [{ target: sentinelEndId }], [funcAId])
1673+
const sentinelEndNode = createMockNode(
1674+
sentinelEndId,
1675+
[
1676+
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
1677+
{ target: 'after-loop', sourceHandle: 'loop_exit' },
1678+
],
1679+
[funcBId]
1680+
)
1681+
const afterLoopNode = createMockNode('after-loop', [], [sentinelEndId])
1682+
1683+
const nodes = new Map<string, DAGNode>([
1684+
[sentinelStartId, sentinelStartNode],
1685+
[conditionId, conditionNode],
1686+
[funcAId, funcANode],
1687+
[funcBId, funcBNode],
1688+
[sentinelEndId, sentinelEndNode],
1689+
['after-loop', afterLoopNode],
1690+
])
1691+
1692+
const dag = createMockDAG(nodes)
1693+
const edgeManager = new EdgeManager(dag)
1694+
1695+
// Simulate execution up to condition
1696+
conditionNode.incomingEdges.clear()
1697+
1698+
// Condition hits dead-end (else branch with no edge)
1699+
const ready = edgeManager.processOutgoingEdges(conditionNode, {
1700+
selectedOption: null,
1701+
})
1702+
1703+
// Only sentinel_end should be ready
1704+
expect(ready).toContain(sentinelEndId)
1705+
1706+
// Intermediate nodes should NOT be in readyNodes
1707+
expect(ready).not.toContain(funcAId)
1708+
expect(ready).not.toContain(funcBId)
1709+
})
16511710
})
16521711

16531712
describe('Condition inside parallel - parallel control edges should not be cascade-deactivated', () => {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,13 @@ export class EdgeManager {
203203

204204
this.deactivatedEdges.add(edgeKey)
205205

206-
if (isCascade) {
207-
cascadeTargets?.add(targetId)
208-
}
209-
210206
const targetNode = this.dag.nodes.get(targetId)
211207
if (!targetNode) return
212208

209+
if (isCascade && this.isTerminalControlNode(targetId)) {
210+
cascadeTargets?.add(targetId)
211+
}
212+
213213
if (this.hasActiveIncomingEdges(targetNode, edgeKey)) {
214214
return
215215
}

0 commit comments

Comments
 (0)