phase: 29_gateway_dashboard_page
Phase 29 completion pass is done. Final state:
**Verification (all re-run this pass, no task files touched)**
- `uv run pytest` → **1746 passed, 16 skipped**; `uv run pytest --cov` → **99.05 %** (gate 94 %); `bash .agents/validate.sh` → **rc=0** (ruff clean, pyright `0 errors, 0 warnings`, `node --check` clean); `uv run pytest -m live --live` → **12 passed**.
- Live, read-only: dev server **10/10 sources `ok`** (incl. `gateway`); `data/logs/29-task06-live.cjs` → **49 PASS / 0 FAIL** with per-port inbound counts reconciled against a direct `/proc/net/tcp{,6}` read (divergence 0); `data/logs/verify-29-task06-predates.py` → 0 failed (1.1.0 arm renders `WHY_PREDATES`, no pipes, every tally `n/a`, job still `ok`); phase-27 mobile audit re-run over all nine served pages → **371 PASS / 0 FAIL / 0 FINDING** (`data/logs/29-final-mobile-audit.out`).
- Criteria: (1) unconfigured `GATEWAY_HOST` → `404 {configured:false}` with dial spies + `#gate-empty` sentence ✅; (2) 1.1.0/`--no-proxy` → `supported:false`, no `0`s, never red ✅; (3) live gateway 1.2.0 → pipes animate, totals reconcile ✅; (4) no `severity`/verdict colour anywhere, validate green ✅; (5) docs updated + gap 13 logged ✅; (6) the five existing dashboards, jobs and `/api/summary` unchanged (live control leg) ✅.
**Docs (consistency pass, incl. pre-existing drift)**
- `README.md`: architecture diagram now lists `sources/gateway.py`, the 30 s gateway job and the third `/snapshot` reader; `GATEWAY_HOST` row says *a name, never a URL*; the `POLL_MS_*` row states `/gateway`'s fixed 15 s browser poll; "nine pollers" → per-source cadences; phase-28 "until phase 29" wording → current state; new Testing paragraph for `tests/test_gateway_source.py` (join pinned from the captures, reconciliation identity, `getaddrinfo` spy).
- `AGENTS.md`: phase-28 stop-point wording replaced with the shipped phase-29 state; suite/coverage figures → 1746 / 16 skipped / 99.08 %→99.05 % band and `~99 %`; window-chips paragraph records that `/gateway` has **no** chips (fixed 24 h); new testing-policy bullet for the join; stale page counts fixed (seven → eight tabbed pages / `MARKUP_PAGES`, clock-order and badge-count transcripts annotated); gap 12 now records the phase-29 nine-page audit re-run.
- `tests/test_api.py`: comment/docstring-only fixes for the "seven pages" wording that phase 29 made eight.
**Deviations / notes**
- Nothing implemented: all six task files were already in `complete/`, so this run was the final verification + docs pass; no git index or phase file was touched.
- Coverage wobbles 99.05–99.08 % between runs on a pre-existing timing-dependent line (`collectors.py` `/slots`-alias `except`/`break`), not a phase-29 path (`sources/gateway.py` is 100 %).
- `uv pip list --outdated` still lists upgrades; dependencies did not move in this phase, so they were left alone rather than upgraded mid-verification.
- Historical browser-transcript counts (phase 18/20/25 script runs) were left as recorded history and annotated instead of rewritten.
**Next phase:** none — `.agents/phases/todo/` is empty after 29; further work needs a new phase authored first.
This commit is contained in:
@@ -33,7 +33,7 @@ from dashboards import certs, collectors
|
||||
from dashboards.config import Settings, load
|
||||
from dashboards.rates import RateEstimator
|
||||
from dashboards.severity import classify
|
||||
from dashboards.sources import nut
|
||||
from dashboards.sources import gateway, nut
|
||||
from dashboards.sources.disks import SMART_SERIES, DisksCollector
|
||||
from dashboards.sources.host import HostCollector
|
||||
from slots_contract import assert_slot_shape
|
||||
@@ -255,6 +255,46 @@ async def test_disks_source_charts_the_physical_drives_the_agent_reports(
|
||||
pytest.skip("no host agent answered")
|
||||
|
||||
|
||||
async def test_the_gateway_source_joins_the_live_ingress(settings: Settings,
|
||||
agent_tls: dict[str, collectors.AgentTls]) -> None:
|
||||
"""Phase 29: `sources/gateway.py` over the **real** gateway agent, read-only.
|
||||
|
||||
The production path — the same `fetch_snapshot` the poller will run, through the stored
|
||||
mTLS authority — so what fails here is a drift between the agent's sections and the join,
|
||||
not a typo in a fixture. The assertions are the ones that hold whatever the traffic is:
|
||||
the agent answers the three sections, every configured frontend is a pipe, and the
|
||||
inbound/outbound/unmatched tallies **partition** the kernel's ESTABLISHED table, which is
|
||||
what lets the page's numbers reconcile against a direct `/proc/net/tcp` read.
|
||||
"""
|
||||
target = next((item for item in settings.host_targets if item.name == settings.gateway_host),
|
||||
None)
|
||||
if target is None:
|
||||
pytest.skip(f"GATEWAY_HOST={settings.gateway_host!r} is not in HOST_TARGETS")
|
||||
async with httpx.AsyncClient(timeout=settings.gateway_timeout) as client:
|
||||
try:
|
||||
snapshot = await collectors.fetch_snapshot(client, target,
|
||||
timeout=settings.gateway_timeout,
|
||||
tls=agent_tls.get(target.name))
|
||||
except collectors.UpstreamError as exc:
|
||||
pytest.skip(f"{target.name} did not answer: {exc}")
|
||||
payload = gateway.parse(snapshot, target.name)
|
||||
if not payload["supported"]:
|
||||
pytest.fail(f"{target.name} runs agent {payload.get('agent_version')} — the gateway page "
|
||||
"needs 1.2.0's haproxy/nginx/sockets sections (AGENTS.md gap 11's rollout)")
|
||||
assert payload["host"] == target.name
|
||||
assert payload["sockets"]["established"] == len(snapshot["sockets"]["established"])
|
||||
assert payload["inbound"], "no frontend on either config would leave the page empty"
|
||||
for pipe in payload["inbound"]:
|
||||
assert isinstance(pipe["connections"], int), "the socket table answered, so a count is known"
|
||||
for group in payload["unmatched"]["groups"]:
|
||||
assert group["connections"] > 0 and group["host_count"] > 1, (
|
||||
"a group is only ever the unresolved fan-out of several names on one port")
|
||||
attributed = sum(pipe["connections"] or 0 for pipe in payload["inbound"] + payload["outbound"])
|
||||
assert attributed + payload["unmatched"]["inbound"] + payload["unmatched"]["outbound"] == \
|
||||
payload["sockets"]["established"], "every socket counted exactly once"
|
||||
assert all(sample.source == gateway.SOURCE for sample in gateway.samples(payload, 0.0))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("engine", ["gufo", "vllm"])
|
||||
async def test_engine_still_publishes_the_series_we_parse(engine: str,
|
||||
settings: Settings) -> None:
|
||||
|
||||
Reference in New Issue
Block a user