plan(SP22): self-review fixes — phase4_at, browser check via shared client, dead code
This commit is contained in:
@@ -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**
|
||||
|
||||
Reference in New Issue
Block a user