"""Tests for cycle_result.json validation in loop_guard. Covers validate_cycle_result(), _load_cycle_result(), and _is_issue_open(). """ from __future__ import annotations import json import time from pathlib import Path 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, "CYCLE_RESULT_FILE", tmp_path / "cycle_result.json") monkeypatch.setattr(lg, "CYCLE_DURATION", 300) monkeypatch.setattr(lg, "GITEA_API", "http://test:3000/api/v1") monkeypatch.setattr(lg, "REPO_SLUG", "owner/repo") def _write_cr(tmp_path, data: dict, age_seconds: float = 0) -> Path: """Write a cycle_result.json and optionally backdate it.""" p = tmp_path / "cycle_result.json" p.write_text(json.dumps(data)) if age_seconds: mtime = time.time() - age_seconds import os os.utime(p, (mtime, mtime)) return p # --- _load_cycle_result --- def test_load_cycle_result_missing(tmp_path): assert lg._load_cycle_result() == {} def test_load_cycle_result_valid(tmp_path): _write_cr(tmp_path, {"issue": 42, "type": "fix"}) assert lg._load_cycle_result() == {"issue": 42, "type": "fix"} def test_load_cycle_result_markdown_fenced(tmp_path): p = tmp_path / "cycle_result.json" p.write_text('```json\n{"issue": 99}\n```') assert lg._load_cycle_result() == {"issue": 99} def test_load_cycle_result_malformed(tmp_path): p = tmp_path / "cycle_result.json" p.write_text("not json at all") assert lg._load_cycle_result() == {} # --- _is_issue_open --- def test_is_issue_open_true(monkeypatch): monkeypatch.setattr(lg, "_get_token", lambda: "tok") resp_data = json.dumps({"state": "open"}).encode() class FakeResp: def read(self): return resp_data def __enter__(self): return self def __exit__(self, *a): pass with patch("urllib.request.urlopen", return_value=FakeResp()): assert lg._is_issue_open(42) is True def test_is_issue_open_closed(monkeypatch): monkeypatch.setattr(lg, "_get_token", lambda: "tok") resp_data = json.dumps({"state": "closed"}).encode() class FakeResp: def read(self): return resp_data def __enter__(self): return self def __exit__(self, *a): pass with patch("urllib.request.urlopen", return_value=FakeResp()): assert lg._is_issue_open(42) is False def test_is_issue_open_no_token(monkeypatch): monkeypatch.setattr(lg, "_get_token", lambda: "") assert lg._is_issue_open(42) is None def test_is_issue_open_api_error(monkeypatch): monkeypatch.setattr(lg, "_get_token", lambda: "tok") with patch("urllib.request.urlopen", side_effect=OSError("timeout")): assert lg._is_issue_open(42) is None # --- validate_cycle_result --- def test_validate_no_file(tmp_path): """No file → returns False, no crash.""" assert lg.validate_cycle_result() is False def test_validate_fresh_file_open_issue(tmp_path, monkeypatch): """Fresh file with open issue → kept.""" _write_cr(tmp_path, {"issue": 10}) monkeypatch.setattr(lg, "_is_issue_open", lambda n: True) assert lg.validate_cycle_result() is False assert (tmp_path / "cycle_result.json").exists() def test_validate_stale_file_removed(tmp_path): """File older than 2× CYCLE_DURATION → removed.""" _write_cr(tmp_path, {"issue": 10}, age_seconds=700) assert lg.validate_cycle_result() is True assert not (tmp_path / "cycle_result.json").exists() def test_validate_fresh_file_closed_issue(tmp_path, monkeypatch): """Fresh file referencing closed issue → removed.""" _write_cr(tmp_path, {"issue": 10}) monkeypatch.setattr(lg, "_is_issue_open", lambda n: False) assert lg.validate_cycle_result() is True assert not (tmp_path / "cycle_result.json").exists() def test_validate_api_failure_keeps_file(tmp_path, monkeypatch): """API failure → file kept (graceful degradation).""" _write_cr(tmp_path, {"issue": 10}) monkeypatch.setattr(lg, "_is_issue_open", lambda n: None) assert lg.validate_cycle_result() is False assert (tmp_path / "cycle_result.json").exists() def test_validate_no_issue_field(tmp_path): """File without issue field → kept (only age check applies).""" _write_cr(tmp_path, {"type": "fix"}) assert lg.validate_cycle_result() is False assert (tmp_path / "cycle_result.json").exists() def test_validate_stale_threshold_boundary(tmp_path, monkeypatch): """File just under threshold → kept (not stale yet).""" _write_cr(tmp_path, {"issue": 10}, age_seconds=599) monkeypatch.setattr(lg, "_is_issue_open", lambda n: True) assert lg.validate_cycle_result() is False assert (tmp_path / "cycle_result.json").exists()