11"""Tests for automation.scripts.workflow_cli module."""
22
3+ import argparse
34import json
4- import subprocess
55import sys
6+ from io import StringIO
7+ from unittest .mock import patch
68
9+ import pytest
710
8- def run_cli (* args : str ) -> tuple [int , str ]:
9- """Run the workflow CLI with given arguments."""
10- result = subprocess .run (
11- [sys .executable , "-m" , "automation.scripts.workflow_cli" , * args ], capture_output = True , text = True
12- )
13- return result .returncode , result .stdout .strip ()
11+ from automation .scripts .workflow_cli import (
12+ cmd_extract_branch ,
13+ cmd_extract_parent_issue ,
14+ cmd_extract_sub_issue ,
15+ cmd_get_attempt_count ,
16+ cmd_parse_plot_path ,
17+ cmd_quality_label ,
18+ cmd_status_transition ,
19+ main ,
20+ )
21+
22+
23+ def run_cmd (func , ** kwargs ) -> tuple [int , str ]:
24+ """Run a CLI command function and capture its output."""
25+ args = argparse .Namespace (** kwargs )
26+ captured = StringIO ()
27+ with patch ("sys.stdout" , captured ):
28+ code = func (args )
29+ return code , captured .getvalue ().strip ()
1430
1531
1632class TestExtractBranch :
1733 """Tests for extract-branch command."""
1834
1935 def test_valid_auto_branch (self ):
20- code , output = run_cli ( "extract-branch" , "auto/scatter-basic/matplotlib" )
36+ code , output = run_cmd ( cmd_extract_branch , branch = "auto/scatter-basic/matplotlib" )
2137 assert code == 0
2238 data = json .loads (output )
2339 assert data ["spec_id" ] == "scatter-basic"
2440 assert data ["library" ] == "matplotlib"
2541 assert data ["is_auto_branch" ] is True
2642
43+ def test_valid_auto_branch_different_spec (self ):
44+ code , output = run_cmd (cmd_extract_branch , branch = "auto/heatmap-basic/seaborn" )
45+ assert code == 0
46+ data = json .loads (output )
47+ assert data ["spec_id" ] == "heatmap-basic"
48+ assert data ["library" ] == "seaborn"
49+ assert data ["is_auto_branch" ] is True
50+
2751 def test_invalid_branch (self ):
28- code , output = run_cli ("extract-branch" , "feature/something" )
52+ code , output = run_cmd (cmd_extract_branch , branch = "feature/something" )
53+ assert code == 1
54+ assert output == "null"
55+
56+ def test_main_branch (self ):
57+ code , output = run_cmd (cmd_extract_branch , branch = "main" )
2958 assert code == 1
3059 assert output == "null"
3160
@@ -34,12 +63,46 @@ class TestExtractSubIssue:
3463 """Tests for extract-sub-issue command."""
3564
3665 def test_with_sub_issue (self ):
37- code , output = run_cli ( "extract-sub-issue" , "Sub-Issue: #42" )
66+ code , output = run_cmd ( cmd_extract_sub_issue , pr_body = "Sub-Issue: #42" )
3867 assert code == 0
3968 assert output == "42"
4069
70+ def test_with_sub_issue_different_format (self ):
71+ code , output = run_cmd (cmd_extract_sub_issue , pr_body = "Sub-Issue: #123\n More text" )
72+ assert code == 0
73+ assert output == "123"
74+
4175 def test_without_sub_issue (self ):
42- code , output = run_cli ("extract-sub-issue" , "No sub-issue here" )
76+ code , output = run_cmd (cmd_extract_sub_issue , pr_body = "No sub-issue here" )
77+ assert code == 1
78+ assert output == ""
79+
80+ def test_empty_body (self ):
81+ code , output = run_cmd (cmd_extract_sub_issue , pr_body = "" )
82+ assert code == 1
83+ assert output == ""
84+
85+
86+ class TestExtractParentIssue :
87+ """Tests for extract-parent-issue command."""
88+
89+ def test_from_pr_body (self ):
90+ code , output = run_cmd (cmd_extract_parent_issue , pr_body = "Parent Issue: #99\n Some text" , issue_body = None )
91+ assert code == 0
92+ assert output == "99"
93+
94+ def test_from_issue_body_fallback (self ):
95+ code , output = run_cmd (cmd_extract_parent_issue , pr_body = "No parent here" , issue_body = "Parent Issue: #77" )
96+ assert code == 0
97+ assert output == "77"
98+
99+ def test_not_found (self ):
100+ code , output = run_cmd (cmd_extract_parent_issue , pr_body = "Nothing" , issue_body = "Also nothing" )
101+ assert code == 1
102+ assert output == ""
103+
104+ def test_both_none (self ):
105+ code , output = run_cmd (cmd_extract_parent_issue , pr_body = "" , issue_body = None )
43106 assert code == 1
44107 assert output == ""
45108
@@ -48,17 +111,27 @@ class TestGetAttemptCount:
48111 """Tests for get-attempt-count command."""
49112
50113 def test_with_attempt_labels (self ):
51- code , output = run_cli ( "get-attempt-count" , "ai-attempt-2,library:matplotlib" )
114+ code , output = run_cmd ( cmd_get_attempt_count , labels = "ai-attempt-2,library:matplotlib" )
52115 assert code == 0
53116 assert output == "2"
54117
118+ def test_with_attempt_1 (self ):
119+ code , output = run_cmd (cmd_get_attempt_count , labels = "ai-attempt-1" )
120+ assert code == 0
121+ assert output == "1"
122+
123+ def test_with_attempt_3 (self ):
124+ code , output = run_cmd (cmd_get_attempt_count , labels = "testing,ai-attempt-3,done" )
125+ assert code == 0
126+ assert output == "3"
127+
55128 def test_without_attempt_labels (self ):
56- code , output = run_cli ( "get-attempt-count" , "library:matplotlib,testing" )
129+ code , output = run_cmd ( cmd_get_attempt_count , labels = "library:matplotlib,testing" )
57130 assert code == 0
58131 assert output == "0"
59132
60133 def test_empty_labels (self ):
61- code , output = run_cli ( "get-attempt-count" , "" )
134+ code , output = run_cmd ( cmd_get_attempt_count , labels = "" )
62135 assert code == 0
63136 assert output == "0"
64137
@@ -67,15 +140,27 @@ class TestParsePlotPath:
67140 """Tests for parse-plot-path command."""
68141
69142 def test_valid_path (self ):
70- code , output = run_cli ( "parse-plot-path" , "plots/matplotlib/scatter/scatter-basic/default.py" )
143+ code , output = run_cmd ( cmd_parse_plot_path , path = "plots/matplotlib/scatter/scatter-basic/default.py" )
71144 assert code == 0
72145 data = json .loads (output )
73146 assert data ["library" ] == "matplotlib"
74147 assert data ["spec_id" ] == "scatter-basic"
75148 assert data ["variant" ] == "default"
76149
150+ def test_valid_path_different_library (self ):
151+ code , output = run_cmd (cmd_parse_plot_path , path = "plots/seaborn/heatmap/heatmap-correlation/default.py" )
152+ assert code == 0
153+ data = json .loads (output )
154+ assert data ["library" ] == "seaborn"
155+ assert data ["spec_id" ] == "heatmap-correlation"
156+
77157 def test_invalid_path (self ):
78- code , output = run_cli ("parse-plot-path" , "invalid/path.py" )
158+ code , output = run_cmd (cmd_parse_plot_path , path = "invalid/path.py" )
159+ assert code == 1
160+ assert output == "null"
161+
162+ def test_empty_path (self ):
163+ code , output = run_cmd (cmd_parse_plot_path , path = "" )
79164 assert code == 1
80165 assert output == "null"
81166
@@ -84,31 +169,131 @@ class TestStatusTransition:
84169 """Tests for status-transition command."""
85170
86171 def test_simple_transition (self ):
87- code , output = run_cli ("status-transition" , "generating,library:matplotlib" , "testing" )
172+ code , output = run_cmd (
173+ cmd_status_transition , current_labels = "generating,library:matplotlib" , to_status = "testing"
174+ )
88175 assert code == 0
89176 assert '--remove-label "generating"' in output
90177 assert '--add-label "testing"' in output
91178
92179 def test_no_change_needed (self ):
93- code , output = run_cli ( "status-transition" , "testing" , "testing" )
180+ code , output = run_cmd ( cmd_status_transition , current_labels = "testing" , to_status = "testing" )
94181 assert code == 0
95182 assert output == ""
96183
184+ def test_empty_current_labels (self ):
185+ code , output = run_cmd (cmd_status_transition , current_labels = "" , to_status = "reviewing" )
186+ assert code == 0
187+ assert '--add-label "reviewing"' in output
188+
189+ def test_multiple_status_labels (self ):
190+ code , output = run_cmd (cmd_status_transition , current_labels = "generating,testing" , to_status = "done" )
191+ assert code == 0
192+ assert "done" in output
193+
97194
98195class TestQualityLabel :
99196 """Tests for quality-label command."""
100197
101198 def test_excellent (self ):
102- code , output = run_cli ( "quality-label" , "95" )
199+ code , output = run_cmd ( cmd_quality_label , score = 95 )
103200 assert code == 0
104201 assert output == "quality:excellent"
105202
106203 def test_good (self ):
107- code , output = run_cli ( "quality-label" , "87" )
204+ code , output = run_cmd ( cmd_quality_label , score = 87 )
108205 assert code == 0
109206 assert output == "quality:good"
110207
208+ def test_needs_work (self ):
209+ code , output = run_cmd (cmd_quality_label , score = 78 )
210+ assert code == 0
211+ assert output == "quality:needs-work"
212+
111213 def test_poor (self ):
112- code , output = run_cli ( "quality-label" , "50" )
214+ code , output = run_cmd ( cmd_quality_label , score = 50 )
113215 assert code == 0
114216 assert output == "quality:poor"
217+
218+ def test_boundary_excellent (self ):
219+ code , output = run_cmd (cmd_quality_label , score = 90 )
220+ assert code == 0
221+ assert output == "quality:excellent"
222+
223+ def test_boundary_good (self ):
224+ code , output = run_cmd (cmd_quality_label , score = 85 )
225+ assert code == 0
226+ assert output == "quality:good"
227+
228+ def test_boundary_needs_work (self ):
229+ code , output = run_cmd (cmd_quality_label , score = 75 )
230+ assert code == 0
231+ assert output == "quality:needs-work"
232+
233+
234+ class TestMain :
235+ """Tests for main() CLI entry point."""
236+
237+ def test_extract_branch_via_main (self , monkeypatch , capsys ):
238+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" , "extract-branch" , "auto/test-spec/plotly" ])
239+ code = main ()
240+ assert code == 0
241+ captured = capsys .readouterr ()
242+ data = json .loads (captured .out .strip ())
243+ assert data ["spec_id" ] == "test-spec"
244+ assert data ["library" ] == "plotly"
245+
246+ def test_extract_sub_issue_via_main (self , monkeypatch , capsys ):
247+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" , "extract-sub-issue" , "Sub-Issue: #55" ])
248+ code = main ()
249+ assert code == 0
250+ captured = capsys .readouterr ()
251+ assert captured .out .strip () == "55"
252+
253+ def test_extract_parent_issue_via_main (self , monkeypatch , capsys ):
254+ monkeypatch .setattr (
255+ sys , "argv" , ["workflow_cli" , "extract-parent-issue" , "Parent Issue: #88" , "--issue-body" , "" ]
256+ )
257+ code = main ()
258+ assert code == 0
259+ captured = capsys .readouterr ()
260+ assert captured .out .strip () == "88"
261+
262+ def test_get_attempt_count_via_main (self , monkeypatch , capsys ):
263+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" , "get-attempt-count" , "ai-attempt-3,done" ])
264+ code = main ()
265+ assert code == 0
266+ captured = capsys .readouterr ()
267+ assert captured .out .strip () == "3"
268+
269+ def test_parse_plot_path_via_main (self , monkeypatch , capsys ):
270+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" , "parse-plot-path" , "plots/bokeh/line/line-basic/default.py" ])
271+ code = main ()
272+ assert code == 0
273+ captured = capsys .readouterr ()
274+ data = json .loads (captured .out .strip ())
275+ assert data ["library" ] == "bokeh"
276+
277+ def test_status_transition_via_main (self , monkeypatch , capsys ):
278+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" , "status-transition" , "generating" , "done" ])
279+ code = main ()
280+ assert code == 0
281+ captured = capsys .readouterr ()
282+ assert "done" in captured .out
283+
284+ def test_quality_label_via_main (self , monkeypatch , capsys ):
285+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" , "quality-label" , "92" ])
286+ code = main ()
287+ assert code == 0
288+ captured = capsys .readouterr ()
289+ assert captured .out .strip () == "quality:excellent"
290+
291+ def test_missing_command (self , monkeypatch ):
292+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" ])
293+ with pytest .raises (SystemExit ):
294+ main ()
295+
296+ def test_unknown_command (self , monkeypatch ):
297+ monkeypatch .setattr (sys , "argv" , ["workflow_cli" , "unknown-command" ])
298+ with pytest .raises (SystemExit ):
299+ main ()
0 commit comments