generated from coulomb/repo-seed
feat(STATE-WP-0064): add consistency sweep remote-all API endpoint
Expose POST /consistency/sweep/remote-all so activity-core can trigger the workstation ADR-001 remote-all sweep via the bridge tunnel pattern. Records consistency_sweep_remote_all progress events and documents the cutover runbook while the local custodian-sync timer remains interim.
This commit is contained in:
182
tests/test_consistency_sweep.py
Normal file
182
tests/test_consistency_sweep.py
Normal file
@@ -0,0 +1,182 @@
|
||||
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):
|
||||
return ConsistencySweepRemoteAllRun(
|
||||
max_seconds=max_seconds,
|
||||
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):
|
||||
return ConsistencySweepRemoteAllRun(
|
||||
max_seconds=max_seconds,
|
||||
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_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})
|
||||
|
||||
assert response.status_code == 201, response.text
|
||||
body = response.json()
|
||||
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"]
|
||||
Reference in New Issue
Block a user