forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
164 lines
4.9 KiB
Python
164 lines
4.9 KiB
Python
"""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()
|