phase: 28_gateway_agent_proxy

**Phase 28 final pass — complete, 100 % green.** All five tasks were already `complete/`; this pass verified the criteria, re-ran the live half, and did the docs pass.

- Gate `bash .agents/validate.sh` **rc=0**: `1650 passed, 15 skipped`, coverage **98.97 %** (floor 94), `ruff` clean, `pyright` **0 errors**, `node --check` clean, hygiene clean (`/tmp/validate-done.log`).
- Live: `pytest -m live --live` **11 passed**; `./scripts/dev-server.sh status` nine sources `ok`; mTLS re-read of `gateway` → agent **1.2.0**, all three sections `available: true`, 6/6 frontends routed, 11 nginx servers + `$targetBackend`, **19** listeners, no core errors (`data/logs/28-finalpass-live.out`); installed agent file byte-identical to this tree (`b0e9ebf1b718…`).
- Criteria: 1 ✅ pytest/coverage · 2 ✅ three sections over a captured tree, `false`/`null` + one `errors` line each elsewhere with `cpu`/`mem`/`disk_io`/`system` intact · 3 ✅ `AGENT_VERSION` 1.2.0 + SCHEMA names all three · 4 ✅ six fixtures verbatim, capture refuses a bad read, 116 socket address columns hold no globally routable endpoint · 5 ✅ gateway runs 1.2.0 and answers over mTLS (transcript `data/logs/28-task05-rollout.md`, `28-finalpass-live.out`) · 6 ✅ validate · 7 ✅ additive: HEAD's 1.1.0 and this 1.2.0 `collect()`ed over one `--root` are key-for-key equal plus the three new sections, `--no-proxy` equal exactly (`data/logs/verify-28-final.py` → 41 PASS, `28-final-verify.out`).
- Docs fixed (README/AGENTS/Containerfile, all re-green): README's opening said "Four dashboards" above a five-row table and said the new sections are "stored in the snapshot" (they are in the **agent's** document — no `/api` payload, no `sample` row, nothing charts them until phase 29); `Containerfile`'s header still said "three dashboards"; AGENTS's coverage figure (98.94 → **98.97 %**), invariant 6 gained the captured-socket-table identifier rule, the agent bullet gained "phase 28 stops at the wire", and the fleet record names the stash-and-compare control plus the volatile listener count. Full record: `data/logs/NOTES-28-final.md`.
- Deviations: none functional; no git index/history touched, phase files untouched (`PLAN.md` and `complete/` unchanged). `uv pip list --outdated` still lists 4 packages (fastapi 0.153→0.161 etc.) — advisory only, dependencies untouched by this phase, left for a deliberate pass.
- Next pending phase: `.agents/phases/todo/29_gateway_dashboard_page/` (the page that consumes these three sections).
This commit is contained in:
2026-10-22 20:24:01 -04:00
parent 5d37444fb0
commit 44d61d3ab9
45 changed files with 4759 additions and 42 deletions
+49
View File
@@ -249,6 +249,55 @@ async def test_real_degradations_still_show_when_smart_noise_is_alongside() -> N
assert result.payload["degraded"] == ["temperatures: no hwmon sensors"]
async def test_a_host_without_a_proxy_config_does_not_degrade_the_system_source() -> None:
"""The proxy add-on's absence lines, same rule and same reason as `smart:` above: two of this
fleet's three hosts run no haproxy and no nginx, so an agent upgrade must not put a
`degraded` payload (and therefore an overview alert, `alerts.py` reads this key) on a box that
never had either. The lines survive in `agent.errors` for the page that owns them — phase 29's
gateway source reads them the way `/disks` reads `smart_errors`.
`collectors.ADDON_ERROR_PREFIXES` is the one list of these prefixes; this asserts every
name by behaviour, so dropping any of them fails here, and `tests/test_agent.py` pins both
the wording of the lines the agent actually writes and the equality of the two lists.
"""
missing_haproxy = ("haproxy: /etc/haproxy/haproxy.cfg not found — haproxy is not installed "
"here, or its config lives elsewhere (--haproxy-config); --no-proxy "
"opts out")
missing_nginx = ("nginx: /etc/nginx/nginx.conf not found — nginx is not installed here, or "
"its config lives elsewhere (--nginx-config); --no-proxy opts out")
no_sockets = ("sockets: /proc/net/tcp not found, /proc/net/tcp6 not found — this host "
"publishes no kernel socket table; --no-proxy opts out")
body = agent_body()
body["errors"] = [missing_haproxy, missing_nginx, no_sockets,
"smart: smartctl 7.5 not found"]
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json=body)
async with client_for(handler) as client:
result = await fetch_host_snapshot(client, Target("gw", "http://agent.test"),
HostCollector(RateEstimator()))
assert "degraded" not in result.payload, "unconfigured is not broken (invariant 3)"
for prefix in ("haproxy:", "nginx:", "sockets:"):
assert any(prefix in str(e) for e in result.payload["agent"]["errors"]), \
"the sentence survives for the page that explains it"
async def test_a_real_degradation_shows_alongside_the_proxy_addon_noise() -> None:
"""Filtering the add-ons must not swallow a genuine half-snapshot beside them."""
body = agent_body()
body["errors"] = ["nginx: /etc/nginx/nginx.conf is there but could not be read: boom",
"temperatures: no hwmon sensors"]
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json=body)
async with client_for(handler) as client:
result = await fetch_host_snapshot(client, Target("gw", "http://agent.test"),
HostCollector(RateEstimator()))
assert result.payload["degraded"] == ["temperatures: no hwmon sensors"]
# --------------------------------------------------------------------------- #
# physical disks (the /disks source's fetch half)
# --------------------------------------------------------------------------- #