Skip to content

Commit dca0758

Browse files
authored
fix(executor): conditional deactivation for loops/parallels (#3069)
* Fix deactivation * Remove comments
1 parent ae17c90 commit dca0758

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

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

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2417,4 +2417,177 @@ describe('EdgeManager', () => {
24172417
expect(successReady).toContain(targetId)
24182418
})
24192419
})
2420+
2421+
describe('Condition with loop downstream - deactivation propagation', () => {
2422+
it('should deactivate nodes after loop when condition branch containing loop is deactivated', () => {
2423+
// Scenario: condition → (if) → sentinel_start → loopBody → sentinel_end → (loop_exit) → after_loop
2424+
// → (else) → other_branch
2425+
// When condition takes "else" path, the entire if-branch including nodes after the loop should be deactivated
2426+
const conditionId = 'condition'
2427+
const sentinelStartId = 'sentinel-start'
2428+
const loopBodyId = 'loop-body'
2429+
const sentinelEndId = 'sentinel-end'
2430+
const afterLoopId = 'after-loop'
2431+
const otherBranchId = 'other-branch'
2432+
2433+
const conditionNode = createMockNode(conditionId, [
2434+
{ target: sentinelStartId, sourceHandle: 'condition-if' },
2435+
{ target: otherBranchId, sourceHandle: 'condition-else' },
2436+
])
2437+
2438+
const sentinelStartNode = createMockNode(
2439+
sentinelStartId,
2440+
[{ target: loopBodyId }],
2441+
[conditionId]
2442+
)
2443+
2444+
const loopBodyNode = createMockNode(
2445+
loopBodyId,
2446+
[{ target: sentinelEndId }],
2447+
[sentinelStartId]
2448+
)
2449+
2450+
const sentinelEndNode = createMockNode(
2451+
sentinelEndId,
2452+
[
2453+
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
2454+
{ target: afterLoopId, sourceHandle: 'loop_exit' },
2455+
],
2456+
[loopBodyId]
2457+
)
2458+
2459+
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
2460+
const otherBranchNode = createMockNode(otherBranchId, [], [conditionId])
2461+
2462+
const nodes = new Map<string, DAGNode>([
2463+
[conditionId, conditionNode],
2464+
[sentinelStartId, sentinelStartNode],
2465+
[loopBodyId, loopBodyNode],
2466+
[sentinelEndId, sentinelEndNode],
2467+
[afterLoopId, afterLoopNode],
2468+
[otherBranchId, otherBranchNode],
2469+
])
2470+
2471+
const dag = createMockDAG(nodes)
2472+
const edgeManager = new EdgeManager(dag)
2473+
2474+
// Condition selects "else" branch, deactivating the "if" branch (which contains the loop)
2475+
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, { selectedOption: 'else' })
2476+
2477+
// Only otherBranch should be ready
2478+
expect(readyNodes).toContain(otherBranchId)
2479+
expect(readyNodes).not.toContain(sentinelStartId)
2480+
2481+
// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
2482+
expect(readyNodes).not.toContain(afterLoopId)
2483+
2484+
// Verify that countActiveIncomingEdges returns 0 for afterLoop
2485+
// (meaning the loop_exit edge was properly deactivated)
2486+
// Note: isNodeReady returns true when all edges are deactivated (no pending deps),
2487+
// but the node won't be in readyNodes since it wasn't reached via an active path
2488+
expect(edgeManager.isNodeReady(afterLoopNode)).toBe(true) // All edges deactivated = no blocking deps
2489+
})
2490+
2491+
it('should deactivate nodes after parallel when condition branch containing parallel is deactivated', () => {
2492+
// Similar scenario with parallel instead of loop
2493+
const conditionId = 'condition'
2494+
const parallelStartId = 'parallel-start'
2495+
const parallelBodyId = 'parallel-body'
2496+
const parallelEndId = 'parallel-end'
2497+
const afterParallelId = 'after-parallel'
2498+
const otherBranchId = 'other-branch'
2499+
2500+
const conditionNode = createMockNode(conditionId, [
2501+
{ target: parallelStartId, sourceHandle: 'condition-if' },
2502+
{ target: otherBranchId, sourceHandle: 'condition-else' },
2503+
])
2504+
2505+
const parallelStartNode = createMockNode(
2506+
parallelStartId,
2507+
[{ target: parallelBodyId }],
2508+
[conditionId]
2509+
)
2510+
2511+
const parallelBodyNode = createMockNode(
2512+
parallelBodyId,
2513+
[{ target: parallelEndId }],
2514+
[parallelStartId]
2515+
)
2516+
2517+
const parallelEndNode = createMockNode(
2518+
parallelEndId,
2519+
[{ target: afterParallelId, sourceHandle: 'parallel_exit' }],
2520+
[parallelBodyId]
2521+
)
2522+
2523+
const afterParallelNode = createMockNode(afterParallelId, [], [parallelEndId])
2524+
const otherBranchNode = createMockNode(otherBranchId, [], [conditionId])
2525+
2526+
const nodes = new Map<string, DAGNode>([
2527+
[conditionId, conditionNode],
2528+
[parallelStartId, parallelStartNode],
2529+
[parallelBodyId, parallelBodyNode],
2530+
[parallelEndId, parallelEndNode],
2531+
[afterParallelId, afterParallelNode],
2532+
[otherBranchId, otherBranchNode],
2533+
])
2534+
2535+
const dag = createMockDAG(nodes)
2536+
const edgeManager = new EdgeManager(dag)
2537+
2538+
// Condition selects "else" branch
2539+
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, { selectedOption: 'else' })
2540+
2541+
expect(readyNodes).toContain(otherBranchId)
2542+
expect(readyNodes).not.toContain(parallelStartId)
2543+
expect(readyNodes).not.toContain(afterParallelId)
2544+
// isNodeReady returns true when all edges are deactivated (no pending deps)
2545+
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
2546+
})
2547+
2548+
it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
2549+
// When a loop actually executes and exits normally, after_loop should become ready
2550+
const sentinelStartId = 'sentinel-start'
2551+
const loopBodyId = 'loop-body'
2552+
const sentinelEndId = 'sentinel-end'
2553+
const afterLoopId = 'after-loop'
2554+
2555+
const sentinelStartNode = createMockNode(sentinelStartId, [{ target: loopBodyId }])
2556+
2557+
const loopBodyNode = createMockNode(
2558+
loopBodyId,
2559+
[{ target: sentinelEndId }],
2560+
[sentinelStartId]
2561+
)
2562+
2563+
const sentinelEndNode = createMockNode(
2564+
sentinelEndId,
2565+
[
2566+
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
2567+
{ target: afterLoopId, sourceHandle: 'loop_exit' },
2568+
],
2569+
[loopBodyId]
2570+
)
2571+
2572+
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
2573+
2574+
const nodes = new Map<string, DAGNode>([
2575+
[sentinelStartId, sentinelStartNode],
2576+
[loopBodyId, loopBodyNode],
2577+
[sentinelEndId, sentinelEndNode],
2578+
[afterLoopId, afterLoopNode],
2579+
])
2580+
2581+
const dag = createMockDAG(nodes)
2582+
const edgeManager = new EdgeManager(dag)
2583+
2584+
// Simulate sentinel_end completing with loop_exit (loop is done)
2585+
const readyNodes = edgeManager.processOutgoingEdges(sentinelEndNode, {
2586+
selectedRoute: 'loop_exit',
2587+
})
2588+
2589+
// afterLoop should be ready
2590+
expect(readyNodes).toContain(afterLoopId)
2591+
})
2592+
})
24202593
})

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export class EdgeManager {
243243
}
244244

245245
for (const [, outgoingEdge] of targetNode.outgoingEdges) {
246-
if (!this.isControlEdge(outgoingEdge.sourceHandle)) {
246+
if (!this.isBackwardsEdge(outgoingEdge.sourceHandle)) {
247247
this.deactivateEdgeAndDescendants(
248248
targetId,
249249
outgoingEdge.target,

0 commit comments

Comments
 (0)