from __future__ import annotations import json from datetime import UTC, datetime from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from api.schemas.consistency_sweep import ( ConsistencySweepIssueSummary, ConsistencySweepRemoteAllRun, ConsistencySweepRepoResult, ) from api.services import consistency_sweep as sweep_service @pytest.mark.asyncio async def test_remote_all_sweep_records_progress_and_parses_stderr(client, monkeypatch): async def fake_run_remote_all_sweep(session, *, max_seconds: int, source: str = "api"): return ConsistencySweepRemoteAllRun( max_seconds=max_seconds, source=source, exit_code=0, lock_skipped=False, repos_processed=[ ConsistencySweepRepoResult( repo_slug="state-hub", repo_path="/home/worsch/state-hub", result="pass", summary=ConsistencySweepIssueSummary(info=1), fixes_applied=["pull: already up to date"], ) ], skipped_clean=["demo-service"], skipped_missing=["remote-only"], skipped_budget=[], progress_event_id=None, started_at=datetime(2026, 6, 21, 12, 0, tzinfo=UTC), completed_at=datetime(2026, 6, 21, 12, 1, tzinfo=UTC), ) monkeypatch.setattr( "api.routers.consistency_sweep.run_remote_all_sweep", AsyncMock(side_effect=fake_run_remote_all_sweep), ) response = await client.post("/consistency/sweep/remote-all", json={"max_seconds": 300}) assert response.status_code == 201, response.text body = response.json() assert body["exit_code"] == 0 assert body["lock_skipped"] is False assert body["repos_processed"][0]["repo_slug"] == "state-hub" assert body["skipped_clean"] == ["demo-service"] assert body["skipped_missing"] == ["remote-only"] @pytest.mark.asyncio async def test_remote_all_sweep_lock_skip_is_idempotent(client, monkeypatch): async def fake_run_remote_all_sweep(session, *, max_seconds: int, source: str = "api"): return ConsistencySweepRemoteAllRun( max_seconds=max_seconds, source=source, exit_code=0, lock_skipped=True, repos_processed=[], skipped_clean=[], skipped_missing=[], skipped_budget=[], progress_event_id=None, started_at=datetime(2026, 6, 21, 12, 0, tzinfo=UTC), completed_at=datetime(2026, 6, 21, 12, 0, 1, tzinfo=UTC), ) monkeypatch.setattr( "api.routers.consistency_sweep.run_remote_all_sweep", AsyncMock(side_effect=fake_run_remote_all_sweep), ) response = await client.post("/consistency/sweep/remote-all", json={}) assert response.status_code == 201, response.text assert response.json()["lock_skipped"] is True assert response.json()["repos_processed"] == [] def test_parse_stderr_extracts_skip_lists(): stderr = ( " CLEAN (skipped): alpha, beta\n" " NOT ON THIS HOST (skipped): gamma\n" " BUDGET EXHAUSTED after 300s (skipped): delta, epsilon\n" ) parsed = sweep_service._parse_stderr(stderr) assert parsed == { "skipped_clean": ["alpha", "beta"], "skipped_missing": ["gamma"], "skipped_budget": ["delta", "epsilon"], } def test_extract_json_payload_skips_human_readable_prefix_lines(): stdout = ( " CLEAN (skipped): quiet-repo\n" " BUDGET EXHAUSTED after 30s (skipped): other-repo\n" '{\n "repo_slug": "state-hub",\n "repo_path": "/home/worsch/state-hub",\n' ' "result": "pass",\n "summary": {"fail": 0, "warn": 0, "info": 0},\n' ' "fixes_applied": []\n}\n' ) payload = sweep_service._extract_json_payload(stdout) assert payload["repo_slug"] == "state-hub" def test_parse_stdout_handles_single_and_batch_payloads(): single = json.dumps( { "repo_slug": "state-hub", "repo_path": "/home/worsch/state-hub", "result": "warn", "summary": {"fail": 0, "warn": 1, "info": 0}, "fixes_applied": [], } ) batch = json.dumps( [ { "repo_slug": "alpha", "repo_path": "/tmp/alpha", "result": "pass", "summary": {"fail": 0, "warn": 0, "info": 0}, "fixes_applied": [], }, { "repo_slug": "beta", "repo_path": "/tmp/beta", "result": "fail", "summary": {"fail": 1, "warn": 0, "info": 0}, "fixes_applied": [], }, ] ) single_result = sweep_service._parse_stdout(single) batch_result = sweep_service._parse_stdout(batch) assert len(single_result) == 1 assert single_result[0].result == "warn" assert [repo.repo_slug for repo in batch_result] == ["alpha", "beta"] assert batch_result[1].result == "fail" @pytest.mark.asyncio async def test_run_remote_all_sweep_invokes_script_and_logs_progress(client, monkeypatch): captured: dict[str, object] = {} def fake_run(cmd, capture_output, text): captured["cmd"] = cmd return SimpleNamespace( stdout=json.dumps( [ { "repo_slug": "state-hub", "repo_path": "/home/worsch/state-hub", "result": "pass", "summary": {"fail": 0, "warn": 0, "info": 0}, "fixes_applied": [], } ] ), stderr=" CLEAN (skipped): quiet-repo\n", returncode=0, ) async def fake_to_thread(fn, *args, **kwargs): return fn(*args, **kwargs) monkeypatch.setattr(sweep_service.asyncio, "to_thread", fake_to_thread) monkeypatch.setattr(sweep_service.subprocess, "run", fake_run) response = await client.post( "/consistency/sweep/remote-all", json={"max_seconds": 120, "source": "local-timer"}, ) assert response.status_code == 201, response.text body = response.json() assert body["source"] == "local-timer" assert "--remote" in captured["cmd"] assert "--all" in captured["cmd"] assert captured["cmd"][captured["cmd"].index("--max-seconds") + 1] == "120" assert body["exit_code"] == 0 assert body["skipped_clean"] == ["quiet-repo"] assert body["progress_event_id"] is not None events = await client.get( "/progress/", params={"event_type": "consistency_sweep_remote_all"}, ) assert events.status_code == 200, events.text assert events.json()[0]["detail"]["skipped_clean"] == ["quiet-repo"] assert events.json()[0]["detail"]["source"] == "local-timer"