This commit was merged in pull request #1253.
This commit is contained in:
145
tests/loop/test_loop_guard_seed.py
Normal file
145
tests/loop/test_loop_guard_seed.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""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
|
||||
original_mkdir = Path.mkdir
|
||||
|
||||
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}"
|
||||
Reference in New Issue
Block a user