@@ -170,6 +170,13 @@ def test_flow_config_dependency_not_scheduled_in_flow():
170170 FlowConfig .model_validate (raw )
171171
172172
173+ def test_flow_config_duplicate_phase_raises ():
174+ raw = _minimal_config ()
175+ raw ["flow" ] = [{"phase" : "p1" }, {"phase" : "p1" }]
176+ with pytest .raises (ValidationError , match = "Duplicate phase" ):
177+ FlowConfig .model_validate (raw )
178+
179+
173180def test_flow_config_unknown_agent_in_phase ():
174181 raw = _minimal_config ()
175182 raw ["phases" ]["p1" ]["agent" ] = "nonexistent"
@@ -295,3 +302,109 @@ def test_from_yaml_invalid_yaml(tmp_path):
295302def test_from_yaml_missing_file (tmp_path ):
296303 with pytest .raises (FlowConfigError , match = "Failed to load" ):
297304 FlowConfig .from_yaml (tmp_path / "nonexistent.yaml" , tmp_path )
305+
306+
307+ # ---------------------------------------------------------------------------
308+ # FlowConfig cycle detection via model_validate
309+ # ---------------------------------------------------------------------------
310+
311+
312+ def _three_phase_config () -> dict :
313+ agent = {"tools" : []}
314+ task = {"name" : "t" , "prompt" : "Do it." }
315+ return {
316+ "agents" : {"writer" : agent },
317+ "phases" : {
318+ "p1" : {"agent" : "writer" , "tasks" : [task ]},
319+ "p2" : {"agent" : "writer" , "tasks" : [task ]},
320+ "p3" : {"agent" : "writer" , "tasks" : [task ]},
321+ },
322+ }
323+
324+
325+ def test_flow_config_direct_cycle_raises ():
326+ raw = _three_phase_config ()
327+ raw ["flow" ] = [
328+ {"phase" : "p1" , "dependencies" : ["p2" ]},
329+ {"phase" : "p2" , "dependencies" : ["p1" ]},
330+ ]
331+ with pytest .raises (ValidationError , match = "Cycle" ):
332+ FlowConfig .model_validate (raw )
333+
334+
335+ def test_flow_config_three_node_cycle_raises ():
336+ raw = _three_phase_config ()
337+ raw ["flow" ] = [
338+ {"phase" : "p1" , "dependencies" : ["p3" ]},
339+ {"phase" : "p2" , "dependencies" : ["p1" ]},
340+ {"phase" : "p3" , "dependencies" : ["p2" ]},
341+ ]
342+ with pytest .raises (ValidationError , match = "Cycle" ):
343+ FlowConfig .model_validate (raw )
344+
345+
346+ def test_flow_config_acyclic_chain_ok ():
347+ raw = _three_phase_config ()
348+ raw ["flow" ] = [
349+ {"phase" : "p1" },
350+ {"phase" : "p2" , "dependencies" : ["p1" ]},
351+ {"phase" : "p3" , "dependencies" : ["p1" ]},
352+ ]
353+ config = FlowConfig .model_validate (raw )
354+ assert len (config .flow ) == 3
355+
356+
357+ def test_flow_disjoined_graphs_ok ():
358+ agent = {"tools" : []}
359+ task = {"name" : "t" , "prompt" : "Do it." }
360+ raw = {
361+ "agents" : {"writer" : agent },
362+ "phases" : {
363+ "p1" : {"agent" : "writer" , "tasks" : [task ]},
364+ "p2" : {"agent" : "writer" , "tasks" : [task ]},
365+ "p3" : {"agent" : "writer" , "tasks" : [task ]},
366+ "p4" : {"agent" : "writer" , "tasks" : [task ]},
367+ },
368+ "flow" : [
369+ {"phase" : "p1" },
370+ {"phase" : "p2" , "dependencies" : ["p1" ]},
371+ {"phase" : "p3" },
372+ {"phase" : "p4" , "dependencies" : ["p3" ]},
373+ ],
374+ }
375+ config = FlowConfig .model_validate (raw )
376+ assert len (config .flow ) == 4
377+
378+
379+ def test_flow_config_self_dependency_raises ():
380+ raw = _minimal_config ()
381+ raw ["flow" ] = [{"phase" : "p1" , "dependencies" : ["p1" ]}]
382+ with pytest .raises (ValidationError , match = "Cycle" ):
383+ FlowConfig .model_validate (raw )
384+
385+
386+ def test_flow_config_two_independent_cycles_reports_both ():
387+ agent = {"tools" : []}
388+ task = {"name" : "t" , "prompt" : "Do it." }
389+ raw = {
390+ "agents" : {"writer" : agent },
391+ "phases" : {
392+ "p1" : {"agent" : "writer" , "tasks" : [task ]},
393+ "p2" : {"agent" : "writer" , "tasks" : [task ]},
394+ "p3" : {"agent" : "writer" , "tasks" : [task ]},
395+ "p4" : {"agent" : "writer" , "tasks" : [task ]},
396+ },
397+ "flow" : [
398+ # dependency edges: p1→p3→p2→p1 and p1→p4→p2→p1
399+ {"phase" : "p1" , "dependencies" : ["p3" , "p4" ]},
400+ {"phase" : "p2" , "dependencies" : ["p1" ]},
401+ {"phase" : "p3" , "dependencies" : ["p2" ]},
402+ {"phase" : "p4" , "dependencies" : ["p2" ]},
403+ ],
404+ }
405+ with pytest .raises (ValidationError ) as exc_info :
406+ FlowConfig .model_validate (raw )
407+ error = str (exc_info .value )
408+ assert "Cycle" in error
409+ assert "p1 → p3 → p2 → p1" in error
410+ assert "p1 → p4 → p2 → p1" in error
0 commit comments