generated from coulomb/repo-seed
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.
182 lines
6.1 KiB
Python
182 lines
6.1 KiB
Python
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"] |