"""Tests for loop_guard.seed_cycle_result and --pick mode. The seed fixes the cycle-metrics dead-pipeline bug (#1250): loop_guard pre-seeds cycle_result.json so cycle_retro.py can always resolve issue= even when the dispatcher doesn't write the file. """ from __future__ import annotations import json import sys from unittest.mock import patch import pytest import scripts.loop_guard as lg @pytest.fixture(autouse=True) def _isolate(tmp_path, monkeypatch): """Redirect loop_guard paths to tmp_path for isolation.""" monkeypatch.setattr(lg, "QUEUE_FILE", tmp_path / "queue.json") monkeypatch.setattr(lg, "IDLE_STATE_FILE", tmp_path / "idle_state.json") monkeypatch.setattr(lg, "CYCLE_RESULT_FILE", tmp_path / "cycle_result.json") monkeypatch.setattr(lg, "GITEA_API", "http://test:3000/api/v1") monkeypatch.setattr(lg, "REPO_SLUG", "owner/repo") # ── seed_cycle_result ────────────────────────────────────────────────── def test_seed_writes_issue_and_type(tmp_path): """seed_cycle_result writes issue + type to cycle_result.json.""" item = {"issue": 42, "type": "bug", "title": "Fix the thing", "ready": True} lg.seed_cycle_result(item) data = json.loads((tmp_path / "cycle_result.json").read_text()) assert data == {"issue": 42, "type": "bug"} def test_seed_does_not_overwrite_existing(tmp_path): """If cycle_result.json already exists, seed_cycle_result leaves it alone.""" existing = {"issue": 99, "type": "feature", "tests_passed": 123} (tmp_path / "cycle_result.json").write_text(json.dumps(existing)) lg.seed_cycle_result({"issue": 1, "type": "bug"}) data = json.loads((tmp_path / "cycle_result.json").read_text()) assert data["issue"] == 99, "Existing file must not be overwritten" def test_seed_missing_issue_field(tmp_path): """Item with no issue key — seed still writes without crashing.""" lg.seed_cycle_result({"type": "unknown"}) data = json.loads((tmp_path / "cycle_result.json").read_text()) assert data["issue"] is None def test_seed_default_type_when_absent(tmp_path): """Item with no type key defaults to 'unknown'.""" lg.seed_cycle_result({"issue": 7}) data = json.loads((tmp_path / "cycle_result.json").read_text()) assert data["type"] == "unknown" def test_seed_oserror_is_graceful(tmp_path, monkeypatch, capsys): """OSError during seed logs a warning but does not raise.""" monkeypatch.setattr(lg, "CYCLE_RESULT_FILE", tmp_path / "no_dir" / "cycle_result.json") from pathlib import Path def failing_mkdir(self, *args, **kwargs): raise OSError("no space left") monkeypatch.setattr(Path, "mkdir", failing_mkdir) # Should not raise lg.seed_cycle_result({"issue": 5, "type": "bug"}) captured = capsys.readouterr() assert "WARNING" in captured.out # ── main() integration ───────────────────────────────────────────────── def _write_queue(tmp_path, items): tmp_path.mkdir(parents=True, exist_ok=True) lg.QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True) lg.QUEUE_FILE.write_text(json.dumps(items)) def test_main_seeds_cycle_result_when_work_found(tmp_path, monkeypatch): """main() seeds cycle_result.json with top queue item on ready queue.""" _write_queue(tmp_path, [{"issue": 10, "type": "feature", "ready": True}]) monkeypatch.setattr(lg, "_fetch_open_issue_numbers", lambda: None) with patch.object(sys, "argv", ["loop_guard"]): rc = lg.main() assert rc == 0 data = json.loads((tmp_path / "cycle_result.json").read_text()) assert data["issue"] == 10 def test_main_no_seed_when_queue_empty(tmp_path, monkeypatch): """main() does not create cycle_result.json when queue is empty.""" _write_queue(tmp_path, []) monkeypatch.setattr(lg, "_fetch_open_issue_numbers", lambda: None) with patch.object(sys, "argv", ["loop_guard"]): rc = lg.main() assert rc == 1 assert not (tmp_path / "cycle_result.json").exists() def test_main_pick_mode_prints_issue(tmp_path, monkeypatch, capsys): """--pick flag prints the top issue number to stdout.""" _write_queue(tmp_path, [{"issue": 55, "type": "bug", "ready": True}]) monkeypatch.setattr(lg, "_fetch_open_issue_numbers", lambda: None) with patch.object(sys, "argv", ["loop_guard", "--pick"]): rc = lg.main() assert rc == 0 captured = capsys.readouterr() # The issue number must appear as a line in stdout lines = captured.out.strip().splitlines() assert str(55) in lines def test_main_pick_mode_empty_queue_no_output(tmp_path, monkeypatch, capsys): """--pick with empty queue exits 1, doesn't print an issue number.""" _write_queue(tmp_path, []) monkeypatch.setattr(lg, "_fetch_open_issue_numbers", lambda: None) with patch.object(sys, "argv", ["loop_guard", "--pick"]): rc = lg.main() assert rc == 1 captured = capsys.readouterr() # No bare integer line printed for line in captured.out.strip().splitlines(): assert not line.strip().isdigit(), f"Unexpected issue number in output: {line!r}"