@@ -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} )
0 commit comments