from datetime import datetime, timedelta, timezone import pytest from api.config import settings from api.services import ops_run_projection as projection @pytest.fixture(autouse=True) def reset_projection_cache() -> None: projection.reset_ops_run_projection_cache() @pytest.mark.asyncio async def test_ops_run_projection_counts_and_contract(monkeypatch) -> None: now = datetime.now(timezone.utc) payload = { "counts": {"open": 2, "claimed": 1, "succeeded": 9, "failed": 2}, "items": [ { "id": "run-open", "activity_definition_id": "definition-1", "target_repo": "state-hub", "state": "open", "created_at": (now - timedelta(hours=2)).isoformat(), "updated_at": (now - timedelta(hours=2)).isoformat(), "result": {}, }, { "id": "run-claimed", "activity_definition_id": "definition-2", "target_repo": "activity-core", "state": "claimed", "claim_owner": "rein-aharness@railiance01", "lease_until": (now + timedelta(minutes=10)).isoformat(), "attempt": 1, "created_at": (now - timedelta(minutes=10)).isoformat(), "updated_at": now.isoformat(), "result": {}, }, { "id": "run-failed", "activity_definition_id": "definition-3", "target_repo": "binky-control", "state": "failed", "created_at": (now - timedelta(hours=3)).isoformat(), "updated_at": (now - timedelta(hours=1)).isoformat(), "result": {"error": "executor timeout"}, }, ], } async def fake_fetch(): return payload monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch) result = await projection.get_ops_run_projection(refresh=True) assert result.available is True assert result.open == 2 assert result.claimed == 1 assert result.failed_24h == 1 assert result.stuck_open_or_claimed == 1 assert {item.id for item in result.items} == {"run-open", "run-claimed", "run-failed"} failed = next(item for item in result.items if item.id == "run-failed") assert failed.last_error == "executor timeout" claimed = next(item for item in result.items if item.id == "run-claimed") assert claimed.lease["owner"] == "rein-aharness@railiance01" @pytest.mark.asyncio async def test_ops_run_projection_serves_stale_cache_on_refresh_failure(monkeypatch) -> None: async def initial_fetch(): return {"counts": {"open": 1}, "items": []} monkeypatch.setattr(projection, "_fetch_ops_runs", initial_fetch) initial = await projection.get_ops_run_projection(refresh=True) assert initial.available is True async def failed_fetch(): raise RuntimeError("upstream down") monkeypatch.setattr(projection, "_fetch_ops_runs", failed_fetch) stale = await projection.get_ops_run_projection(refresh=True) assert stale.available is True assert stale.stale is True assert "upstream down" in stale.error @pytest.mark.asyncio async def test_ops_run_projection_reports_unconfigured(monkeypatch) -> None: monkeypatch.setattr(settings, "activity_core_url", None) result = await projection.get_ops_run_projection(refresh=True) assert result.available is False assert "ACTIVITY_CORE_URL is not configured" in result.error @pytest.mark.asyncio async def test_ops_runs_summary_route(client, monkeypatch) -> None: async def fake_fetch(): return {"counts": {"open": 3, "claimed": 2}, "items": []} monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch) response = await client.get("/ops-runs/summary", params={"refresh": "true"}) assert response.status_code == 200 assert response.json()["open"] == 3 assert response.json()["claimed"] == 2 assert (await client.post("/ops-runs/claim", json={})).status_code == 404 @pytest.mark.asyncio async def test_state_summary_contains_ops_run_projection(client, monkeypatch) -> None: async def fake_fetch(): return {"counts": {"open": 4}, "items": []} monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch) response = await client.get("/state/summary", params={"refresh": "true"}) assert response.status_code == 200 assert response.json()["ops_runs"]["open"] == 4