|
12 | 12 | CanaryIssueUpdater, |
13 | 13 | CanaryState, |
14 | 14 | ErrorKind, |
| 15 | + _cli_main, |
15 | 16 | ) |
16 | 17 |
|
17 | 18 | pytestmark = [pytest.mark.layer("execution"), pytest.mark.small] |
@@ -202,3 +203,158 @@ def mock_run_gh(args, **kwargs): |
202 | 203 | assert num == 42 |
203 | 204 | assert state.last_issue_number == 42 |
204 | 205 | assert any(e.get("event") == "canary_issue_edit_failed" for e in cap_logs) |
| 206 | + |
| 207 | + |
| 208 | +class TestCliMain: |
| 209 | + def test_post_failure_records_and_saves( |
| 210 | + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 211 | + ) -> None: |
| 212 | + """post-failure writes updated state to the state file.""" |
| 213 | + monkeypatch.setenv("GITHUB_REPOSITORY", "test-org/test-repo") |
| 214 | + state_path = tmp_path / "state.json" |
| 215 | + |
| 216 | + gh_call_count = 0 |
| 217 | + |
| 218 | + def mock_run_gh(args, **kwargs): |
| 219 | + nonlocal gh_call_count |
| 220 | + gh_call_count += 1 |
| 221 | + return CompletedProcess(args=args, returncode=1, stdout="", stderr="") |
| 222 | + |
| 223 | + monkeypatch.setattr("autoskillit._probe_canary.run_gh", mock_run_gh) |
| 224 | + |
| 225 | + rc = _cli_main( |
| 226 | + [ |
| 227 | + "post-failure", |
| 228 | + "--state-file", |
| 229 | + str(state_path), |
| 230 | + "--backend", |
| 231 | + "claude-code", |
| 232 | + "--cli-version", |
| 233 | + "1.0.0", |
| 234 | + "--failure-type", |
| 235 | + "network", |
| 236 | + "--workflow-run-url", |
| 237 | + "https://example.com/run/1", |
| 238 | + ] |
| 239 | + ) |
| 240 | + assert rc == 0 |
| 241 | + assert state_path.exists() |
| 242 | + raw = json.loads(state_path.read_text()) |
| 243 | + assert raw["network_streak"] == 1 |
| 244 | + assert gh_call_count == 0, "run_gh must not be called when streak is below threshold" |
| 245 | + |
| 246 | + def test_post_failure_threshold_triggers_issue_creation( |
| 247 | + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 248 | + ) -> None: |
| 249 | + """When streak reaches N_CONSECUTIVE_FLAKE_GUARD, ensure_issue is called.""" |
| 250 | + monkeypatch.setenv("GITHUB_REPOSITORY", "test-org/test-repo") |
| 251 | + state_path = tmp_path / "state.json" |
| 252 | + state_path.write_text( |
| 253 | + json.dumps( |
| 254 | + { |
| 255 | + "network_streak": N_CONSECUTIVE_FLAKE_GUARD - 1, |
| 256 | + "schema_streak": 0, |
| 257 | + "last_issue_number": None, |
| 258 | + } |
| 259 | + ) |
| 260 | + ) |
| 261 | + |
| 262 | + gh_calls: list[list[str]] = [] |
| 263 | + |
| 264 | + def mock_run_gh(args, **kwargs): |
| 265 | + gh_calls.append(list(args)) |
| 266 | + if args[0:2] == ["issue", "list"]: |
| 267 | + return CompletedProcess(args=args, returncode=0, stdout="[]", stderr="") |
| 268 | + if args[0:2] == ["issue", "create"]: |
| 269 | + return CompletedProcess( |
| 270 | + args=args, |
| 271 | + returncode=0, |
| 272 | + stdout=json.dumps({"number": 7}), |
| 273 | + stderr="", |
| 274 | + ) |
| 275 | + return CompletedProcess(args=args, returncode=1, stdout="", stderr="") |
| 276 | + |
| 277 | + monkeypatch.setattr("autoskillit._probe_canary.run_gh", mock_run_gh) |
| 278 | + |
| 279 | + rc = _cli_main( |
| 280 | + [ |
| 281 | + "post-failure", |
| 282 | + "--state-file", |
| 283 | + str(state_path), |
| 284 | + "--backend", |
| 285 | + "claude-code", |
| 286 | + "--cli-version", |
| 287 | + "1.0.0", |
| 288 | + "--failure-type", |
| 289 | + "network", |
| 290 | + "--workflow-run-url", |
| 291 | + "https://example.com/run/2", |
| 292 | + ] |
| 293 | + ) |
| 294 | + assert rc == 0 |
| 295 | + assert any(call[0:2] == ["issue", "list"] for call in gh_calls) |
| 296 | + assert any(call[0:2] == ["issue", "create"] for call in gh_calls) |
| 297 | + saved = json.loads(state_path.read_text()) |
| 298 | + assert saved["network_streak"] == N_CONSECUTIVE_FLAKE_GUARD |
| 299 | + assert saved["last_issue_number"] == 7 |
| 300 | + |
| 301 | + def test_post_failure_below_threshold_no_issue( |
| 302 | + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 303 | + ) -> None: |
| 304 | + """Below threshold, no gh issue commands are issued.""" |
| 305 | + monkeypatch.setenv("GITHUB_REPOSITORY", "test-org/test-repo") |
| 306 | + state_path = tmp_path / "state.json" |
| 307 | + |
| 308 | + def mock_run_gh(args, **kwargs): |
| 309 | + raise AssertionError(f"Unexpected gh call: {args}") |
| 310 | + |
| 311 | + monkeypatch.setattr("autoskillit._probe_canary.run_gh", mock_run_gh) |
| 312 | + |
| 313 | + rc = _cli_main( |
| 314 | + [ |
| 315 | + "post-failure", |
| 316 | + "--state-file", |
| 317 | + str(state_path), |
| 318 | + "--backend", |
| 319 | + "claude-code", |
| 320 | + "--cli-version", |
| 321 | + "1.0.0", |
| 322 | + "--failure-type", |
| 323 | + "network", |
| 324 | + "--workflow-run-url", |
| 325 | + "https://example.com/run/3", |
| 326 | + ] |
| 327 | + ) |
| 328 | + assert rc == 0 |
| 329 | + assert state_path.exists() |
| 330 | + raw = json.loads(state_path.read_text()) |
| 331 | + assert raw["network_streak"] == 1 |
| 332 | + |
| 333 | + def test_post_failure_missing_github_repository( |
| 334 | + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 335 | + ) -> None: |
| 336 | + """Missing GITHUB_REPOSITORY env var returns 1.""" |
| 337 | + monkeypatch.delenv("GITHUB_REPOSITORY", raising=False) |
| 338 | + state_path = tmp_path / "state.json" |
| 339 | + |
| 340 | + rc = _cli_main( |
| 341 | + [ |
| 342 | + "post-failure", |
| 343 | + "--state-file", |
| 344 | + str(state_path), |
| 345 | + "--backend", |
| 346 | + "claude-code", |
| 347 | + "--cli-version", |
| 348 | + "1.0.0", |
| 349 | + "--failure-type", |
| 350 | + "network", |
| 351 | + "--workflow-run-url", |
| 352 | + "https://example.com/run/4", |
| 353 | + ] |
| 354 | + ) |
| 355 | + assert rc == 1 |
| 356 | + |
| 357 | + def test_cli_no_command_returns_nonzero(self) -> None: |
| 358 | + """Calling _cli_main with no subcommand returns non-zero.""" |
| 359 | + rc = _cli_main([]) |
| 360 | + assert rc != 0 |
0 commit comments