plan(SP22): self-review fixes — phase4_at, browser check via shared client, dead code

This commit is contained in:
Tyler
2026-06-21 12:56:25 -06:00
parent 022b229d81
commit 134eb4f404
@@ -2392,13 +2392,32 @@ async def test_phase1_preflight_passes_when_backend_healthy(tmp_path: Path):
@pytest.mark.asyncio
@respx.mock
async def test_phase1_preflight_fails_fast_if_browser_down(tmp_path: Path):
"""If the Vite dev server is not reachable, phase 1 fails-fast
with BrowserNotReachableError before phase 2 even tries."""
from cyclone_pipeline.exceptions import BrowserNotReachableError
respx.get("http://127.0.0.1:8000/api/health").mock(
return_value=Response(200, json={
"status": "ok", "version": "0.1.0",
"db": {"ok": True},
"scheduler": {"running": True, "interval_s": 60},
"pubsub": {}, "batch": {},
})
)
respx.get("http://127.0.0.1:8000/api/admin/scheduler/status").mock(
return_value=Response(200, json={
"running": True, "interval_s": 60,
"sftp_block": "co_medicaid",
})
)
# Make the head request fail with a connection error
respx.head("http://127.0.0.1:9999/").mock(
side_effect=httpx.ConnectError("Connection refused")
)
cfg = PipelineConfig(
api_base="http://127.0.0.1:8000",
browser_base="http://127.0.0.1:9999", # nothing listens here
browser_base="http://127.0.0.1:9999",
run_dir=tmp_path / "runs",
headless=True,
)
@@ -2526,10 +2545,14 @@ class CyclonePipeline:
f"Backend unhealthy: {snap.model_dump()}"
)
sched = await self._client.scheduler_status()
# Verify the browser-side URL is reachable
# Verify the browser-side URL is reachable. We use the same
# httpx.AsyncClient as the API calls so tests can mock the
# head request via `respx`.
try:
async with httpx.AsyncClient(timeout=5.0) as http:
r = await http.head(self.cfg.browser_base + "/")
assert self._client._http is not None # type: ignore[attr-defined]
r = await self._client._http.head( # type: ignore[attr-defined]
self.cfg.browser_base + "/"
)
r.raise_for_status()
except httpx.HTTPError as e:
raise BrowserNotReachableError(self.cfg.browser_base) from e
@@ -2950,6 +2973,10 @@ Append to `cyclone_pipeline/pipeline.py`:
self.run_state.pending_data["submission_filename"] = (
result_obj.filename
)
# Record the moment phase 4 completed; phase 7 uses this as
# the `since` for `processed-files` so we only see inbound
# files from the MFT scheduler *after* our submit.
self.run_state.pending_data["phase4_at"] = _now_iso()
log.info(
"phase4_submit_ok",
submission_id=result_obj.submission_id,
@@ -3418,7 +3445,7 @@ Append to `cyclone_pipeline/pipeline.py`:
t0 = time.monotonic()
assert self._client is not None
since = self.run_state.pending_data.get(
"submission_filename_set_at", self.run_state.started_at
"phase4_at", self.run_state.started_at
)
files = await self._client.processed_files(since=since)
html_files = [f for f in files if f.kind == "html"]
@@ -3442,10 +3469,8 @@ Append to `cyclone_pipeline/pipeline.py`:
result_detail=self._result_detail(),
started_at=self.run_state.started_at,
finished_at=_now_iso(),
duration_s=(
_iso_to_monotonic(self.run_state.started_at) -
_iso_to_monotonic(self.run_state.started_at) +
sum(p.duration_s for p in self.run_state.completed_phases)
duration_s=sum(
p.duration_s for p in self.run_state.completed_phases
),
phases=list(self.run_state.completed_phases),
expected_835_by=expected_835,
@@ -3548,18 +3573,6 @@ def _next_monday_iso() -> str:
return now_mt.date().isoformat()
days_ahead = (7 - weekday) % 7 or 7
return (now_mt + timedelta(days=days_ahead)).date().isoformat()
def _iso_to_monotonic(iso: str) -> float:
"""Best-effort ISO→monotonic. Returns 0.0 on parse error so the
report's `duration_s` is at least non-negative."""
try:
from datetime import datetime
return datetime.fromisoformat(
iso.replace("Z", "+00:00")
).timestamp()
except (ValueError, AttributeError):
return 0.0
```
- [ ] **Step 4: Run test to verify it passes**