main
py 181 lines 6.04 KB
Raw
1 from __future__ import annotations
2
3 import json
4 from pathlib import Path
5 from unittest import mock
6
7 from scripts import copilot_failure
8
9
10 def test_classifies_copilot_token_failure_as_actionable_non_retryable() -> None:
11 report = copilot_failure.classify_log("Authentication failed: Bad credentials", exit_code=1)
12
13 assert report.failure_class == "copilot_token_failure"
14 assert report.retryable is False
15 assert report.actionable is True
16 assert "renew COPILOT_GH_TOKEN" in report.diagnostic
17 assert (
18 copilot_failure.classify_log("HTTP 403 from Copilot").failure_class
19 == "copilot_token_failure"
20 )
21 assert (
22 copilot_failure.classify_log("HTTP 401 from Copilot").failure_class
23 == "copilot_token_failure"
24 )
25 assert (
26 copilot_failure.classify_log("HTTP 403 invalid token").failure_class
27 == "copilot_token_failure"
28 )
29
30
31 def test_classifies_context_timeout_and_transient_failures() -> None:
32 assert (
33 copilot_failure.classify_log("maximum context length exceeded").failure_class
34 == "context_too_large"
35 )
36 assert copilot_failure.classify_log("request timed out").failure_class == "timeout"
37 assert (
38 copilot_failure.classify_log("HTTP 503 temporarily unavailable").failure_class
39 == "transient_error"
40 )
41
42
43 def test_main_writes_report_without_creating_issue_for_transient(tmp_path: Path) -> None:
44 log_path = tmp_path / "copilot.log"
45 report_path = tmp_path / "report.json"
46 log_path.write_text("HTTP 429 rate limit", encoding="utf-8")
47
48 with mock.patch.object(copilot_failure, "create_or_update_token_issue") as issue_mock:
49 exit_code = copilot_failure.main(
50 [
51 "--log",
52 str(log_path),
53 "--exit-code",
54 "1",
55 "--report-json",
56 str(report_path),
57 "--create-token-issue",
58 "--repo",
59 "jmservera/SquadScope",
60 ]
61 )
62
63 payload = json.loads(report_path.read_text(encoding="utf-8"))
64 assert exit_code == 0
65 assert payload["failure_class"] == "transient_error"
66 assert payload["retryable"] is True
67 issue_mock.assert_not_called()
68
69
70 def test_main_creates_or_updates_issue_for_token_failure(tmp_path: Path) -> None:
71 log_path = tmp_path / "copilot.log"
72 report_path = tmp_path / "report.json"
73 log_path.write_text("COPILOT_GITHUB_TOKEN invalid token", encoding="utf-8")
74
75 with mock.patch.object(
76 copilot_failure,
77 "create_or_update_token_issue",
78 return_value="https://github.com/jmservera/SquadScope/issues/123",
79 ) as issue_mock:
80 exit_code = copilot_failure.main(
81 [
82 "--log",
83 str(log_path),
84 "--exit-code",
85 "1",
86 "--report-json",
87 str(report_path),
88 "--create-token-issue",
89 "--repo",
90 "jmservera/SquadScope",
91 "--week",
92 "2026-W23",
93 "--run-id",
94 "27055543722",
95 ]
96 )
97
98 payload = json.loads(report_path.read_text(encoding="utf-8"))
99 assert exit_code == 0
100 assert payload["failure_class"] == "copilot_token_failure"
101 assert payload["issue"] == "https://github.com/jmservera/SquadScope/issues/123"
102 issue_mock.assert_called_once()
103
104
105 def test_main_creates_or_updates_issue_for_copilot_inaccessible(tmp_path: Path) -> None:
106 log_path = tmp_path / "copilot.log"
107 report_path = tmp_path / "report.json"
108 log_path.write_text("copilot is not available: command not found", encoding="utf-8")
109
110 with mock.patch.object(
111 copilot_failure,
112 "create_or_update_token_issue",
113 return_value="https://github.com/jmservera/SquadScope/issues/123",
114 ) as issue_mock:
115 exit_code = copilot_failure.main(
116 [
117 "--log",
118 str(log_path),
119 "--exit-code",
120 "127",
121 "--report-json",
122 str(report_path),
123 "--create-token-issue",
124 "--repo",
125 "jmservera/SquadScope",
126 "--week",
127 "2026-W23",
128 "--run-id",
129 "27055543722",
130 ]
131 )
132
133 payload = json.loads(report_path.read_text(encoding="utf-8"))
134 assert exit_code == 0
135 assert payload["failure_class"] == "copilot_inaccessible"
136 assert payload["issue"] == "https://github.com/jmservera/SquadScope/issues/123"
137 issue_mock.assert_called_once()
138
139
140 def test_create_or_update_token_issue_returns_consistent_url_for_existing_issue() -> None:
141 report = copilot_failure.classify_log("invalid token")
142 responses = [
143 mock.Mock(
144 returncode=0,
145 stdout='[{"number": 123, "title": "Renew GitHub Copilot token for weekly analysis workflow"}]',
146 stderr="",
147 ),
148 mock.Mock(returncode=0, stdout="", stderr=""),
149 ]
150
151 with mock.patch.object(copilot_failure, "run_gh", side_effect=responses):
152 result = copilot_failure.create_or_update_token_issue(
153 report,
154 repo="jmservera/SquadScope",
155 assignee="jmservera",
156 week="2026-W23",
157 run_id="27055543722",
158 )
159
160 assert result == "https://github.com/jmservera/SquadScope/issues/123"
161
162
163 def test_create_or_update_token_issue_returns_consistent_url_for_created_issue() -> None:
164 report = copilot_failure.classify_log("invalid token")
165 responses = [
166 mock.Mock(returncode=0, stdout="[]", stderr=""),
167 mock.Mock(
168 returncode=0, stdout="https://github.com/jmservera/SquadScope/issues/124\n", stderr=""
169 ),
170 ]
171
172 with mock.patch.object(copilot_failure, "run_gh", side_effect=responses):
173 result = copilot_failure.create_or_update_token_issue(
174 report,
175 repo="jmservera/SquadScope",
176 assignee="jmservera",
177 week="2026-W23",
178 run_id="27055543722",
179 )
180
181 assert result == "https://github.com/jmservera/SquadScope/issues/124"