@@ -2518,6 +2518,151 @@ def test_main_json_strict_failure_exits_nonzero(monkeypatch):
25182518 mock_print .assert_called_once_with (json .dumps (result ))
25192519
25202520
2521+ @pytest .mark .parametrize (
2522+ ("output_format" , "extra_args" , "incomplete" , "expected_exit" , "show_cycle" ),
2523+ [
2524+ pytest .param ("concise" , [], False , 1 , True , id = "concise" ),
2525+ pytest .param ("concise" , ["--limit" , "0" ], False , 1 , False , id = "concise-limit" ),
2526+ pytest .param ("json" , ["--strict" ], False , 1 , True , id = "json-strict" ),
2527+ pytest .param (
2528+ "json" , ["--strict" , "--gate" ], False , 1 , True , id = "json-strict-gate"
2529+ ),
2530+ pytest .param ("pretty" , ["--strict" ], False , 1 , True , id = "pretty-strict" ),
2531+ pytest .param ("rich" , ["--strict" ], False , 1 , True , id = "rich-strict" ),
2532+ pytest .param (
2533+ "concise" ,
2534+ ["--strict" , "--select=SKY-L012" ],
2535+ False ,
2536+ 0 ,
2537+ False ,
2538+ id = "concise-unselected" ,
2539+ ),
2540+ pytest .param (
2541+ "json" ,
2542+ ["--strict" , "--gate" , "--select=SKY-L012" ],
2543+ False ,
2544+ 0 ,
2545+ False ,
2546+ id = "json-strict-gate-unselected" ,
2547+ ),
2548+ pytest .param ("json" , ["--gate" ], False , 0 , True , id = "json-ordinary-gate" ),
2549+ pytest .param ("concise" , ["--gate" ], False , 0 , True , id = "concise-ordinary-gate" ),
2550+ pytest .param ("json" , ["--strict" , "--force" ], False , 0 , True , id = "json-forced" ),
2551+ pytest .param (
2552+ "json" , ["--strict" , "--gate" ], True , 2 , True , id = "json-incomplete"
2553+ ),
2554+ pytest .param (
2555+ "concise" , ["--force" ], True , 2 , True , id = "concise-incomplete-forced"
2556+ ),
2557+ ],
2558+ )
2559+ def test_main_circular_dependency_reporting_and_exit_codes (
2560+ monkeypatch ,
2561+ capsys ,
2562+ output_format ,
2563+ extra_args ,
2564+ incomplete ,
2565+ expected_exit ,
2566+ show_cycle ,
2567+ ):
2568+ cycle = {
2569+ "rule_id" : "SKY-CIRC" ,
2570+ "kind" : "circular_dependency" ,
2571+ "category" : "ARCHITECTURE" ,
2572+ "severity" : "MEDIUM" ,
2573+ "file" : "pkg/left.py" ,
2574+ "line" : 4 ,
2575+ "message" : "Circular dependency: pkg.left → pkg.right → pkg.left" ,
2576+ "cycle" : ["pkg.left" , "pkg.right" ],
2577+ "cycle_length" : 2 ,
2578+ "suggested_break" : "pkg.left → pkg.right" ,
2579+ }
2580+ result = {
2581+ "analysis_summary" : {"total_files" : 2 },
2582+ "circular_dependencies" : [cycle ],
2583+ }
2584+ if incomplete :
2585+ result ["analysis_errors" ] = [
2586+ {
2587+ "rule_id" : "SKY-ANALYSIS-INCOMPLETE" ,
2588+ "kind" : "syntax_error" ,
2589+ "severity" : "HIGH" ,
2590+ "file" : "broken.py" ,
2591+ "line" : 1 ,
2592+ "message" : "invalid syntax" ,
2593+ }
2594+ ]
2595+ monkeypatch .setattr (
2596+ cli .sys ,
2597+ "argv" ,
2598+ [
2599+ "skylos" ,
2600+ "." ,
2601+ "--format" ,
2602+ output_format ,
2603+ "--no-provenance" ,
2604+ "--no-upload" ,
2605+ * extra_args ,
2606+ ],
2607+ )
2608+ terminal_output = StringIO ()
2609+ fake_logger = Mock ()
2610+ fake_logger .console = Console (
2611+ file = terminal_output ,
2612+ width = 160 ,
2613+ force_terminal = False ,
2614+ theme = cli ._skylos_console_theme (),
2615+ )
2616+ exit_code = 0
2617+ with (
2618+ patch ("skylos.cli.setup_logger" , return_value = fake_logger ),
2619+ patch ("skylos.cli.Progress" , return_value = _progress_ctx ()),
2620+ patch ("skylos.cli.run_analyze" , return_value = json .dumps (result )),
2621+ patch ("skylos.cli.load_config" , return_value = {"gate" : {"max_quality" : 0 }}),
2622+ patch ("skylos.cli.print_badge" ),
2623+ ):
2624+ try :
2625+ cli .main ()
2626+ except SystemExit as exc :
2627+ exit_code = exc .code
2628+
2629+ output = capsys .readouterr ().out + terminal_output .getvalue ()
2630+ assert exit_code == expected_exit
2631+ if output_format == "json" :
2632+ rendered = json .loads (output )
2633+ assert rendered .get ("circular_dependencies" , []) == (
2634+ [cycle ] if show_cycle else []
2635+ )
2636+ elif show_cycle :
2637+ if output_format == "rich" :
2638+ assert "Circular Dependencies" in output
2639+ assert "pkg.left → pkg.right → pkg.left" in output
2640+ else :
2641+ assert output .count ("SKY-CIRC" ) == 1
2642+ assert "pkg/left.py:4" in output
2643+ assert cycle ["message" ] in output
2644+ else :
2645+ assert "SKY-CIRC" not in output
2646+ assert cycle ["message" ] not in output
2647+ if incomplete :
2648+ assert "SKY-ANALYSIS-INCOMPLETE" in output
2649+
2650+
2651+ def test_concise_circular_dependency_without_location_remains_visible ():
2652+ result = {
2653+ "circular_dependencies" : [
2654+ {
2655+ "rule_id" : "SKY-CIRC" ,
2656+ "message" : "Circular dependency: left → right → left" ,
2657+ }
2658+ ]
2659+ }
2660+
2661+ assert cli ._format_concise_results (result ) == (
2662+ "?:1 SKY-CIRC Circular dependency: left → right → left\n "
2663+ )
2664+
2665+
25212666def test_main_json_incomplete_analysis_exits_two_after_output (monkeypatch ):
25222667 result = {
25232668 "analysis_summary" : {
0 commit comments