@@ -370,3 +370,107 @@ async def end(input: dict):
370370 assert result ["a_result" ] == 1
371371 assert "b_result" not in result
372372 assert "parallel_results" not in result
373+
374+
375+ @pytest .mark .asyncio
376+ async def test_router_parallel_duplicate_node_names ():
377+ """Router returning duplicate node names should execute the same node multiple times.
378+
379+ Uses different sleep durations (1s vs 3s) to verify all branches are waited for.
380+ """
381+ import asyncio
382+
383+ call_count = {"tool_a" : 0 , "router" : 0 }
384+ sleep_times = [3 , 3 , 1 ] # Last invocation is fast, others are slow
385+
386+ @node (start = True )
387+ async def start (input : dict ):
388+ return {}
389+
390+ @router (name = "parallel_router" )
391+ async def parallel_router (input : dict ):
392+ call_count ["router" ] += 1
393+ if call_count ["router" ] > 1 :
394+ return {"choice" : "end" }
395+ # Return the same node name multiple times
396+ return {"choices" : ["tool_a" , "tool_a" , "tool_a" ]}
397+
398+ @node (name = "tool_a" )
399+ async def tool_a (input : dict ):
400+ call_count ["tool_a" ] += 1
401+ sleep_time = sleep_times .pop ()
402+ await asyncio .sleep (sleep_time )
403+ return {"a_result" : call_count ["tool_a" ]}
404+
405+ @node (end = True )
406+ async def end (input : dict ):
407+ return {"final" : "done" }
408+
409+ g = Graph ()
410+ g .add_node (start ).add_node (parallel_router ).add_node (tool_a ).add_node (end )
411+ g .add_edge (start , parallel_router )
412+ g .add_edge (parallel_router , tool_a )
413+ g .add_join ([tool_a ], parallel_router )
414+ g .add_edge (parallel_router , end )
415+
416+ result = await g .execute ({"input" : {}})
417+
418+ # tool_a should have been called 3 times (once for each duplicate in choices)
419+ assert call_count ["tool_a" ] == 3
420+ # Router should have been called twice (first for parallel, second to continue to end)
421+ assert call_count ["router" ] == 2
422+ assert result ["final" ] == "done"
423+
424+
425+ @pytest .mark .asyncio
426+ async def test_router_parallel_mixed_duplicate_nodes ():
427+ """Router returning a mix of unique and duplicate node names."""
428+ import asyncio
429+
430+ call_count = {"tool_a" : 0 , "tool_b" : 0 , "router" : 0 }
431+
432+ @node (start = True )
433+ async def start (input : dict ):
434+ return {}
435+
436+ @router (name = "parallel_router" )
437+ async def parallel_router (input : dict ):
438+ call_count ["router" ] += 1
439+ if call_count ["router" ] > 1 :
440+ return {"choice" : "end" }
441+ # Mix of unique and duplicate nodes
442+ return {"choices" : ["tool_a" , "tool_b" , "tool_a" , "tool_b" , "tool_a" ]}
443+
444+ @node (name = "tool_a" )
445+ async def tool_a (input : dict ):
446+ call_count ["tool_a" ] += 1
447+ await asyncio .sleep (0.01 )
448+ return {"a_count" : call_count ["tool_a" ]}
449+
450+ @node (name = "tool_b" )
451+ async def tool_b (input : dict ):
452+ call_count ["tool_b" ] += 1
453+ await asyncio .sleep (0.01 )
454+ return {"b_count" : call_count ["tool_b" ]}
455+
456+ @node (end = True )
457+ async def end (input : dict ):
458+ return {"final" : "done" }
459+
460+ g = Graph ()
461+ g .add_node (start ).add_node (parallel_router ).add_node (tool_a ).add_node (tool_b ).add_node (end )
462+ g .add_edge (start , parallel_router )
463+ g .add_edge (parallel_router , tool_a )
464+ g .add_edge (parallel_router , tool_b )
465+ g .add_join ([tool_a , tool_b ], parallel_router )
466+ g .add_edge (parallel_router , end )
467+
468+ result = await g .execute ({"input" : {}})
469+
470+ # tool_a should have been called 3 times
471+ assert call_count ["tool_a" ] == 3
472+ # tool_b should have been called 2 times
473+ assert call_count ["tool_b" ] == 2
474+ # Router called twice
475+ assert call_count ["router" ] == 2
476+ assert result ["final" ] == "done"
0 commit comments