Files
dashboards/tests/test_api.py
T
ducoterra 51772ffef2 phase: 32_gateway_resolved_target_liveness
Fixture and live stream.conf are byte-identical (modulo header comment). Let me test the parser directly:
2026-10-26 14:03:42 -04:00

10076 lines
587 KiB
Python

"""HTTP contract: what the pages may assume, and what must never leak.
The behaviours here are the ones a screenshot cannot show: an unconfigured source answering
404 `configured: false` instead of dialling a hostname that does not exist; a bearer token
that stops at the backend; the single write endpoint refusing on every gate; no external
asset the network can fail to deliver.
"""
from __future__ import annotations
import inspect
import ipaddress
import json
import re
import time
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from pathlib import Path
from typing import Any
import httpx
import pytest
from cryptography import x509
from cryptography.x509.oid import NameOID
from fastapi import HTTPException
from fastapi.testclient import TestClient
import store_perf
from auth_client import sign_in
from auth_contract import ADMIN_PASSWORD, FAST_PBKDF2
from card_contract import HOST_SHAPES, assert_card_contract
from dashboards import alerts, api, auth, certs
from dashboards import app as app_module
from dashboards.api import MIN_PASSWORD_LENGTH, RANGE_SECONDS, _disk_visible, parse_range
from dashboards.app import create_app
from dashboards.collectors import UpstreamError
from dashboards.config import Settings, Target
from dashboards.rates import MIN_WINDOW_SECONDS, RateEstimator
from dashboards.severity import classify
from dashboards.sources import gateway as gateway_source
from dashboards.sources import nut
from dashboards.sources.disks import SMART_SERIES, DisksCollector, parse_disks_snapshot
from dashboards.sources.host import HostCollector, parse_host_snapshot
from dashboards.sources.llm import (
HISTORY_METRICS,
parse_gufo_metrics,
parse_vllm_metrics,
)
from dashboards.store import RES_RAW, Event, Sample, SampleStore
from rate_window_contract import LLM_WINDOW, delta_label
from slots_contract import (
CANONICAL_TASK_ID_FIELD,
OPTIONAL_NEXT_TOKEN_FIELDS,
OPTIONAL_ROW_FIELDS,
REQUIRED_NEXT_TOKEN_FIELDS,
REQUIRED_ROW_FIELDS,
TASK_ID_ALIASES,
USER_CONTENT_FIELDS,
load_slots_capture,
)
# refused immediately, and never dials out of the test environment
DEAD_URL = "http://127.0.0.1:1"
#: Every dashboard route, in nav order. The markup pins below (no CDN, no `.sev-*`, no
#: narration, no orphan `id`) run across this list rather than a hand-written four, so a new page
#: cannot join the masthead and skip the checks the others had to pass.
#: `/gateway` joined it in phase 29 task 05, which is the moment it became the **sixth** dashboard
#: rather than a route with a page: this tuple is the tab table, so entering it means the tab, its
#: order, and its `aria-current` are pinned on every signed-in page from here on.
PAGES = ("/", "/system", "/disks", "/llm", "/ups", "/gateway")
#: `/settings` is a **page** and not a dashboard (phase 11): it carries the same masthead with
#: no `aria-current` tab, and the account link in every dashboard's masthead is how you reach
#: it. Kept out of `PAGES` because the pins over that tuple assert tab structure — one current
#: tab, a fixed tab order — which this page deliberately does not have. `MARKUP_PAGES` is the
#: list for the checks that are about the bytes rather than the nav.
SETTINGS_PAGE = "/settings"
#: `/certs` is the second admin page (phase 15 task 05), kept out of `PAGES` for exactly the same
#: reason: it is not a tab. It is in `MARKUP_PAGES` and `PAGE_SCRIPTS`, so the CDN, `.sev-`,
#: flavour-text and orphan-`id` audits cover it like the six dashboards.
CERTS_PAGE = "/certs"
#: The name the `/gateway` pins use (`PAGE_SCRIPTS`, its page-shape parametrises) now that the
#: tuple above owns its tab.
GATEWAY_PAGE = "/gateway"
#: Every page that renders the masthead while signed in: the six dashboards and the two admin
#: pages. The nav pins loop this list, which is why eight pages carry six tabs and exactly two of
#: them carry nothing current.
MARKUP_PAGES = (*PAGES, SETTINGS_PAGE, CERTS_PAGE)
@contextmanager
def build_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *, simulate: bool = False,
**overrides: Any) -> Iterator[TestClient]:
"""A TestClient whose lifespan runs, against a private DB and no real network.
The store is injected, so `create_app()` deliberately does not close it (production
owns its own); the caller here does, or every test leaks a SQLite handle and the
`--cov` log fills with ResourceWarnings that bury the warnings worth reading.
"""
if simulate:
monkeypatch.setenv("NUT_SIMULATE", "1")
else:
monkeypatch.delenv("NUT_SIMULATE", raising=False)
db = str(tmp_path / "api.sqlite3")
settings = Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2, **overrides)
store = SampleStore(db)
client = TestClient(create_app(settings, store))
client.__enter__()
try:
sign_in(client)
yield client
finally:
client.__exit__(None, None, None)
store.close()
@pytest.fixture
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]:
with build_client(tmp_path, monkeypatch) as test_client:
yield test_client
@pytest.fixture
def ups_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]:
"""NUT on the labelled simulator, actions allowed — the dev configuration."""
with build_client(tmp_path, monkeypatch, simulate=True, nut_host="127.0.0.1",
nut_allow_actions=True) as c:
yield c
def wait_for_source_error(client: TestClient, key: str, fragment: str,
*, timeout: float = 15.0) -> str:
"""Wait for one poll loop to record an error saying `fragment`, and hand back the sentence.
The poll loops run in the app's event loop, so an assertion about what a source said has to
wait for a poll rather than read the state at import time. The failure names the last error
seen, because "it never said that" and "it said something else entirely" are different bugs.
"""
deadline = time.time() + timeout
error = ""
while time.time() < deadline:
rows = {row["key"]: row for row in client.get("/api/health").json()["sources"]}
error = str((rows.get(key) or {}).get("error") or "")
if fragment in error:
return error
time.sleep(0.05)
pytest.fail(f"{key} never reported {fragment!r} (last error: {error!r})")
def wait_for_payload(client: TestClient, path: str, *, timeout: float = 5.0) -> dict[str, Any]:
deadline = time.time() + timeout
while time.time() < deadline:
response = client.get(path)
if response.status_code == 200:
return response.json()
time.sleep(0.1)
pytest.fail(f"{path} never became available")
def failing_host_payload(scrape: Any, agent_snapshot: dict[str, Any]) -> dict[str, Any]:
"""Two scrapes of the real driveripper capture, nudged onto known sides of the table.
Driven from the fixture rather than hand-written so a schema drift breaks here first.
"""
def nudge(snap: dict[str, Any]) -> None:
snap["memory"]["used_bytes"] = snap["memory"]["total_bytes"] * 0.95 # mem.used_pct -> fail
snap["cpu_temperature_celsius"] = 91.0 # cpu.temp_c -> fail
for fs in snap["filesystems"]:
fs["used_pct"] = 93.0 # disk -> fail
return scrape(agent_snapshot, cpu_busy=10.0, mutate=nudge)
@pytest.fixture
def data_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
agent_snapshot: dict[str, Any], host_scrape: Any) -> Iterator[TestClient]:
"""Overview-shaped app: one host payload in the hub plus the simulated UPSes.
The host job points at a dead port, so a poll can never overwrite the injected payload
(a failed poll keeps the last good one) — the numbers below are ours, not the fleet's.
"""
monkeypatch.setenv("NUT_SIMULATE", "1")
db = str(tmp_path / "summary.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, nut_host="127.0.0.1",
admin_password=ADMIN_PASSWORD, pbkdf2_iterations=FAST_PBKDF2,
host_targets=(Target("driveripper", DEAD_URL),)), store)
test_client = TestClient(app)
test_client.__enter__()
try:
sign_in(test_client)
app.state.ctx.hub.jobs["host:driveripper"].state.payload = failing_host_payload(
host_scrape, agent_snapshot)
wait_for_payload(test_client, "/api/ups/snapshot")
yield test_client
finally:
test_client.__exit__(None, None, None)
store.close()
@pytest.fixture
def healthy_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
agent_snapshot: dict[str, Any],
host_scrape: Any) -> Iterator[TestClient]:
"""The all-clear app: one host answering on both of its jobs, nothing past a threshold.
The job's collector is swapped for one that hands back the injected payload, so the
health record says `ok` truthfully instead of being overridden behind the poller — and
that has to happen for the `disks` job too, which polls the same dead port: a host is
either answering or it is not, and a fixture that claims all-clear cannot leave one of
its two readers failing. Each reader gets **its own** projection of the same capture
(`parse_disks_snapshot` for the drive page), because a `disks` job holding a `system`
payload would make `/api/summary`'s drive block answer from the wrong shape and read as
a passing test that proved nothing.
"""
with build_client(tmp_path, monkeypatch,
host_targets=(Target("driveripper", DEAD_URL),)) as test_client:
from dashboards.poller import Result # noqa: PLC0415 - only this fixture needs it
payload = host_scrape(agent_snapshot, cpu_busy=10.0)
disks_payload = parse_disks_snapshot("driveripper", agent_snapshot,
DisksCollector()).payload
async def collect() -> Result:
return Result(payload=payload, samples=[])
async def collect_disks() -> Result:
return Result(payload=disks_payload, samples=[])
hub = test_client.app.state.ctx.hub # type: ignore[attr-defined]
job = hub.jobs["host:driveripper"]
disks_job = hub.jobs["disks:driveripper"]
job.fn = collect
disks_job.fn = collect_disks
# let the poll already in flight against the dead port land first (it fails in
# microseconds), then take the health record over: the alerts must describe a host
# that is answering, not the half-second of startup where it was not
deadline = time.time() + 2.0
while time.time() < deadline and job.state.attempts == 0:
time.sleep(0.02)
time.sleep(0.05)
for reader in hub.jobs.values():
if reader.dashboard not in ("system", "disks"):
continue
reader.state.payload = disks_payload if reader is disks_job else payload
reader.state.ok = True
reader.state.error = None
reader.state.consecutive_failures = 0
reader.state.last_ok = time.time()
yield test_client
# ---- severity travels with the value --------------------------------------- #
def test_summary_carries_the_verdict_for_every_column_the_page_colours(data_client) -> None:
body = data_client.get("/api/summary").json()
row = body["system"]["hosts"][0]
assert row["severity"] == {"cpu_usage_pct": "ok", "mem_used_pct": "fail",
"disk_worst_used_pct": "fail", "temp_c": "fail"}
# the fleet card is judged by the same table, so amber means the same thing at both
# sizes — and no GPU verdict, at fleet size or host size (phase 06: busy is the job)
assert body["system"]["fleet"]["severity"] == {
"cpu_usage_pct_avg": "ok", "memory_used_pct": "fail",
"disk_worst_used_pct": "fail",
}
for ups in body["ups"]["ups"]:
assert set(ups["severity"]) == {"charge_pct", "load_pct", "runtime_seconds"}
assert ups["severity"]["charge_pct"] == classify("ups.charge_pct", ups["charge_pct"])
assert ups["severity"]["charge_pct"] == "ok", "the simulator starts at float charge"
def test_summary_shape_did_not_shrink(data_client) -> None:
"""Additive-only anchor: /api/summary gained `severity` and lost nothing.
The overview, the live tests, and any external consumer read the flat keys; this test is
the tripwire for a well-meaning refactor that "tidies" them away.
"""
body = data_client.get("/api/summary").json()
fleet_keys = {"hosts_total", "hosts_live", "cpu_cores", "cpu_usage_pct_avg",
"memory_total_bytes", "memory_used_bytes", "memory_used_pct",
"disk_worst_used_pct", "load1_total", "net_rx_bytes_per_s",
"net_tx_bytes_per_s", "gpu_count", "gpu_busy_pct_avg", "power_estimate_w"}
host_keys = {"name", "os", "cpu_usage_pct", "load1", "cores", "mem_used_pct",
"disk_worst_used_pct", "gpu_busy_pct", "temp_c", "uptime_seconds"}
ups_keys = {"name", "overall", "status", "charge_pct", "load_pct", "runtime_seconds"}
assert fleet_keys <= set(body["system"]["fleet"]), fleet_keys - set(body["system"]["fleet"])
assert host_keys <= set(body["system"]["hosts"][0]), host_keys - set(body["system"]["hosts"][0])
assert ups_keys <= set(body["ups"]["ups"][0]), ups_keys - set(body["ups"]["ups"][0])
# `disks` joined in phase 09 and `gateway` in phase 29, and changed nothing else: the set
# grows, never shrinks
assert set(body) == {"ts", "system", "disks", "gateway", "llm", "ups", "recent_events",
"alerts"}
# ...and the new keys are there beside the old ones, not instead of them
assert "severity" in body["system"]["fleet"]
assert "severity" in body["system"]["hosts"][0]
assert "severity" in body["ups"]["ups"][0]
# `/` paints two panels; the payload is still the whole-fleet contract. Every path here is
# read by something that is not the overview page: the live suite, an external probe, a
# notifier later. Deleting a card must never delete a key.
#: `/gateway`'s entries are the eleven fields phase 30 task 02 reshaped the block into plus the
#: `warnings` tally task 03 joined, listed rather than spot-checked because two consumers read it: a
#: notifier walking the totals, and the overview's one line (`page-index.js`'s `GATEWAY_FIELDS` +
#: `GATEWAY_WARNING_FIELD` + `href`), which is pinned against this same document by
#: `test_the_overview_line_reads_the_gateway_block_it_claims`. The pair of **row counts**
#: (`listeners`, `connections` — what the two configs declare) sits beside the pair of **live sums**
#: (`live_in`, `live_out` — what the socket table answered) because a gateway can have thirty
#: declared routes and no measurement of any of them, and `warnings` belongs with the row counts:
#: a config fact, so `0` there is the truth and only `null` is unknown.
SUMMARY_CONTRACT = ("system.enabled", "system.fleet", "system.hosts",
"disks.enabled", "disks.fleet", "disks.hosts",
"gateway.configured", "gateway.host", "gateway.href", "gateway.supported",
"gateway.unreachable", "gateway.listeners", "gateway.connections",
"gateway.live_in", "gateway.live_out", "gateway.map_targets",
"gateway.warnings", "gateway.states",
"llm.engines", "ups.ups", "recent_events", "alerts")
def dig(body: dict[str, Any], path: str) -> Any:
"""`body["a"]["b"]` that names the missing path when it is not there."""
node: Any = body
for part in path.split("."):
assert isinstance(node, dict) and part in node, f"/api/summary lost `{path}`"
node = node[part]
return node
@pytest.mark.parametrize("fixture", ["client", "data_client"])
def test_summary_serves_every_path_another_consumer_reads(request, fixture: str) -> None:
"""The keys survive the overview cleanup whether or not anything is configured.
Run twice on purpose: `client` has no targets at all, so it proves the keys are present
as `None`/`[]` rather than omitted when a source is unconfigured (invariant 3 is about
the payload shape as much as about colour); `data_client` proves the same paths carry
the fleet once there is one.
"""
test_client = request.getfixturevalue(fixture)
body = test_client.get("/api/summary").json()
populated = fixture == "data_client"
for path in SUMMARY_CONTRACT:
dig(body, path) # presence is the assertion; a missing path raises with its name
assert dig(body, "system.enabled") is populated
assert (dig(body, "system.fleet") is not None) is populated
names = {host["name"] for host in dig(body, "system.hosts")}
assert names == ({"driveripper"} if populated else set())
# the drive-health block is configured exactly when the agents it reads are, and lists the
# same hosts: a `disks` job exists for every `HOST_TARGETS` entry and for no other reason
assert dig(body, "disks.enabled") is populated
assert (dig(body, "disks.fleet") is not None) is populated
assert {host["name"] for host in dig(body, "disks.hosts")} == names
batteries = {item["name"] for item in dig(body, "ups.ups")}
assert batteries == ({"apc", "cyberpower"} if populated else set())
# the ingress block is present and empty in **both** fixtures: it needs a *name* among the
# targets (`GATEWAY_HOST`, default `gateway`), and `data_client` monitors `driveripper`. An
# unconfigured source is a block of nulls with no states, not a missing key and not a red row
# — and the nulls are nulls rather than `0`, because "no job ran" is not "no connections".
assert dig(body, "gateway.configured") is False
assert dig(body, "gateway.states") == []
assert (dig(body, "gateway.listeners"), dig(body, "gateway.connections")) == (None, None)
assert (dig(body, "gateway.live_in"), dig(body, "gateway.live_out")) == (None, None)
assert dig(body, "llm.engines") == [], "neither fixture configures an engine"
assert isinstance(dig(body, "recent_events"), list)
assert isinstance(dig(body, "alerts"), list)
# and the payload as a whole still carries every top-level section it always did
assert set(body) >= {"ts", "system", "llm", "ups", "recent_events", "alerts"}
# ---- alerts: the landing page's answer to "what is wrong" ------------------- #
def test_summary_is_empty_of_alerts_when_everything_is_healthy(healthy_client) -> None:
"""`[]`, not a green banner: an empty list is the clearest possible all-clear."""
body = healthy_client.get("/api/summary").json()
assert body["alerts"] == []
assert body["system"]["fleet"]["hosts_live"] == 1
def test_summary_raises_one_alert_for_the_host_that_stopped_answering(data_client) -> None:
"""One dead host is one line naming it, with the link to the page that explains it."""
body = data_client.get("/api/summary").json()
down = [item for item in body["alerts"] if item["source"] == "driveripper"]
assert len(down) == 1, "its stale payload must not add amber lines on top of the red one"
assert down[0]["severity"] == "fail"
assert down[0]["href"] == "/system"
assert "unreachable" in down[0]["message"]
def test_a_host_that_has_never_answered_is_still_a_row_in_the_fleet(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A dead agent is a red row in the fleet, not a fleet that quietly got smaller.
`/api/system/snapshot` already stubs a job with no payload; `/api/summary` has to agree
with it, or a target that never answered since startup leaves the table at "2/2 hosts"
while the panel above says a third box is down (`AGENTS.md` invariants 3 and 4).
"""
with build_client(tmp_path, monkeypatch,
host_targets=(Target("driveripper", DEAD_URL),)) as test_client:
body = test_client.get("/api/summary").json()
rows = body["system"]["hosts"]
assert [row["name"] for row in rows] == ["driveripper"]
assert rows[0]["unreachable"] is True
assert rows[0]["cpu_usage_pct"] is None, "n/a, never a 0.0 nobody measured"
assert body["system"]["fleet"]["hosts_total"] == 1
assert body["system"]["fleet"]["hosts_live"] == 0
# and the alert list stays exactly one line for it, not a row plus a stale payload
assert [item["source"] for item in body["alerts"]] == ["driveripper"]
def test_summary_alerts_are_worst_first_and_carry_the_contract(data_client) -> None:
body = data_client.get("/api/summary").json()
alerts_list = body["alerts"]
assert alerts_list, "a dead host plus simulated UPS data cannot read as all-clear"
for item in alerts_list:
assert set(item) == {"id", "severity", "source", "message", "href", "at"}
assert item["severity"] in ("warn", "fail")
ranks = [0 if item["severity"] == "fail" else 1 for item in alerts_list]
assert ranks == sorted(ranks), "fails before warns"
assert any("simulat" in item["message"].lower() and item["href"] == "/ups"
for item in alerts_list), "NUT_SIMULATE must be said out loud on the landing page"
def test_summary_reuses_the_alert_builder_verbatim(data_client) -> None:
"""The endpoint is a thin call: what `alerts.build` says over the same state is the body."""
body = data_client.get("/api/summary").json()
hub = data_client.app.state.ctx.hub # type: ignore[attr-defined]
settings = data_client.app.state.ctx.settings # type: ignore[attr-defined]
hosts = [job.state.payload for job in hub.jobs.values()
if job.dashboard == "system" and job.state.payload]
ups_payload = hub.jobs["nut"].state.payload
expected = alerts.build(settings, hub.states(), hosts, [], ups_payload, [])
assert [item["id"] for item in body["alerts"]] == [item["id"] for item in expected]
def test_snapshot_payloads_carry_their_own_severity_sections(data_client) -> None:
"""/api/system/snapshot and /api/ups/snapshot ship the same verdicts the summary reuses."""
system = data_client.get("/api/system/snapshot").json()
host = system["hosts"][0]
assert set(host["cpu"]["severity"]) == {"usage_pct", "temp_c"}
assert set(host["memory"]["severity"]) == {"used_pct"}
assert set(host["disks"]["severity"]) == {"worst_used_pct"}
assert "severity" not in host["gpus"][0], (
"a GPU verdict came back on /api/system/snapshot — phase 06 removed it"
)
ups = data_client.get("/api/ups/snapshot").json()
for card in ups["ups"]:
assert set(card["severity"]) == {"charge_pct", "load_pct", "battery_runtime"}
# numbers keep their own shape; severity is a sibling, never a replacement
assert isinstance(card["numbers"], dict) and "charge_pct" in card["numbers"]
# ---- the self-test verdict on the two surfaces that were wrong (phase 12) --- #
#: The `ups.test.result` sentences as the hardware answers them. `Done and passed` is the
#: CyberPower's verbatim word from the 2026-10-19 capture (`tests/test_nut.py
#: ::LIVE_SELF_TEST_WORDS`); `FAILED` is what a driver says when a test ran and did not pass,
#: and is the reason the check exists at all. `tests/test_nut.py` pins the *word map*; what is
#: pinned here is the surface `TODO.md:3` names — a warning in the overview's Warnings panel —
#: because a verdict that grades correctly inside `assess()` and still reaches the alert feed
#: as a warning is the same bug wearing a different hat.
SELF_TEST_PASS_WORD = "Done and passed"
SELF_TEST_FAIL_WORD = "FAILED"
#: How `/api/summary` says the check out loud (`alerts._ups_alerts`): `"<check name>: <detail>"`.
SELF_TEST_ALERT_TEXT = "Last self-test"
def set_self_test_verdict(payload: dict[str, Any], status: str, detail: str) -> None:
"""Rewrite every card's `Last self-test` check in the hub's own UPS payload.
Mutating the payload the job actually serves (instead of hand-building a fake one) is what
makes this an end-to-end pin: `/api/ups/snapshot` and `alerts.build()` then read the *same*
object, so the test cannot pass by having the two surfaces disagree about the verdict.
"""
graded = 0
for card in payload["ups"]:
for check in card["health"]["checks"]:
if check["name"] == SELF_TEST_ALERT_TEXT:
check["status"] = status
check["detail"] = detail
graded += 1
assert graded, "no `Last self-test` check in the UPS payload — nothing was tested"
@contextmanager
def self_test_app(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> Iterator[tuple[TestClient, dict[str, Any]]]:
"""A UPS app whose served payload cannot be replaced by a poll mid-assertion.
The simulated UPSes answer `PASSED`, so the pass direction would survive a race with the
poll loop and the *fail* direction would not — a test that flakes on a poll is a test of
timing. Hence `healthy_client`'s trick for the `nut` job: the collector hands back the very
dict this harness holds, so the health record says `ok` truthfully (invariant 4) and the
payload stays ours. The interval is shortened for the same reason — waiting a real
`NUT_INTERVAL_SECONDS` per test would only make the suite slower at proving the same thing.
"""
with build_client(tmp_path, monkeypatch, simulate=True, nut_host="127.0.0.1",
nut_interval=0.5) as test_client:
from dashboards.poller import Result # noqa: PLC0415 - only this harness needs it
hub = test_client.app.state.ctx.hub # type: ignore[attr-defined]
job = hub.jobs["nut"]
wait_for_payload(test_client, "/api/ups/snapshot")
payload = job.state.payload
async def collect() -> Result:
return Result(payload=payload, samples=[])
job.fn = collect
# let one poll of *our* collector land before the caller mutates the payload, so the
# health record and the served bytes are both demonstrably ours
baseline = job.state.attempts
deadline = time.time() + 5.0
while time.time() < deadline and job.state.attempts <= baseline:
time.sleep(0.02)
assert job.state.payload is payload and job.state.ok, (
"the nut job is not serving the payload under test — the pin below would be fiction"
)
yield test_client, payload
def self_test_alerts(alerts_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [item for item in alerts_list
if SELF_TEST_ALERT_TEXT.lower() in item["message"].lower()
or SELF_TEST_ALERT_TEXT.lower() in item["id"].lower()]
def test_a_passed_self_test_is_quiet_on_the_overview_and_ok_on_the_ups_page(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The TODO's sentence, end to end: `Done and passed` is not a warning anywhere.
Both halves of the fix are asserted on one payload: `/api/ups/snapshot` carries the `ok`
entry the `/ups` check list renders verbatim, and `/api/summary` — the only call the
overview makes — carries **no** row naming it. The simulated card still contributes its
`NUT_SIMULATE` line, so the list is not empty and this cannot be a vacuous pass.
"""
with self_test_app(tmp_path, monkeypatch) as (test_client, payload):
set_self_test_verdict(payload, "ok", SELF_TEST_PASS_WORD)
cards = wait_for_payload(test_client, "/api/ups/snapshot")["ups"]
for card in cards:
check = next(c for c in card["health"]["checks"]
if c["name"] == SELF_TEST_ALERT_TEXT)
assert check["status"] == "ok", f"{card['name']} reports a pass as {check['status']}"
assert check["detail"] == SELF_TEST_PASS_WORD, "the driver's word is the useful half"
rows = self_test_alerts(test_client.get("/api/summary").json()["alerts"])
assert rows == [], f"a passed battery test is on the overview as {rows}"
assert any(item["source"] == "ups" for item in
test_client.get("/api/summary").json()["alerts"]), (
"the simulated-UPS line disappeared, so this assertion has nothing left to be quiet about"
)
def test_a_never_tested_battery_is_not_an_alert_either(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The APC's factory `No test initiated` reaches the overview as silence, not amber.
This is the state the live hardware is actually in on one of the two UPSes, so the
screenshot proves the absence of a warning for a *never-run* test while the payload pin
above proves it for a passed one. `unknown` is not a verdict an operator can act on, and
`alerts._RANK` admits only `fail` and `warn`.
"""
with self_test_app(tmp_path, monkeypatch) as (test_client, payload):
set_self_test_verdict(payload, "unknown", nut.NEVER_TESTED_DETAIL)
cards = wait_for_payload(test_client, "/api/ups/snapshot")["ups"]
assert all(next(c for c in card["health"]["checks"]
if c["name"] == SELF_TEST_ALERT_TEXT)["status"] == "unknown"
for card in cards)
assert self_test_alerts(test_client.get("/api/summary").json()["alerts"]) == []
def test_a_failed_self_test_still_reaches_the_overview(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The check keeps its reason for existing: a test that failed is a red line, once per UPS.
Without this direction the absence test above is satisfiable by deleting the check, which
is the failure mode the phase's completion criterion calls out.
"""
with self_test_app(tmp_path, monkeypatch) as (test_client, payload):
set_self_test_verdict(payload, "fail", SELF_TEST_FAIL_WORD)
names = {card["name"] for card in
wait_for_payload(test_client, "/api/ups/snapshot")["ups"]}
assert names, "no UPS card to fail"
rows = self_test_alerts(test_client.get("/api/summary").json()["alerts"])
assert {row["source"] for row in rows} == names
for row in rows:
assert row["severity"] == "fail", f"a failed battery test reads {row['severity']}"
assert row["href"] == "/ups", "the alert must link to the card that names the UPS"
assert SELF_TEST_ALERT_TEXT in row["message"] and SELF_TEST_FAIL_WORD in row["message"]
assert row["message"].count(SELF_TEST_ALERT_TEXT) == 1, (
"the row names the check twice — see alerts._clean_error")
assert all(row["id"] == f"{row['source']}:health:{SELF_TEST_ALERT_TEXT}" for row in rows)
def test_the_ups_check_list_needs_no_new_markup_for_a_passed_verdict(client) -> None:
"""Task 03 expected no page change, and this pins *why* there is none.
`page-ups.js` puts `check.status` straight on the `<li>`, and `dash.css` colours only the
`.mark` glyph from the shared `--ok`/`--warn`/`--fail` tokens while the check's name and
detail stay `--text`/`--text-muted` — so an `ok` row is quiet ink with a tick, and a verdict
fixed in `assess()` is a verdict rendered. Nothing on either asset names the self-test
check, which is the point: a special case added here would be a second opinion about the
driver's word, and the API already made it.
"""
js = _code(client.get("/static/page-ups.js").text)
assert '<li class="${check.status}">' in js, "the row no longer wears the payload verdict as its class"
for verdict in ("ok", "warn", "fail", "unknown"):
assert f"{verdict}: '" in js[js.index("const MARKS"):], f"MARKS lost the {verdict} glyph"
rows = js[js.index("const health = item.health"):js.index("const commands = item.commands")]
assert "self-test" not in rows.lower(), "the check list grew an opinion about one check by name"
css = _code(_css(client))
ok_rule = re.search(r"ul\.checks li\.ok \.mark \{([^}]*)\}", css)
assert ok_rule and "var(--ok)" in ok_rule.group(1), "the ok tick is not the shared token"
assert "ul.checks li.ok {" not in css, "the whole ok row is coloured — a pass is not a banner"
assert "self-test" not in css.lower(), "the stylesheet special-cases one check by name"
# ---- unconfigured: say so, and do not dial -------------------------------- #
def test_config_reports_every_source_as_off(client) -> None:
body = client.get("/api/config").json()
assert body["system"]["enabled"] is False
assert body["system"]["targets"] == []
assert body["disks"] == {"enabled": False, "interval_seconds": 300.0,
"timeout_seconds": 30.0}, (
"the drive-health page is configured exactly when the host agents are")
assert body["gateway"] == {
"enabled": False,
# the default name is still printed when nothing matches it: "no host called `gateway` is
# being dialled" is the sentence, and the name is the half the operator has to change
"host": "gateway",
"url": None,
"interval_seconds": 30.0,
"timeout_seconds": 10.0,
}, "the ingress is configured by a name in HOST_TARGETS and by nothing else"
assert body["llm"]["gufo"]["enabled"] is False
assert body["llm"]["vllm"]["enabled"] is False
assert body["ups"]["enabled"] is False
assert body["ups"]["actions_enabled"] is False
assert body["ranges"], "the page builds its window buttons from this list"
def test_health_with_nothing_configured_is_not_a_failure(client) -> None:
"""Nothing configured is 'empty', not 'broken' — and the page must not go red."""
body = client.get("/api/health").json()
assert body["status"] in ("ok", "empty")
assert body["ok"] is True
assert body["sources"] == []
def test_the_disk_source_has_its_own_health_record_per_host(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""`disks` is a source of its own, not a field of `system` (phase 09).
Its own record is what gives the page its own dot and its own staleness, and it is
also what makes `alerts.py` need a rule: two jobs read the same agent, so one dead
host must still be one line on the overview (see `test_summary_raises_one_alert...`).
"""
with build_client(tmp_path, monkeypatch,
host_targets=(Target("driveripper", DEAD_URL),)) as test_client:
sources = {row["key"]: row for row in test_client.get("/api/health").json()["sources"]}
assert "disks:driveripper" in sources, "no separate record, no separate dot"
assert sources["disks:driveripper"]["dashboard"] == "disks"
assert sources["disks:driveripper"]["label"] == "driveripper"
assert sources["disks:driveripper"]["configured"] is True
assert sources["host:driveripper"]["dashboard"] == "system", (
"the system job is still its own source: one slow SMART poll must not "
"decide whether the host looks alive")
def test_no_targets_means_no_disk_job_that_could_look_broken(client) -> None:
"""No host agents, no `disks` task at all — so nothing red for an unconfigured page."""
assert client.get("/api/health").json()["sources"] == []
@pytest.mark.parametrize("path", ["/api/system/snapshot", "/api/llm/snapshot",
"/api/ups/snapshot", "/api/ups/commands"])
def test_unconfigured_source_is_404_configured_false(client, path) -> None:
response = client.get(path)
assert response.status_code == 404
assert response.json()["configured"] is False
assert "detail" in response.json()
def test_summary_still_answers_with_no_sources(client) -> None:
body = client.get("/api/summary").json()
assert body["system"]["enabled"] is False
assert body["system"]["fleet"] is None
assert body["llm"]["enabled"] is False
assert body["ups"]["enabled"] is False
def test_unknown_route_is_json_not_html(client) -> None:
response = client.get("/api/nope")
assert response.status_code == 404
assert response.headers["content-type"].startswith("application/json")
assert "no such route" in response.json()["detail"]
def test_an_upstream_failure_escaping_a_route_keeps_its_own_status(tmp_path: Path) -> None:
"""`collectors.UpstreamError` is the one error type a route may raise to say "upstream".
The handler turns it into `{ok: false, detail}` with the collector's status (502, or 504
for a timeout) instead of a 500 traceback, and the message is the collector's already
redacted sentence. Nothing dials here: the error is raised by the route under test.
"""
db = str(tmp_path / "upstream-error.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
@app.get("/api/_boom")
async def boom() -> None:
raise UpstreamError("driveripper: http://127.0.0.1:9120/snapshot timed out", status=504)
with TestClient(app) as test_client:
sign_in(test_client)
response = test_client.get("/api/_boom")
assert response.status_code == 504, "a timeout stays a timeout, not a 500"
assert response.json() == {"ok": False,
"detail": "driveripper: http://127.0.0.1:9120/snapshot "
"timed out"}
assert "no-store" in response.headers["cache-control"]
store.close()
# ---- tokens --------------------------------------------------------------- #
def test_agent_tokens_never_reach_the_browser(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("NUT_SIMULATE", "")
targets = [Target(name="driveripper", url="http://driveripper:9120",
headers={"Authorization": "Bearer SEKRIT-TOKEN"})]
db = str(tmp_path / "tokens.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=tuple(targets)), store)
with TestClient(app) as client:
sign_in(client)
body = client.get("/api/config")
assert "SEKRIT-TOKEN" not in body.text
target = body.json()["system"]["targets"][0]
assert target["name"] == "driveripper"
assert "SEKRIT" not in target["url"] and "@" not in target["url"]
# and a host that never answered stays in the list, marked, with the fleet card intact
snapshot = wait_for_state(client, "/api/system/snapshot")
assert snapshot["hosts"][0]["name"] == "driveripper"
assert snapshot["hosts"][0].get("unreachable") is True
assert snapshot["fleet"]["hosts_total"] == 1
assert snapshot["fleet"]["hosts_live"] == 0
assert snapshot["fleet"]["memory_total_bytes"] is None
store.close()
def test_a_dead_host_never_writes_its_token_into_the_feed(tmp_path: Path) -> None:
"""The second path a credential can take to a browser: the failure message.
`parse_targets` moves a userinfo token out of the URL and into a bearer header, so the
documented `HOST_TARGETS` path never puts one in `Target.url` — this is the backstop for
any `Target` that does (a hand-built one, a future parser), and the message path is where
it would matter: the poller keeps an `UpstreamError` string in the health record and logs
it as an event, so a userinfo-bearing URL there would sit in SQLite and be served on
`/api/events` forever (AGENTS.md invariant 6). Asserted on every endpoint a page reads,
not just `/api/config`.
"""
db = str(tmp_path / "dead-host-token.sqlite3")
store = SampleStore(db)
settings = Settings(
db_path=db,
admin_password=ADMIN_PASSWORD, pbkdf2_iterations=FAST_PBKDF2,
host_interval=0.05,
host_targets=(Target(name="driveripper", url="http://SEKRIT-AGENT-TOKEN@127.0.0.1:1"),),
)
with TestClient(create_app(settings, store)) as client:
sign_in(client)
deadline = time.time() + 10
while time.time() < deadline:
health = client.get("/api/health").json()
if health["sources"] and health["sources"][0]["error"]:
break
time.sleep(0.05)
else:
pytest.fail("the dead host never recorded an error")
for path in ("/api/health", "/api/events", "/api/system/snapshot",
"/api/disks/snapshot", "/api/summary"):
body = client.get(path).text
assert "SEKRIT-AGENT-TOKEN" not in body, f"{path} leaked the agent token"
# and the outage is still legible — redaction is not the same as silence
assert "driveripper" in client.get("/api/health").text
assert "unreachable" in client.get("/api/events").text
# the `disks` job dials the same token-bearing URL on its own cadence, so its
# sentence is a second copy of the same risk: `fetch_snapshot` is the one place
# either dashboard builds a failure message, and it uses the redacted URL
disks_state = next(row for row in client.get("/api/health").json()["sources"]
if row["key"] == "disks:driveripper")
assert disks_state["error"] and "SEKRIT-AGENT-TOKEN" not in disks_state["error"]
assert "127.0.0.1:1/snapshot" in disks_state["error"], "the failure still names where"
store.close()
def test_a_revoked_agent_fails_both_of_its_sources_without_a_dial(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The chain end to end, wiring included: revoke it, and the next poll stops talking to it.
`collectors.agent_tls_map` is what `build_hub` hands the jobs, so this is the proof that a
revocation reaches a *running* app through its own `meta` table — no restart, and no second
process telling the first. The spy replaces `httpx.AsyncClient.get` on the class, so it
catches the shared client and the per-dial TLS client alike: what must not happen is any
request at all, because a dial that then gets refused still put a socket on a host the
operator struck out.
"""
db = str(tmp_path / "revoked-agent.sqlite3")
store = SampleStore(db)
settings = Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_interval=0.05, disks_interval=0.05,
host_targets=(Target(name="deskwork", url="https://127.0.0.1:1"),))
with TestClient(create_app(settings, store)) as client:
sign_in(client)
# Before the revoke the same dead host fails as an unreachable upstream. That sentence is
# the control: it is what a dial produces, so the one after it has to mean something else.
wait_for_source_error(client, "host:deskwork", "unreachable")
state = certs.issue_agent_cert(store, certs.ensure_certs(store), "deskwork",
["127.0.0.1"])
certs.revoke(store, state, "deskwork")
calls: list[str] = []
async def spy_get(self: Any, url: Any, **kwargs: Any) -> Any:
calls.append(str(url))
raise AssertionError(f"a revoked agent was dialled at {url}")
monkeypatch.setattr(httpx.AsyncClient, "get", spy_get)
for key in ("host:deskwork", "disks:deskwork"):
error = wait_for_source_error(client, key, "agent certificate revoked")
assert "unreachable" not in error, "the refusal replaced the dial's sentence"
assert calls == [], "the gate is in front of the network, not behind it"
# and the overview says the operator revoked it rather than that the box went down
messages = [row["message"] for row in client.get("/api/summary").json()["alerts"]]
assert any("certificate revoked" in text for text in messages), messages
store.close()
def test_engine_urls_are_redacted_the_same_way_hosts_are(tmp_path: Path) -> None:
"""`GUFO_BASE_URL`/`VLLM_BASE_URL` go to the browser verbatim in `/api/config`, and unlike
`HOST_TARGETS` nothing parses a userinfo token out of them into a header — so a URL that
happens to carry credentials has to be redacted where it is served (invariant 6).
"""
db = str(tmp_path / "engine-redact.sqlite3")
settings = Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
gufo_url="http://SEKRIT-ENGINE-TOKEN@deskwork:8731",
vllm_url="http://driveripper:8000")
store = SampleStore(db)
with TestClient(create_app(settings, store)) as client:
sign_in(client)
body = client.get("/api/config")
assert "SEKRIT-ENGINE-TOKEN" not in body.text
llm = body.json()["llm"]
assert llm["gufo"]["base_url"] == "http://deskwork:8731"
assert llm["vllm"]["base_url"] == "http://driveripper:8000" # untouched when clean
store.close()
@pytest.mark.parametrize(("llm_interval", "rate_window"), [(2.0, 10.0), (4.0, 20.0)])
def test_build_hub_gives_each_source_the_estimator_its_own_cadence_fills(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
llm_interval: float, rate_window: float) -> None:
"""Two rate estimators, one per measuring cadence (phase 23, `TODO.md:8`).
The spy is the point: the numbers themselves are `config` defaults and a test on the default
would pass even if `build_hub` handed one estimator to both sources. So this asserts the two
**objects** the jobs receive, and does it at a non-default cadence pair to prove the LLM span
tracks `LLM_INTERVAL_SECONDS` rather than a second knob nobody configured — one 10 s window
smoothed a slot start across the five polls the operator was watching (the ramp), and a 2 s
window over a 10 s `host` poll would answer `n/a` every poll.
The payload-level twin of this pin is the `Δ<window>` label each rate cell carries: this is
the wiring, that is what the browser prints (phase 23 task 02 pins the labels).
"""
seen: dict[str, RateEstimator] = {}
real_host_jobs = app_module.host_jobs
real_llm_jobs = app_module.llm_jobs
def spy_host_jobs(targets: Any, http: Any, host_collector: Any, interval: float,
**kwargs: Any) -> list[Any]:
seen["host"] = host_collector.rates
return real_host_jobs(targets, http, host_collector, interval, **kwargs)
def spy_llm_jobs(cfg: Any, http: Any, rates: RateEstimator) -> list[Any]:
seen["llm"] = rates
return real_llm_jobs(cfg, http, rates)
monkeypatch.setattr(app_module, "host_jobs", spy_host_jobs)
monkeypatch.setattr(app_module, "llm_jobs", spy_llm_jobs)
db = str(tmp_path / "rate-windows.sqlite3")
store = SampleStore(db)
settings = Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=(Target("deskwork", DEAD_URL),),
gufo_url="http://deskwork:8731", vllm_url="http://driveripper:8000",
llm_interval=llm_interval, rate_window=rate_window)
# `build_hub` only wires jobs; nothing here dials, so the shared client is never started
# and there is no event loop to close one in.
hub, _nut_monitor = app_module.build_hub(settings, store, httpx.AsyncClient())
store.close()
assert sorted(seen) == ["host", "llm"], "a source stopped being handed an estimator"
assert sorted(hub.jobs) == ["disks:deskwork", "host:deskwork", "llm:gufo", "llm:vllm"]
assert seen["llm"] is not seen["host"], \
"one estimator for both cadences is the bug this phase exists to fix"
assert seen["llm"].span == pytest.approx(llm_interval), \
"the LLM window must track LLM_INTERVAL_SECONDS, not a second knob"
assert seen["host"].span == pytest.approx(rate_window), \
"the host rates keep RATE_WINDOW_SECONDS: a 2 s span over a 10 s poll is `n/a` forever"
# …and the smaller window still carries the production floor, so the phase-13 refusal of the
# poll loop's ~0.2 s catch-up gap is not a consequence of the wider span.
assert seen["llm"].min_window == MIN_WINDOW_SECONDS
assert seen["host"].min_window == MIN_WINDOW_SECONDS
assert seen["llm"].span > MIN_WINDOW_SECONDS, "the LLM window would refuse every poll"
def test_config_reports_the_window_the_llm_rates_are_actually_measured_over(
client: TestClient) -> None:
"""`/api/config`'s llm window is a measurement claim, so it is the LLM's own span.
It used to echo `RATE_WINDOW_SECONDS` for both sources, which was harmless while one estimator
served them all. Since phase 23 that same field would have advertised a 10 s window beside
rates measured over 2 s — a number the page can print and no poll ever used (rule 2).
"""
llm = client.get("/api/config").json()["llm"]
assert llm["rate_window_seconds"] == llm["interval_seconds"] == 2.0
def wait_for_state(client: TestClient, path: str, *, timeout: float = 5.0) -> dict[str, Any]:
deadline = time.time() + timeout
while time.time() < deadline:
response = client.get(path)
if response.status_code == 200:
return response.json()
time.sleep(0.1)
pytest.fail(f"{path} never answered")
def test_target_problems_are_surfaced_not_swallowed(client) -> None:
body = client.get("/api/config").json()
assert body["system"]["target_problems"] == []
# ---- the credential the server refuses to start without (phase 11) --------- #
def test_no_verifier_and_no_seed_means_the_server_does_not_come_up(tmp_path: Path) -> None:
"""Fail closed, at the one place a mistake is unmissable: startup.
Task 03 is what demands a session on every route; this is the half before it — a deployment
with neither a stored `auth.verifier` row nor `DASH_ADMIN_PASSWORD` must not run open, so
the lifespan raises instead of publishing a context, starting the pollers, or serving a
page. Asserting that nothing was written is part of it: "refused" that seeded a row on the
way out would leave the next start half-configured.
"""
db = str(tmp_path / "no-credential.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=""), store)
try:
with pytest.raises(RuntimeError) as caught, TestClient(app):
pytest.fail("the lifespan yielded with no admin credential")
message = str(caught.value)
assert "DASH_ADMIN_PASSWORD" in message, "the operator's fix is in the exit output"
assert "auth.verifier" in message and "DELETE FROM meta" in message
assert not hasattr(app.state, "ctx"), "nothing is published, so nothing can serve"
assert store.get_meta("auth.verifier") is None
assert store.get_meta("auth.session_secret") is None
finally:
store.close()
def test_the_same_server_starts_once_the_environment_seeds_a_password(tmp_path: Path) -> None:
"""The other side of the gate, so the failure above cannot be satisfied by a broken app.
With a seed the process comes up, the state hangs off the context (task 03 reads it on
every request), and both rows are durable — which is also what lets the operator delete
`DASH_ADMIN_PASSWORD` from the deployment after the first boot.
"""
db = str(tmp_path / "seeded.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
try:
with TestClient(app) as client:
assert client.get("/api/health").status_code == 200 # public: the healthcheck
state = app.state.ctx.auth
assert state is not None
assert state.seeded is True and state.env_ignored is False
assert state.iterations == FAST_PBKDF2
assert auth.verify_password(ADMIN_PASSWORD, state.verifier) is True
assert store.get_meta("auth.verifier") == state.verifier
assert store.get_meta("auth.session_secret") == state.secret
assert ADMIN_PASSWORD not in state.verifier + state.secret
finally:
store.close()
# ---- pages ---------------------------------------------------------------- #
@pytest.mark.parametrize("path,title", [
("/", "overview"), ("/system", "system"), ("/disks", "disks"), ("/llm", "llm"),
("/ups", "ups"), (GATEWAY_PAGE, "gateway"),
])
def test_pages_serve_and_declare_themselves(client, path, title) -> None:
response = client.get(path)
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert title in response.text.lower()
def test_no_page_loads_anything_from_the_internet(client) -> None:
"""No CDN, no webfonts: this dashboard has to work with no route to the internet.
`/login` is not in `MARKUP_PAGES` and cannot be: this `client` is signed in, a signed-in
`GET /login` is a `303`, and following it would audit `/`'s bytes under the wrong name. The
same fragments are checked against the real login page, unsigned, by
`tests/test_auth_api.py::test_the_login_page_loads_nothing_from_the_internet`.
"""
for path in MARKUP_PAGES:
html = client.get(path).text
for fragment in ("//cdn.", "https://fonts.", "//unpkg.com", "//cdnjs"):
assert fragment not in html, f"{path} references {fragment}"
def test_static_assets_are_served(client) -> None:
for asset in ASSETS:
assert client.get(asset).status_code == 200, asset
# ---- /settings: the account page (phase 11 task 06) ----------------------- #
#
# `/settings` is the operator's own surface: the session this browser holds, a sign-out, and the
# one form that changes the admin password. Its HTTP surface is tested twice on purpose — the
# route-enumeration test in `tests/test_auth_api.py` proves nothing outside the public list
# answers anonymously (which is how `/settings` is gated for free by being in `PAGES` in
# `app.py`), and the assertions here pin the **page**: the form fields a password manager needs,
# the outcome line that is a live region, the precedence sentence that stops the "I edited the
# env file and nothing happened" support call, and the masthead link that is the only way in.
def test_the_settings_page_carries_the_session_and_the_form(client) -> None:
response = client.get(SETTINGS_PAGE)
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
html = response.text
# the session panel and its way out
for fragment in ('id="sign-out"', 'id="session-user"', 'id="session-expires"',
'id="session-left"'):
assert fragment in html, f"/settings has no {fragment}"
# the change-password form: three fields, each labelled, each with the floor spelled out,
# and the autocomplete values a password manager needs to fill the right one
for fragment in ('id="current-password"', 'autocomplete="current-password"',
'id="new-password"', 'autocomplete="new-password"',
'id="confirm-password"', 'id="change-password"',
'<label for="current-password">', '<label for="new-password">',
'<label for="confirm-password">', 'aria-describedby="password-hint"'):
assert fragment in html, f"/settings' form is missing {fragment}"
assert "8 characters" in html, "the form stopped naming the length floor it enforces"
# the outcome line is announced, because the operator asked for it and nothing else will
assert 'id="settings-result"' in html and 'role="status"' in html
def test_the_settings_page_states_the_precedence_rule(client) -> None:
"""The sentence that makes the env file's losing half legible on the page that wins.
`README.md` documents the same rule, but the operator reading it there is deciding what to
write in a file; the operator reading *this* page just changed a password and needs to know
why a container env will not undo it.
"""
html = client.get(SETTINGS_PAGE).text
assert "DASH_ADMIN_PASSWORD" in html
assert "auth.verifier" in html
assert "never overwrites" in html
def test_the_settings_page_is_not_one_of_the_dashboards(client) -> None:
"""The tabs are there to leave with; nothing marks itself current, because nothing is.
A tab marked current on a page that is not in the tab list claims a location the app does not
have, and the six dashboards' own masthead pin (`aria-current` exactly once) relies on that
marker meaning something.
"""
html = client.get(SETTINGS_PAGE).text
assert '<nav class="tabs"' in html
assert 'aria-current="page"' not in html
for path in PAGES:
assert f'href="{path}"' in html, f"/settings cannot navigate to {path}"
# The fleet badges belong here too (`TODO.md:3`): the container is what made them appear on
# the six dashboards, and a page that renders the masthead without it is the page where they
# "disappear". `Dash.initChrome()` fills it from `/api/health` exactly as it fills the others.
assert 'id="nav-status"' in html, "/settings lost the fleet badges the operator asked for"
def test_the_settings_page_refuses_a_caller_with_no_session(tmp_path: Path) -> None:
"""One page-level assertion of the gate, not a second enumeration of it.
`tests/test_auth_api.py::test_every_route_that_is_not_public_requires_a_session` is the
general proof; this one exists so a regression in `/settings` names `/settings`.
"""
db = str(tmp_path / "settings-gate.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
try:
with TestClient(app, follow_redirects=False) as anon:
page = anon.get(SETTINGS_PAGE, headers={"accept": "text/html"})
assert page.status_code == 303
assert page.headers["location"] == f"/login?next={SETTINGS_PAGE}"
data = anon.get(SETTINGS_PAGE, headers={"accept": "application/json"})
assert data.status_code == 401
assert data.json() == {"ok": False, "detail": "sign in required",
"login": "/login"}
finally:
store.close()
def test_every_page_links_to_both_admin_pages(client, tmp_path: Path) -> None:
"""`⚙ Account` and `⚷ Certs` are the only way to the two admin pages, so they are everywhere.
One loop over all eight signed-in pages (`MARKUP_PAGES`), because the control row is the *same*
on all of them: `⚷ Certs` → `⚙ Account` → theme toggle → clock, in `.masthead-right` and
**outside** `.tabs` (the tab strip's structure — six dashboards, one of them current — is pinned
by `test_the_disks_tab_is_in_the_masthead_of_every_page`, and a seventh entry with no
`aria-current` would break what that marker means).
Both links are on the pages they name, which **inverts** phase 15's "no self-link"
precedent on the operator's decision (`TODO.md:4` — "clicking the certs tab causes the certs
button to disappear … These should remain visible"): masthead chrome that changes shape
depending on where you already are reads as a broken control, while a link to the page you
are on is a no-op that answers with the page you were looking at.
The clock last is phase 25 task 03 (`TODO.md:5` — "The time should show up as the farthest
right item on the navbar"). It is pinned as **markup**, not as a CSS `order` property, because
that is what makes the claim true: task 02 left the control row `justify-content: flex-end`, so
the last child of `.masthead-right` *is* the farthest-right item, and an `order:` override the
pin could not see would leave this test green over a bar that says something else. (Below 900 px
the clock is hidden by the long-standing `.clock` media rule — a readout, not a control, so
hiding it is not the "no control is hidden to fit the width" rule this app keeps — and the
markup order this test reads is width-independent.)
"""
account_open = '<a class="ghost account" href="/settings"'
certs_open = '<a class="ghost account" href="/certs"'
# the opening tag **and** its label: the claim is the cluster does not change shape between
# pages, and `/certs` shipped a theme toggle missing its `aria-pressed` for exactly the
# reason nothing ever compared the seven markups
account = f'{account_open} title="Session and password">⚙ Account</a>'
certs_link = f'{certs_open} title="Agent certificates">⚷ Certs</a>'
theme = '<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">'
# The other half of the clock claim, from the stylesheet side: no `order:` on anything in the
# bar, because a re-ordering property would leave this markup audit describing a bar that
# paints in some other order (`data/logs/25-task03-verify.cjs` measures the painted order at
# both widths; this is the served-byte guard under that measurement).
css = _css(client)
for selector in (".masthead", ".masthead-right", "#nav-status", ".clock"):
assert "order" not in _declarations(css, selector), \
f"{selector} grew an `order:` declaration — the clock's place in the row is markup"
for path in MARKUP_PAGES:
html = client.get(path).text
assert certs_link in html, f"{path} has no route to the certificate authority"
assert account in html, f"{path} has no route to the account page"
before_theme = html[:html.index('id="theme-toggle"')]
assert certs_link in before_theme, f"{path} put the Certs link after the theme toggle"
assert account in before_theme, f"{path} put the Account link after the theme toggle"
assert before_theme.index(certs_link) < before_theme.index(account), \
f"{path} orders the two admin controls differently from the dashboards"
# …and the clock after the toggle, i.e. last in `.masthead-right`: on the `flex-end`
# control row that is the farthest-right item on the bar (TODO.md:5).
assert html.index('id="clock"') > html.index('id="theme-toggle"'), \
f"{path} does not end its control row with the clock"
tabs = html[html.index('<nav class="tabs"'):html.index("</nav>")]
assert "/certs" not in tabs, f"{path} made Certs a dashboard tab"
assert "/settings" not in tabs, f"{path} made Account a dashboard tab"
assert theme in html, f"{path}'s theme toggle no longer matches the other seven"
# The one page that renders neither, and its own client: `client` is signed in, and a
# signed-in `GET /login` is a `303` to the dashboard, which would audit `/` and call it
# `/login` (the trap documented at `test_the_login_page_carries_no_nav_and_no_toggle`).
# Pre-auth there is nothing to administer, so the masthead stays brand link + theme toggle.
db = str(tmp_path / "admin-links-login.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
try:
with TestClient(app) as anon:
response = anon.get("/login")
assert response.status_code == 200, \
"a signed-out /login must be the form, not a bounce"
login_html = response.text
finally:
store.close()
# the opening tag, so an `href` under any other class or label still fails here
for link in (certs_open, account_open):
assert link not in login_html, f"/login offers {link} to a caller who cannot use it"
def test_the_certs_page_is_a_page_and_not_a_dashboard(client) -> None:
"""`/certs` borrows the dashboard chrome and none of the dashboard meaning (task 15.05).
Six tabs with nothing current, the five fleet badges like every other masthead (phase 25:
the badges report the fleet, and that report is the point of the site on this page too), and
the same head and stylesheet as every other page — this is the
`test_the_settings_page_is_not_one_of_the_dashboards` assertion for the second admin page,
and it is what a tab in the strip for the certificate authority would fail on. (Phase 29
renamed it: `/gateway` **is** the sixth dashboard now, so "sixth" no longer means "not a
dashboard".)
"""
html = client.get(CERTS_PAGE).text
assert html.count('aria-current="page"') == 0
tabs = re.findall(r'<a href="([^"]+)"[^>]*aria-current="page"', html)
assert tabs == [], f"/certs marked a tab current: {tabs}"
assert 'id="nav-status"' in html, "/certs lost the fleet badges the operator asked for"
for path in PAGES:
assert f'href="{path}"' in html, f"/certs cannot navigate to {path}"
assert '/static/dash.css' in html
def test_the_certs_page_carries_the_authority_and_the_agents_table(client) -> None:
"""The three blocks the page is: the root, the client line, and one row per host.
Pinned on the served bytes rather than on a browser: the root and client panels and the table
are markup, and the two rows of the table body are built from `GET /api/certs` by
`page-certs.js` — so the per-row action labels are asserted on the served **script**, the way
`/ups` pins its command buttons. `tests/test_api.py` has no JS runner (a locked anchor), and a
label that shipped in neither file would be an invisible button either way.
"""
html = client.get(CERTS_PAGE).text
js = client.get("/static/page-certs.js").text
assert "Root CA" in html and 'id="root-serial"' in html
assert 'id="root-expires"' in html and 'id="root-left"' in html
assert "Rotate root CA" in html, "the CA-wide action disappeared"
assert "Dashboard client" in html and 'id="client-serial"' in html
assert 'id="client-expires"' in html, "/certs lost the client expiry"
assert 'id="agents-table"' in html and "<tbody></tbody>" in html
assert 'id="agents-empty"' in html and "hidden" in html
for label in ("Rotate", "Revoke", "bundle"):
assert f"'{label}'" in js, f"the {label} action lost its label"
for route in ("/rotate", "/revoke", "/bundle"):
assert route in js, f"the page no longer reaches {route}"
assert "/api/certs/root/rotate" in js
def test_the_certs_agents_table_scrolls_to_its_last_column(client) -> None:
"""`#agents-table` carries the `.scroll` modifier, and the stylesheet still styles it.
`TODO.md:9`: at phone width the serial, expiry, `san` and controls columns were clipped by
`.card { overflow: hidden }` with no way to reach them. `dash.css` turns `table.data` into
a scroll container through the `scroll` class, so the class and the rule are one mechanism
and are pinned together — the phase-18 idiom of comparing the two places a contract lives,
because a rename on either side silently un-scrolls the table.
(Phase 27 task 01 added a second door: at ≤640px *every* `table.data` scrolls horizontally.
That does not retire this pin — `.scroll` out-ranks it, two classes to one, so this table's
both-axis box and its 320 px height cap are still decided here, on a phone included.)
"""
html = client.get(CERTS_PAGE).text
assert '<table class="data scroll" id="agents-table">' in html, \
"the agents table lost its .scroll modifier — the controls column is unreachable at 360px"
css = client.get("/static/dash.css").text
start = css.index("table.data.scroll {")
rule = css[start:css.index("}", start)]
assert "overflow: auto" in rule, "table.data.scroll no longer scrolls — the class names nothing"
def test_the_certs_page_prints_the_names_the_authority_mints(client) -> None:
"""The CN of each certificate is markup, so the literal is pinned to `certs.py`.
`MIN_PASSWORD_LENGTH`'s idiom: there is no build step to share a Python constant with a page,
so a page that restates one says it in markup and a test compares the two. Printing a CN the
module did not sign with would be the page inventing a fact about a stored certificate.
"""
html = client.get(CERTS_PAGE).text
assert f">{certs.ROOT_CN}<" in html, "the root panel's CN is not what certs.py signs"
assert f">{certs.CLIENT_CN}<" in html, "the client line's CN is not what certs.py signs"
def test_the_certs_page_reads_only_fields_the_cert_api_publishes(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""End to end, in both directions: the page's column table and `GET /api/certs` are one shape.
The served script's `COLUMNS` entries name every payload key the page indexes, so this is the
`SLOT_FIELDS` device on the surface where a rename is worst: a `deployed_bundle_rev` that
quietly became `agent_bundle_rev` would print `n/a` beside a healthy fleet forever.
"""
script = "/static/page-certs.js"
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(3),
host_interval=3600.0) as client:
js = client.get(script).text
names: set[str] = set()
for listed in re.findall(r"fields: \[([^\]]*)\]", js):
names.update(re.findall(r"'([^']+)'", listed))
assert names == CERT_AGENT_KEYS, sorted(names ^ CERT_AGENT_KEYS)
client.post("/api/certs/deskwork/rotate")
for row in cert_agent_rows(client):
assert len(row) == 8
for name in names:
assert name in row, f"the page indexes `{name}` and the API does not publish it"
# …and the two authority blocks: the page prints a serial and an expiry for each, which is
# exactly the key set the API answers there.
body = cert_document(client)
for block in ("root", "client"):
assert set(body[block]) == CERT_TIMESERIAL_KEYS
for name in CERT_TIMESERIAL_KEYS:
assert f".{name}" in js, f"the page stopped reading `{name}`"
def test_the_certs_page_never_colours_a_certificate(client) -> None:
"""No severity vocabulary reaches this page at all — an expiry is a date, not a verdict.
`/api/certs` sends no `severity` anywhere (task 04, and `severity.py` rates no certificate),
so a page that coloured an expiring cert would be inventing a threshold in JavaScript. The one
colour on the page is the payload's `status` word through a word→class map, and `unissued`
maps to no class at all — a certificate that does not exist must not borrow green.
"""
js = _code(client.get("/static/page-certs.js").text)
assert "severity" not in js, "/certs grew a verdict the API does not send"
assert "severityFrom" not in js and "severityFor" not in js
table = js[js.index("const STATUS_CHIP = {"):js.index("};")]
assert "active: 'ok'" in table and "revoked: 'fail'" in table
assert "unissued: ''" in table, "an unissued host would be coloured"
assert "not_after" not in table and "serial" not in table, "an expiry became a verdict"
def test_the_certs_page_asks_before_it_writes_and_offers_nothing_the_api_refuses(
client) -> None:
"""Every write is confirmed, and no row offers a control its own payload rules out.
`confirm()` and not the typed `DANGER` word: that gate belongs to commands that cut power,
and this page has none — the worst outcome here is a host that is not dialled until somebody
installs a bundle. The guards are the API's, restated: a row outside `HOST_TARGETS` gets no
buttons at all (`404 {"configured": false}` is all three routes' first line), a never-issued
host has nothing to revoke and no bundle to download, and the bundle download is a plain
`GET` link because a session cookie is all a signed `GET` needs.
"""
js = _code(client.get("/static/page-certs.js").text)
assert "window.confirm(" in js
assert "DANGER" not in js, "the power-cut word migrated to a page with no power cut"
assert "entry.configured" in js and "not in HOST_TARGETS" in js
assert "entry.status !== 'unissued'" in js, "a row could revoke nothing"
assert "entry.serial" in js
assert "bundle.href = '/api/certs/'" in js
assert "method: 'POST'" in js
# …and the refusal is text, never markup: these are server sentences
assert "innerHTML" not in js
def test_the_certs_page_prints_the_restart_sentence_the_api_spoke(client) -> None:
"""`restart_required` prints the route's `hint`, not a sentence the page wrote for itself.
The restart fact lives in `api.py` (the CA and the client pair are startup material, so the
table is new before the socket is). A page copy of it is the version that quietly stops
saying "and every bundle needs re-installing".
"""
js = client.get("/static/page-certs.js").text
assert "restart_required" in js
assert "body.hint" in js
assert "restart the dashboard" not in js.lower(), "the page invented the restart sentence"
def test_the_password_floor_the_form_checks_is_the_one_the_api_enforces(client) -> None:
"""The client-side length check is a courtesy; the server's is the rule.
There is no build step to share the constant (a locked anchor), so the two literals are
pinned together here: a floor raised in `api.py` alone would leave the form promising eight
characters to a request the server answers `422`.
"""
js = client.get("/static/page-settings.js").text
found = re.search(r"const MIN_PASSWORD_LENGTH = (\d+);", js)
assert found, "page-settings.js no longer names its length floor"
assert int(found.group(1)) == MIN_PASSWORD_LENGTH
def test_a_rotated_secret_clears_the_cached_session_every_page_uses(client) -> None:
"""`Dash.resetAuth()` is the difference between a password change and a 403 dashboard.
`POST /api/auth/password` rotates the signing secret on purpose (`auth.update_verifier`),
which invalidates the CSRF token `ensureAuth()` memoised. Every page writes through
`fetchJSON`, which reads that cache — so unless the change clears it, the operator changes
the password, clicks *beeper disable* on `/ups`, and gets `missing or invalid CSRF token`
until they reload the page. A bounce on a `401` clears it for the same reason: the session it
described no longer exists.
"""
app_js = client.get("/static/app.js").text
assert "function resetAuth(" in app_js
exports = app_js.rsplit("return {", 1)[1].split("}", 1)[0]
assert re.search(r"\bresetAuth\b", exports), "Dash.resetAuth is not exported"
reset = app_js[app_js.index("function resetAuth("):app_js.index("function bounceToLogin(")]
for field in ("loaded: false", "authenticated: false", "csrf: null"):
assert field in reset, f"resetAuth() left `{field}` cached"
assert "authPromise = null" in reset, \
"resetAuth() left an in-flight status call memoised — the fresh write would reuse it"
bounce = app_js[app_js.index("function bounceToLogin("):app_js.index("const READ_METHODS")]
assert "resetAuth()" in bounce, "a 401 bounce kept the dead session's token cached"
js = client.get("/static/page-settings.js").text
assert js.count("Dash.resetAuth()") >= 2, \
"the settings page must reset the cached session after both a change and a sign-out"
assert "textContent" in js and "innerHTML" not in js, \
"/settings renders a server sentence as markup"
# ---- the severity language in the static assets --------------------------- #
#
# There is no JS test runner (a locked anchor: adding one would mean a build step).
# What *can* be asserted end-to-end, from the served bytes, is the contract the pages
# depend on: the hero style exists, its colour comes from the shared ok/warn/fail tokens,
# no parallel `.sev-*` vocabulary appeared, and `Dash.hero()` reads the verdict instead of
# comparing the number to a threshold in JavaScript.
ASSETS = ("/static/dash.css", "/static/app.js", "/static/charts.js",
"/static/page-index.js", "/static/page-disks.js", "/static/page-system.js",
"/static/page-llm.js", "/static/page-ups.js", "/static/page-login.js",
"/static/page-settings.js", "/static/page-certs.js", "/static/page-gateway.js")
def _css(client: TestClient) -> str:
return client.get("/static/dash.css").text
def _css_section(css: str, name: str) -> str:
"""One `/* ---- name … */` section of the stylesheet, up to the **next** section header.
Slicing to a named neighbour worked while the sheet had five sections. Every page has since
added a component section, and a slice that runs across someone else's rules accuses the
fleet block of colouring numbers it never coloured — so the boundary is structural: the end
of *this* section, whoever comes next.
"""
start = css.index(f"/* ---- {name}")
return css[start:css.index("/* ----", start + 10)]
def _hero_css(css: str) -> str:
"""Just the `hero numbers` section of the stylesheet."""
return css.split("/* ---- hero numbers", 1)[1].split("/* ---- bars", 1)[0]
def test_css_ships_the_hero_number_style(client) -> None:
css = _css(client)
hero_block = _hero_css(css)
for rule in (".hero {", ".hero .label", ".hero .value", ".hero .unit", ".hero .sub",
".hero.ok .value", ".hero.warn .value", ".hero.fail .value",
".hero-bar", ".hero-bar.ok .bar"):
assert rule in hero_block, f"dash.css lost the {rule} rule"
# a display size with fixed-width digits, so the number does not jitter between polls
assert "2.4rem" in hero_block
assert "tabular-nums" in hero_block
def test_hero_colour_comes_from_the_shared_verdict_tokens(client) -> None:
"""`.hero` reuses `--ok/--warn/--fail`; it does not invent a palette of its own."""
hero_block = _hero_css(_css(client))
for token in ("var(--ok)", "var(--warn)", "var(--fail)", "var(--text)",
"var(--text-muted)", "var(--text-faint)"):
assert token in hero_block, f"the hero style no longer uses {token}"
def test_no_stylesheet_rule_uses_an_undeclared_token(client) -> None:
"""A typo'd `var(--x)` renders unstyled and reads as a broken panel, not an error."""
css = _css(client)
root = re.search(r":root\s*\{(.*?)\n\}", css, re.S)
assert root is not None
declared = set(re.findall(r"(--[a-z0-9-]+)\s*:", root.group(1)))
used = set(re.findall(r"var\(\s*(--[a-z0-9-]+)", css))
assert not used - declared
def _code(body: str) -> str:
"""An asset with its comments stripped — prose may *name* a class the code must not define."""
body = re.sub(r"/\*.*?\*/", " ", body, flags=re.S)
body = re.sub(r"<!--.*?-->", " ", body, flags=re.S)
return re.sub(r"^\s*//.*$", " ", body, flags=re.M)
# The `fleet` object paths `/llm`'s aggregate block reads. `engines_total` drives the
# "hidden below two engines" rule and the tag; `engines_live` is what makes a partial sum
# visible; the rest are the row itself.
FLEET_BLOCK_PATHS = frozenset({
"engines_total", "engines_live", "requests_active", "requests_waiting",
"prompt_tokens_per_second", "decode_tokens_per_second", "tokens_total",
})
# The summed half of that list — the ones that must be null, not 0, with nothing answering.
FLEET_SUM_PATHS = ("requests_active", "requests_waiting", "prompt_tokens_per_second",
"decode_tokens_per_second", "tokens_total")
def _fleet_tag(html: str) -> str:
"""The aggregate block's opening tag — the one place its `hidden` default lives."""
start = html.index('<section class="card flat fleet"')
return html[start:html.index(">", start)]
def _fleet_render_path(js: str) -> str:
"""Just the body of `renderFleet()` — the aggregate block's whole code path, comments out.
Everything the block writes goes through here, so it is the slice that can be asserted
verdict-free without tripping over the engine panels' rated KV sparkline next door.
"""
js = _code(js)
return js[js.index("function renderFleet("):js.index("async function refresh(")]
def _fleet_field_metrics(js: str) -> list[str]:
"""The `fleet` keys the block's rows are bound to, in the order the block prints them."""
js = _code(js)
start = js.index("const FLEET_FIELDS = [")
table = js[start:js.index("];", start) + 2]
return re.findall(r"metric: '([a-z_]+)'", table)
def test_no_parallel_severity_vocabulary_exists_anywhere(client) -> None:
"""One word per colour: `ok|warn|fail`, as in severity.py and the existing components."""
for asset in (*ASSETS, *MARKUP_PAGES):
body = _code(client.get(asset).text)
assert ".sev-" not in body, f"{asset} invented a .sev-* class"
assert not re.search(r"classList\.(?:add|toggle)\(\s*[\"'](?:danger|good|warning)[\"']",
body), f"{asset} invented a synonym for fail"
def test_app_js_exposes_the_hero_helpers(client) -> None:
js = client.get("/static/app.js").text
assert "function hero(" in js
assert "function severityFrom(" in js
exports = js.rsplit("return {", 1)[1].split("}", 1)[0]
for name in ("hero", "severityFrom", "severityFor"):
assert re.search(rf"\b{name}\b", exports), f"Dash.{name} is not exported"
def test_hero_reads_the_verdict_instead_of_a_threshold(client) -> None:
"""`hero()` may only colour what the API rated — no thresholds re-derived in JS."""
js = client.get("/static/app.js").text
start = js.index("function hero(")
body = js[start:js.index("\n function setDot", start)]
assert "severityFrom(" in body
assert "severityFor(" not in body
# an unknown value gets n/a and no colour class at all
assert "naText" in body
assert re.search(r"raw != null", body)
def test_severity_for_is_kept_as_the_documented_fallback(client) -> None:
"""Every page call site is gone now that phase 05 converted `/ups`.
`/` was converted in phase 02, `/system` in phase 03, `/llm` in phase 04 and `/ups` in
phase 05, so no page colours anything from a threshold it wrote itself. The function
itself stays: it is the documented fallback `setBar()` uses when a caller names no
verdict, and a page that ever needs a verdict the API does not send should add it to
`severity.py` rather than here — which is what the docstring on it says.
"""
js = client.get("/static/app.js").text
assert "FALLBACK ONLY" in js
assert "severityFor" in js, "the documented fallback itself was deleted"
for page in PAGE_SCRIPTS.values():
assert "severityFor" not in client.get(f"/static/{page}").text, \
f"{page} colours something from a threshold of its own"
def test_overview_colours_from_the_payload_not_from_js_thresholds(client) -> None:
"""`page-index.js` reads verdicts; it does not compare a number to a bound."""
js = client.get("/static/page-index.js").text
assert "severityFrom" in js
assert "severityFor" not in js
def test_system_page_colours_from_the_payload_not_from_js_thresholds(client) -> None:
"""`page-system.js` hero cards paint payload verdicts, not thresholds of their own.
Phase 03 task 02's promise: every colour on a host card comes from a `severity` dict
`severity.py` produced (`cpu.severity.usage_pct`, `memory.severity.used_pct`,
`disks.severity.worst_used_pct`, `cpu.severity.temp_c`).
A threshold written here could disagree with the overview's alerts; a hero with no
verdict prints `n/a` uncoloured, which is `Dash.hero()`'s contract.
The GPU slot names no verdict on purpose (phase 06): `severity.THRESHOLDS` has no
`gpu.*` row, the payload carries no `gpus[].severity`, and the hero is built without
`severity` or `bar` — a bar under a verdictless number would be a verdict by shape.
"""
js = client.get("/static/page-system.js").text
assert "severityFor(" not in js, "the system page re-invented a threshold"
assert "Dash.hero(" in js
for hook in ("cpu.severity.usage_pct", "memory.severity.used_pct",
"disks.severity.worst_used_pct", "cpu.severity.temp_c"):
assert hook in js, f"the card no longer names {hook} as a colour source"
# and the GPU hero grades nothing: no verdict hook, no utilisation bar
assert "gpus[].severity" not in js, "the GPU slot invented a colour source again"
gpu_slot = js[js.index("function renderGpuSlot("):js.index("function renderGpuExtra(")]
assert "severity:" not in gpu_slot, "the GPU hero passes a severity option again"
assert "bar:" not in gpu_slot, "the GPU hero grew a bar back (a verdict by shape)"
assert "hero-bar" not in gpu_slot, "the GPU hero rebuilt the bar variant of .hero"
#: Every cell of the `/system` GPU card's value grid, in render order, with the `gpus[]`
#: key(s) it prints. This is the `SLOT_FIELDS` device applied to the four values phase 21
#: task 02 created: the table is read out of the served `GPU_VALUE_FIELDS` and resolved
#: against every captured host shape *and* against `/api/system/snapshot`, so a rename on
#: either side fails a test instead of printing `n/a` beside a healthy fleet — and so the
#: dense ` · `-joined sub-line the TODO complained about cannot come back.
GPU_VALUE_CELLS: tuple[tuple[str, tuple[str, ...]], ...] = (
("Model", ("model",)),
("VRAM", ("vram_used_bytes", "vram_total_bytes")),
("Pool", ("mem_used_pct",)),
("Temp", ("temp_c",)),
)
def _gpu_value_fields(js: str) -> list[tuple[str, tuple[str, ...]]]:
"""The served `GPU_VALUE_FIELDS` table as `(label, payload keys)` rows."""
table = re.search(r"const GPU_VALUE_FIELDS = \[(.*?)\n\];", js, re.S)
assert table, "page-system.js no longer has a GPU_VALUE_FIELDS table"
rows: list[tuple[str, tuple[str, ...]]] = []
for entry in re.findall(r"\{[^{}]*\}", table.group(1)):
label = re.search(r"label: '(\w+)'", entry)
field = re.search(r"field: '(\w+)'", entry)
assert label and field, f"a GPU value cell names no label or field: {entry!r}"
total = re.search(r"total: '(\w+)'", entry)
rows.append((label.group(1),
(field.group(1), total.group(1)) if total else (field.group(1),)))
return rows
def test_gpu_values_are_four_labelled_cells_not_a_dense_sub_line(client) -> None:
"""`TODO.md:12` — "… is too much info for such small text and should be its own values."
The GPU slot used to print `Radeon 8060S Graphics · VRAM 180.2 MiB / 512.0 MiB · pool
79% · 69 °C` as one `.sub` line in 11px mono. It is now four labelled cells, and the
pin is bidirectional: the table has the four rows in order, and the joined-sentence
builder that produced the TODO's quoted string is gone (`gpuSub` and its
`bits.join(' · ')` are the thing that made the card unreadable).
Two invariants ride on the same slice. The model string is upstream text (a PCI device
name), so it leaves through `DashCharts.escapeHtml`; and the temperature keeps its
null guard — `0 °C` would be a cool GPU that does not exist (phase 06).
"""
js = client.get("/static/page-system.js").text
assert _gpu_value_fields(js) == list(GPU_VALUE_CELLS), \
"the GPU value grid is no longer Model / VRAM / Pool / Temp over those payload keys"
# the °C rides in the value (the label is the word, as on the /disks card and the GPU
# detail table), and it appears exactly once in the table: one cell, one unit.
assert js.count("unit: ' °C'") == 1, "the Temp cell's unit is not the one °C on the card"
body = js[js.index("function gpuValues("):js.index("function renderGpuSlot(")]
assert "GPU_VALUE_FIELDS.map(" in body, "gpuValues() stopped rendering the table"
assert "escapeHtml" in body, "the Model cell prints an upstream string unescaped"
assert "== null" in body, "a cell lost its null guard (missing must print n/a, not 0)"
# the dense sub-line the TODO quoted is gone, container and all
assert "function gpuSub(" not in js, "gpuSub() came back"
assert "join(' · ')" not in js, "the ` · `-joined GPU footnote came back"
# "More detail" keeps the per-label breakdown: a surface change, not a de-duplication
extra = js[js.index("function renderGpuExtra("):js.index("function renderHostCard(")]
assert "Temp" in extra and "gpu.temp_c" in extra, "GPU detail lost its temperature column"
def test_gpu_value_cells_resolve_against_every_captured_host(
agent_snapshots: dict[str, dict[str, Any]]) -> None:
"""Every key the four cells print exists on every captured card — discrete GPU and APU.
`tests/card_contract.py` owns the payload table; this resolves the *page's* table
against the same three captures, which is where a rename would otherwise be caught by
an operator looking at `n/a` next to a healthy machine. `temp_c` may legitimately be
`None` (a driver with no hwmon input) — present is the contract, and the cell prints
`n/a` for it, never `0 °C`.
"""
for name in HOST_SHAPES:
payload = parse_host_snapshot(name, agent_snapshots[name], HostCollector(
RateEstimator(span=60.0)), now=1_000.0).payload
assert_card_contract(payload)
for gpu in payload["gpus"]:
for label, keys in GPU_VALUE_CELLS:
for key in keys:
assert key in gpu, f"{name}: the {label!r} cell has no {key!r} key"
if not payload["gpus"]:
# gateway: the omission's input. An empty list is a *fact* about the box, and
# `renderGpuSlot()` turns that fact into an absent slot rather than an `n/a`.
assert name == "gateway", "a capture lost its GPU row"
def test_the_gpu_value_cells_are_served_for_the_hosts_that_have_a_card(
tmp_path: Path, agent_snapshots: dict[str, dict[str, Any]]) -> None:
"""The same four keys over HTTP, on the body `/system` actually renders.
Parsing in-process proves the source; this proves the endpoint. One app with all three
hosts injected (dead ports, so no poll can overwrite what the test put there) answers
with the discrete-GPU card, the APU card, and gateway's empty list — the exact union
`renderGpuSlot()` branches on: cards present means four labelled values per card, `[]`
means no slot at all.
"""
db = str(tmp_path / "gpu-cells.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=tuple(
Target(name, DEAD_URL) for name in HOST_SHAPES)), store)
test_client = TestClient(app)
test_client.__enter__()
try:
sign_in(test_client)
hub = app.state.ctx.hub
for name in HOST_SHAPES:
hub.jobs[f"host:{name}"].state.payload = parse_host_snapshot(
name, agent_snapshots[name], HostCollector(RateEstimator(span=60.0)),
now=1_000.0).payload
hosts = {row["name"]: row for row in test_client.get("/api/system/snapshot").json()["hosts"]}
assert set(hosts) == set(HOST_SHAPES)
for name, row in hosts.items():
for gpu in row["gpus"]:
for label, keys in GPU_VALUE_CELLS:
for key in keys:
assert key in gpu, f"{name}: /api/system/snapshot has no {key!r} " \
f"for the {label!r} cell"
assert hosts["gateway"]["gpus"] == [], \
"gateway must answer [] — that is what makes the page omit the slot"
assert hosts["driveripper"]["gpus"] and hosts["deskwork"]["gpus"]
finally:
test_client.__exit__(None, None, None)
store.close()
def test_a_host_with_no_gpu_omits_the_gpu_slot_instead_of_printing_n_a(client) -> None:
"""`TODO.md:12` — "Boxes without a GPU should not report N/A but instead should omit
these values."
gateway has no card, and the sixth hero slot used to say so with a big faint `n/a` plus
the sentence "No GPU reported by this agent" — invariant 3's complaint in the one place
the page had not learned it. `renderGpuSlot()` now empties the slot and sets `hidden`,
and `[hidden] { display: none !important }` at the end of `dash.css` is what takes the
wrapper out of the `.heroes` grid so the row reflows to five. Pinned on the served
bytes (no JS runner — a locked anchor) and measured in a browser by
`data/logs/21-task02-gpu-values.cjs`.
The wrapper element itself stays in `hostCard()`: the hero row remains one static list
of six `data-k` slots (pinned by the test below), a host whose agent starts reporting a
card has something to grow into, and an empty hidden element is zero footprint.
"""
js = client.get("/static/page-system.js").text
slot = js[js.index("function renderGpuSlot("):js.index("function renderGpuExtra(")]
assert "cell.hidden = gpus.length === 0" in slot, \
"the GPU slot no longer hides itself when the host has no card"
assert "No GPU reported" not in js, "the no-GPU n/a sentence came back"
assert "n/a" not in slot, "the GPU slot grew its own n/a fallback again"
# and the row itself keeps the six static slots, so a GPU host still has somewhere to go
assert "display: none !important" in client.get("/static/dash.css").text, \
"[hidden] stopped beating .hero-slot's display, so a hidden slot would still " \
"occupy a hero track"
def test_gpu_value_grid_ships_no_verdict_and_wraps(client) -> None:
"""The four new cells colour nothing and let a long model string wrap.
Phase 06's rule extends to them: `severity.THRESHOLDS` has no `gpu.*` row, the payload
carries no `gpus[].severity`, so neither the markup nor the grid may reach for
`--ok`/`--warn`/`--fail` or a `severity` option (a busy GPU on an LLM server is the job).
`overflow-wrap` is the other half of `TODO.md:12` — the readability complaint *is* the
20-character model string, and truncating it would keep the complaint.
"""
js = client.get("/static/page-system.js").text
# word-boundary on purpose: the docstring in this slice is allowed to *say* "severity",
# the code is not allowed to pass one (or a bar, or a verdict class) to anything
markup = js[js.index("const GPU_VALUE_FIELDS"):js.index("function renderGpuSlot(")]
verdicts = re.findall(r"\b(ok|warn|fail|severity:|bar:|hero-bar)\b", markup)
assert not verdicts, f"the GPU value grid grew a verdict: {verdicts}"
css = client.get("/static/dash.css").text
rule = re.search(r"\.gpu-values \{([^}]*)\}", css)
assert rule, "/system lost its .gpu-values grid"
assert "auto-fit" in rule.group(1), ".gpu-values stopped adapting to the hero track"
wrap = re.search(r"\.gpu-values \.value \{([^}]*)\}", css)
assert wrap and "overflow-wrap" in wrap.group(1), \
"a long GPU model string truncates instead of wrapping"
for token in ("--ok", "--warn", "--fail"):
assert token not in rule.group(1) + (wrap.group(1) if wrap else ""), \
f"the GPU value grid coloured itself with {token} (severity.py rates no GPU)"
def test_system_host_card_is_six_heroes_without_per_core(client) -> None:
"""Each host card ships six hero slots in order: CPU, MEM, TEMP, DISK, GPU, NET.
Asserted on the served script because there is no JS test runner (a locked anchor).
The two dense `.metrics` rows and the per-core heat bars are exactly what made the
card unreadable, and "no per-core anywhere on /system" is a promise a stray
`renderCores` call would silently break — the series stays stored, the page just
stopped drawing it (PLAN.md *Dropped visuals*).
Six **slots**, not six visible heroes: the GPU slot ships `hidden` and
`renderGpuSlot()` un-hides it only for a host whose payload has a card, so gateway's
card renders five (pinned by the test above and measured in the browser by
`data/logs/21-task02-gpu-values.cjs`). The static order below is what makes that a
one-line rule instead of a re-flowing template.
"""
js = client.get("/static/page-system.js").text
card = js[js.index("function hostCard("):js.index("function renderHostCard(")]
slots = re.findall(r'<div class="(?:hero|hero-slot)[^"]*" data-k="([^"]+)"', card)
assert slots == ["cpu", "mem", "temp", "disk", "gpus", "net"], \
"the hero row is no longer the six numbers in the agreed order"
assert re.search(r'data-k="gpus"[^>]*hidden', card), \
"the GPU slot is visible before the payload says this host has a card"
# per-core visualisation is gone from the card code entirely
assert "cores" not in card, "a per-core visual came back to the card"
assert "renderCores" not in js and "core_usage" not in js
# the NET slot is a slot with two lines and no verdict: a byte rate has no good/bad
assert 'data-k="net"' in card
assert not re.search(r'data-k="net"[^>]*data-sev', card), "the NET slot invented a colour"
# what the glance leaves out stays reachable, collapsed by default
assert "More detail" in card
for key in ("load", "freq", "dr", "dw", "dutil", "uptime", "details"):
assert f'data-k="{key}"' in card, f"{key} left the page instead of the glance"
# the panel owns its charts: the slots live inside the card, not on the page
for chart in ("util", "net"):
assert f'data-chart="{chart}"' in card, f"the panel lost its {chart} chart slot"
def test_system_page_keeps_no_fleet_wide_chart_or_metric_picker(client) -> None:
"""Nothing on `/system` plots two hosts, and nothing needs a selection to make sense.
TODO.md L4 is the instruction ("The system page doesn't need shared charts for
anything") and the overview's alerts + fleet table are what made removing them safe
(phase 02). Asserted on the served bytes because the only thing that would otherwise
notice a resurrected `<select>` is a browser: there is no JS test runner (a locked
anchor), and `test_no_page_carries_an_id_nothing_reads` cannot see ids that were deleted
correctly from the page but are still queried by the script.
"""
html = _code(client.get("/system").text)
for gone in ('id="chart-cpu"', 'id="chart-mem"', 'id="chart-disk"', 'id="chart-net"',
'id="chart-gpu"', 'id="chart-temp"', 'id="chart-detail"', 'id="detail-host"',
'id="detail-metric"', 'id="f-cpu"', 'id="f-load"', 'id="f-gpu"',
"Fleet totals", "Fleet charts", "Series detail", "<select"):
assert gone not in html, f"/system still carries {gone}"
# the two panels that are left are the per-host grid and the event feed
assert html.count("<section") == 2, "/system grew a panel back on top of the host grid"
assert 'id="hosts"' in html and 'id="events"' in html
# and the script must not reach for the deleted endpoints/ids either — a `#detail-host`
# query against a removed node is a silent null, so the picker would just never appear
js = client.get("/static/page-system.js").text
for gone in ("#chart-", "detail-host", "detail-metric", "detail-note",
"renderFleet", "makeCharts", "refreshGpuChart", "populateDetailMetrics"):
assert gone not in js, f"page-system.js still references {gone}"
def test_every_system_chart_is_drawn_inside_its_own_host_panel(client) -> None:
"""One host per chart: the legends of `/system` can never list two machine names.
The construction is a factory that takes the card, so a chart cannot be built against a
page-level id; the metric list is per host, so the GPU streams cannot leak from one
panel into another; and a panel is torn down (with its charts) when its host leaves the
payload, which is the bug class a keyed-by-host chart map exists to avoid.
"""
js = client.get("/static/page-system.js").text
builds = re.findall(r"new DashCharts\.LineChart\(([^,]+),", js)
assert builds, "the system page builds no charts at all"
# every chart is built from a `[data-chart]` slot inside the host card, never from a
# page-level id — which is what makes "one host per chart" structural, not a convention
assert all(re.search(r"(?:card|slot)\.querySelector\(.\[data-chart=", b) for b in builds), \
f"a chart is built outside a host panel: {builds}"
# three construction sites, one per slot (util, net, gpu) — never one per metric
assert len(builds) == 3, f"charts are not built one per panel slot: {builds}"
assert sorted(re.findall(r'data-chart=\\?"(\w+)"', " ".join(builds))) == ["gpu", "net", "util"]
factory = js[js.index("async function renderHostCharts("):js.index("function pruneHosts(")]
assert "hosts=${encodeURIComponent(host.name)}" in factory, \
"the per-host chart query is not scoped to one host"
assert "&metrics=" in factory and "state.range" in factory, \
"a host's charts need one metric list and the selected window"
# GPU series are named from the payload's own `gpus[].index`, never discovered/guessed
assert "gpu.busy_pct@" in js and "gpu.index" in js
# the GPU chart exists only while the host reports cards
gpu_slot = js[js.index("function hostChartSet("):js.index("function seriesFor(")]
assert re.search(r"if \(!wantsGpu && set\.gpu\)", gpu_slot), \
"a host that stops reporting GPUs keeps a GPU chart it has no data for"
pruner = js[js.index("function pruneHosts("):js.index("// ---- snapshot")]
assert "state.hosts.delete(name)" in pruner and "state.charts.delete(name)" in pruner, \
"a host that left the payload keeps a live panel and drawing charts"
def test_system_history_charts_are_collapsed_behind_a_toggle(client) -> None:
"""TODO.md L4: "The historical charts on the system resources page should only be visible
if expanded. I only want to see current stats to keep things clean."
The default `/system` view is the hero row. Pinned on the served script because the card
markup is generated in JS and there is no JS test runner (a locked anchor), and the two
ways this regresses are invisible otherwise: a `<details open>` (charts visible by
default, i.e. the task un-done but the page still pretty) and a paint pass that runs
while the stack is shut, which builds three `LineChart`s per host inside a
`display:none` container — measured 0 px wide, never drawn, and rebuilt every poll.
"""
js = client.get("/static/page-system.js").text
card = js[js.index("function hostCard("):js.index("function memorySub(")]
# the stack is *wrapped*, not merely preceded by a heading: it lives inside the <details>
box = card.index('<details class="charts-box" data-k="charts">')
stack = card.index('<div class="host-charts">')
closed = card.index("</details>")
assert box < stack < closed, "the chart stack is not inside the toggle"
assert closed < card.index("More detail"), "the charts toggle swallowed the detail row too"
assert not re.search(r"<details[^>]*\bopen\b", card), \
"a panel ships its charts open — the default view is not current stats"
assert re.search(r"<summary[^>]*>\s*Charts", card), "the toggle no longer reads Charts"
# it also says which window the expanded stack draws (the chips stay in the page head)
assert 'data-k="charts-window"' in card, "the toggle does not carry the window"
# …and the toggle is what turns the paint pass on
assert "addEventListener('toggle'" in card and "chartsToggled(" in card
setter = js[js.index("function setChartsWindow("):js.index("function memorySub(")]
assert "state.rangeKey" in setter, \
"the toggle re-derives its window label instead of using the chips' own"
handler = js[js.index("Dash.rangeControl("):js.index("state.poller = poll(")]
assert "state.rangeKey = key" in handler and "setChartsWindow(" in handler, \
"changing the window leaves every toggle labelling the old one"
# nothing is built or drawn while the stack is shut, in either path that could paint
paint = js[js.index("function drawHostCharts("):js.index("async function renderHostCharts(")]
assert "if (!chartsOpen(card)) return" in paint, "a closed panel gets painted anyway"
assert "hostChartSet(card, host)" in paint, \
"the LineChart instances are no longer built by the open pass"
toggle = js[js.index("function chartsToggled("):js.index("function drawHostCharts(")]
assert "if (!chartsOpen(card)) return" in toggle, "closing the stack still paints"
assert "renderHostCharts(card, host)" in toggle and "state.range" in toggle, \
"a cached response from another window is drawn under this window's label"
# the request discipline is unchanged: one per host per refresh, every series named
fetch = js[js.index("async function renderHostCharts("):js.index("function windowLabel(")]
assert "hostChartSet(" not in fetch, "the poll path still builds charts for closed panels"
assert "state.history.set(" in fetch, \
"the response is not kept, so the first open is an empty box until the next poll"
assert "if (range === state.range) drawHostCharts(" in fetch, \
"a response for a window the operator just left is painted under the new label"
assert "drawHostCharts(" in fetch, "an open panel is no longer repainted on refresh"
def test_a_pruned_host_takes_its_charts_toggle_with_it(client) -> None:
"""A host that leaves the payload must leave no orphan control in the grid.
The toggle is a child of the panel, which is why `card.remove()` is enough — but that is
only true while the markup is built inside `hostCard()`, so the placement is pinned here
along with the three per-host Maps the panel owns.
"""
js = client.get("/static/page-system.js").text
card = js[js.index("function hostCard("):js.index("function memorySub(")]
assert 'data-k="charts"' in card, "the toggle is no longer part of the panel"
pruner = js[js.index("function pruneHosts("):js.index("// ---- snapshot")]
assert "state.hosts.get(name).remove()" in pruner
for owned in ("state.charts", "state.payloads", "state.history"):
assert f"{owned}.delete(name)" in pruner, f"{owned} outlives the panel it belongs to"
def test_the_charts_toggle_is_a_quiet_disclosure_not_a_new_component(client) -> None:
"""The collapse adds no palette and no second vocabulary (UI conventions).
Open/closed is carried by a caret *shape* as well as a colour step, the summary is muted
rather than accented, and it introduces no `.sev-*` family — the same rule the hero
numbers live by, checked here because a `<summary>` is the first control on the page a
designer might be tempted to grade.
"""
css = client.get("/static/dash.css").text
block = css[css.index("details.charts-box { "):css.index(".host-charts {")]
assert "details.charts-box > summary::before" in block, "the toggle has no caret shape"
assert 'details.charts-box[open] > summary::before' in block, \
"open and closed differ only in colour"
assert "var(--text-faint)" in block, "the closed toggle is not muted"
assert ".sev-" not in block and "var(--accent)" not in block, "the toggle invented a palette"
assert 'class="charts-box"' in client.get("/static/page-system.js").text, \
"the stylesheet styles a class the page no longer renders"
def test_an_expanded_chart_stack_does_not_grow_its_collapsed_siblings(client) -> None:
"""One host open must not stretch the hosts beside it into blank cards.
`/system` is the same shape `/llm` had to fix: an expanded panel beside a collapsed one
grows the collapsed card to match, and a blank panel reads as "no data" (the misreading
the unconfigured rules exist to prevent). `.grid.items-start` already exists for exactly
this, so the page uses it rather than growing a rule of its own.
"""
html = _code(client.get("/system").text)
hosts_class = re.findall(r'<div class="([^"]*)" id="hosts">', html)
assert hosts_class == ["grid auto items-start"], \
f"the host grid stretches its cards again: {hosts_class}"
assert ".grid.items-start" in client.get("/static/dash.css").text, \
"the rule the host grid relies on is gone"
def test_every_system_chart_names_itself_for_assistive_tech(client) -> None:
"""A 120px SVG is silence to a screen reader unless the page names it.
The phase-03 accessibility pass (task 04) found the per-host charts drawing anonymous
plots: the legend is a sibling element and labels nothing, so the `<svg>` itself must
carry `role="img"` + `aria-label` that says which host and which metrics. Pinned on the
served bytes because there is no JS test runner (a locked anchor).
"""
charts_js = client.get("/static/charts.js").text
# the shared LineChart applies the name to the svg, and only when the caller opted in
assert "if (this.opts.ariaLabel)" in charts_js, "LineChart stopped honouring ariaLabel"
assert "setAttribute('role', 'img')" in charts_js
assert "setAttribute('aria-label', String(this.opts.ariaLabel))" in charts_js
js = client.get("/static/page-system.js").text
# all three slots name themselves, and each name starts with the host the panel belongs
# to — an aria-label that cannot say *which machine* is not an accessible name here
assert js.count("ariaLabel: `") == 3, "a per-host chart slot lost its accessible name"
for slot in ("utilisation", "network traffic", "GPU busy"):
assert f"${{host.name}} \u2014 {slot}" in js or f"${{host.name}} — {slot}" in js, \
f"the {slot} chart has no host-scoped accessible name"
# ---- event feeds: a bounded, paginated window ----------------------------- #
#
# Every `ul.events` feed is one component (`Dash.eventsPager`) because a pager that
# works on only one page is the bug this shape prevents: the `event` table grows for as long
# as the fleet runs, and an unbounded feed turns `/system` into a scroll to nowhere
# (`TODO.md` L6 — "All the events tables need to be scrollable and paginated"). There is no
# JS test runner (a locked anchor), so what is pinned here is the shape every feed must keep:
# one shared renderer, a fetch bigger than one page, a footer that counts and labels itself,
# and a container that scrolls instead of growing the page.
# page -> (its script, the `source=` the poller stores that page's events under)
EVENT_FEEDS = {
"/system": ("page-system.js", "system"),
"/disks": ("page-disks.js", "disks"),
"/llm": ("page-llm.js", "llm"),
"/ups": ("page-ups.js", "ups"),
}
def _event_feed_path(js: str) -> str:
"""Just the `refreshEvents()` half of a page script — the feed's whole code path, comments out."""
js = _code(js)
return js[js.index("async function refreshEvents("):]
def _pager_call(feed: str, selector: str) -> str:
"""The options object of one `Dash.eventsPager($('<selector>'), …)` call inside the feed path.
Sliced per list so an assertion can name *which* feed lost its renderer, its accessible name
or its empty sentence. `/ups` is the page with two of them, so a whole-file assertion there
cannot tell one list from the other.
"""
head = f"Dash.eventsPager($('{selector}')"
assert head in feed, f"this feed is no longer rendered through Dash.eventsPager ({selector})"
start = feed.index(head)
end = feed.find("});", start)
assert end != -1, f"the {selector} pager call is never closed"
return feed[start:end]
def _feed_fetch_url(js: str) -> str:
"""The `/api/events` URL a feed asks for, quoted or templated."""
found = re.search(r"fetchJSON\([`'](/api/events[^`']*)[`']\)", _event_feed_path(js))
assert found, "this page no longer fetches the event feed"
return found.group(1)
def test_the_event_feeds_are_one_shared_pager_not_one_per_page(client) -> None:
"""`Dash.eventsPager()` renders every feed, and no page grows its own pager.
One copy per page is one chance per page to keep the old behaviour — and `/system` is the
one whose feed was already too tall when this was written down. `/disks` joined as a
fourth call site of the same helper, not as a fifth implementation of it.
"""
app_js = _code(client.get("/static/app.js").text)
assert app_js.count("function eventsPager(") == 1
exports = client.get("/static/app.js").text.rsplit("return {", 1)[1].split("}", 1)[0]
assert "eventsPager" in exports, "Dash.eventsPager is not exported"
for path, (script, _) in EVENT_FEEDS.items():
js = _code(client.get(f"/static/{script}").text)
assert "Dash.eventsPager($('#events')" in js, f"{path} does not render its feed through the pager"
assert "function eventsPager(" not in js, f"{path} invented its own pager"
assert "events.slice(" not in js, f"{path} windows its own events instead of asking the pager"
assert "row:" in _event_feed_path(js), f"{path} passes no row renderer"
def test_the_pager_pages_50_events_and_fetches_more_than_one_page(client) -> None:
"""The two numbers that make a client-side pager work, and the relation between them.
`EVENT_PAGE_SIZE` is the 50 the operator asked for; `EVENT_FEED_LIMIT` is what the feed
asks `/api/events` for. A limit at or below the page size is a pager that silently never
shows — there is never a second page to reach — and the feeds used to fetch 20/25/40.
"""
app_js = client.get("/static/app.js").text
page_size = re.search(r"const EVENT_PAGE_SIZE = (\d+);", app_js)
feed_limit = re.search(r"const EVENT_FEED_LIMIT = (\d+);", app_js)
assert page_size and int(page_size.group(1)) == 50, "a feed no longer pages 50 events"
assert feed_limit, "the feeds' event limit is no longer a named constant"
assert int(feed_limit.group(1)) > int(page_size.group(1)), \
"a fetch at or below one page means page 2 can never exist"
for path, (script, source) in EVENT_FEEDS.items():
url = _feed_fetch_url(client.get(f"/static/{script}").text)
assert "EVENT_FEED_LIMIT" in url, f"{path} hardcodes its own event limit"
assert f"source={source}" in url, f"{path} asks for the wrong event source"
def test_the_pager_footer_counts_and_labels_its_controls(client) -> None:
"""The footer counts (`1\u201350 of 137`), and prev/next are labelled and disabled at the ends.
The count is what tells the operator the window is a window; a control that stays live at
the last page claims there is more history than there is.
"""
app_js = _code(client.get("/static/app.js").text)
assert "${first}\u2013${last} of ${total}" in app_js, "the footer no longer counts"
assert 'data-pager="prev"' in app_js and 'data-pager="next"' in app_js
assert "prev.setAttribute('aria-label'" in app_js
assert "next.setAttribute('aria-label'" in app_js
assert "prev.disabled = pager.page === 0" in app_js
assert "next.disabled = pager.page >= pager.pages - 1" in app_js
# pagination only applies when there is more than one page of data
assert "foot.hidden = !paginating" in app_js
css = client.get("/static/dash.css").text
assert ".events-pager {" in css, "the pager footer has no style"
assert "button.ghost:disabled" in css, "a disabled pager control looks live"
def test_the_event_feeds_scroll_instead_of_growing_the_page(client) -> None:
"""The `<ul>` is the scrollable element, bounded in `dash.css`; the page stays short.
Pinned on the served bytes because there is no JS test runner (a locked anchor): a
stylesheet edit that drops `max-height` is invisible until the feed is long, and by then
the page is massive again.
"""
css = client.get("/static/dash.css").text
block = css[css.index("ul.events {"):css.index("}", css.index("ul.events {"))]
assert "max-height" in block and "overflow-y: auto" in block, \
"the event feed is unbounded again — the page grows with history"
for path in EVENT_FEEDS:
html = client.get(path).text
assert '<ul class="events" id="events"' in html, f"{path} lost its feed's id"
def _details_blocks(html: str) -> list[str]:
"""Every `<details>…</details>` block on a served page, in document order.
Nesting is not a thing on these pages (one disclosure per card), so a flat scan of pairs is
enough — and it is what lets a pin say *no disclosure contains this*, rather than only
"not the one disclosure I happen to know about". Phase 26 needs exactly that: the operator's
complaint was that a connection error was reachable only by opening something.
"""
blocks = []
for found in re.finditer(r"<details\b[^>]*>", html):
end = html.find("</details>", found.end())
assert end != -1, "a served <details> is never closed"
blocks.append(html[found.start():end])
return blocks
def test_ups_power_events_feed_sits_behind_a_collapsed_disclosure(client) -> None:
"""The `/ups` power feed is *expandable*: one `details.events-box`, shut by default.
`TODO.md:4` asked for an expandable list of incidents and phase 26 kept that shape — what it
overruled was *what* was behind it. A collapsed `<details>` renders no content, so the shut
card is still the resting state; since phase 26 it gates **visibility only** (the feed is
fetched on every poll, which the next test pins), and it holds only the power kinds.
Pinned on the served bytes because there is no JS test runner (a locked anchor): an `open`
attribute would undo the phase silently, the `#events` id has to stay *inside* the disclosure
or the pager and the bounded 320px scroll lose their target, and the **error** half
(`#events-errors`) has to be outside *every* disclosure and ship `hidden` rather than open —
a `NUT server NutError: cannot reach upsd …` line an operator has to click to read is the
bug this phase exists to fix (`TODO.md:6`).
"""
html = _code(client.get("/ups").text)
box = re.search(r'<details class="events-box">(.*?)</details>', html, re.S)
assert box, "the Power events card is no longer a details.events-box disclosure"
assert not re.search(r"<details[^>]*\bopen\b", html), \
"the disclosure ships open — the list is tall again, not expandable"
inner = box.group(1)
assert "<summary>Power events</summary>" in inner, "the summary lost its label"
assert '<ul class="events" id="events"' in inner, "the feed moved outside the disclosure"
assert "loading…" in inner, "the first-expand placeholder is gone"
assert "<h3>Power events</h3>" not in html, "the card heads its own body twice"
# The error half is on the page **and not collapsible**: no disclosure anywhere holds it.
# The card-local pin in `test_the_ups_connection_errors_are_a_card_of_their_own_above_the_disclosure`
# checks the card does not *contain* a `<details>`; this is the stronger direction — moving
# that card inside a new disclosure somewhere else would pass there and fail here.
assert 'id="events-errors"' in html, "the connection-error list left the served page"
assert not any('id="events-errors"' in block for block in _details_blocks(html)), \
"the error list is inside a disclosure — reading it needs a click again"
card_open = html[html.index('<section class="card" id="ups-errors-card"'):
html.index(">", html.index('<section class="card" id="ups-errors-card"'))]
assert "hidden" in card_open, \
"the error card's resting state is visible — an empty card on every quiet day"
# the rest of the page is untouched by the disclosure
for still_there in ('id="ups-cards"', 'id="chart-charge"', 'id="chart-load"',
'id="confirm-dialog"'):
assert still_there in client.get("/ups").text, f"/ups lost {still_there}"
# The event kinds `/ups` keeps behind the collapsed disclosure, and the writers that store
# them. `collectors.py` writes `status` (an `ups.status` transition, power loss included)
# and `test` (a self-test verdict); `api.py` writes `action` (a command this page sent).
# Everything else in the feed is a connection/error event (`unreachable` / `reachable` /
# `recording` / `store-error`) and belongs in the list that is never hidden.
UPS_POWER_EVENT_KINDS = ("status", "test", "action")
def _power_event_kinds(js: str) -> frozenset[str]:
"""The kinds `page-ups.js` keeps behind the disclosure, read out of its own set literal.
Parsing beats matching one exact line. The claim is *these three and only these* — a kind
added to the hidden list moves an operator-visible row out of sight, and a kind dropped from
it hides a power event — so an exact-string assert that happens to survive a reformat is not
the pin. An empty set is refused rather than returned, because it would make every event an
error row and look like a passing classification.
"""
found = re.search(r"const POWER_EVENT_KINDS = new Set\(\[(.*?)]\)", js, re.S)
assert found, "page-ups.js no longer defines POWER_EVENT_KINDS as a set literal of kind words"
kinds = re.findall(r"['\"]([^'\"]+)['\"]", found.group(1))
assert kinds, "POWER_EVENT_KINDS is empty — every event would land in the error list"
return frozenset(kinds)
def test_ups_events_are_fetched_on_every_poll(client) -> None:
"""Phase 19's lazy gate is out, and the feed splits into power events and errors.
`TODO.md:6` is not a complaint about the disclosure existing, it is a complaint about what
was behind it: a poll failure (`NUT server NutError: cannot reach upsd … (timed out after
4s)`) sat in the same collapsed list as the power events, so the one thing an operator
needs without opening anything was the one thing they had to open something for. The fix
is page-side and keyed on the stored `kind`: `status` / `test` / `action` stay behind the
disclosure, **everything else** is the always-visible error list — and because that list
is visible, `refresh()` fetches on every poll (a visible list that ages is worse than one
more 200-event fetch per interval). `/api/events` grew no vocabulary for any of this.
Pinned on the served script because there is no JS test runner (a locked anchor), and
pinned in both directions: the gate's absence *and* the kind set, so a silent rename on
the writer's side fails here instead of moving errors back into the hidden list.
"""
js = _code(client.get("/static/page-ups.js").text)
# The kind set, read out of the literal instead of matched as one line: exactly these three.
assert _power_event_kinds(js) == frozenset(UPS_POWER_EVENT_KINDS), \
(f"the hidden list's kind set is {sorted(_power_event_kinds(js))}, not "
f"{sorted(UPS_POWER_EVENT_KINDS)} — that classification is the operator's, not the page's")
# The lazy cache is gone: with an always-visible list there is nothing left to stamp.
decl = js[js.index("const state = {"):js.index("const MARKS")]
assert "events" not in decl, "the disclosure's lazy-fetch state came back"
assert "state.events" not in js, "something still reads the deleted state.events object"
# The poll path asks every interval, gated on nothing.
refresh = js[js.index("async function refresh("):js.index("async function refreshEvents(")]
assert re.search(r"^\s*await refreshEvents\(\);", refresh, flags=re.M), \
"the poll loop no longer fetches the feed on every poll"
assert "state.events.open" not in refresh and "eventsBox" not in refresh, \
"the fetch is gated on the disclosure again"
# The toggle handler's fetch logic is gone with it: the native `<details>` now gates only
# visibility, so no `toggle` listener may exist to decide whether to fetch.
boot = js[js.index("document.addEventListener('DOMContentLoaded'"):]
assert "addEventListener('toggle'" not in boot, \
"a toggle handler is deciding whether to fetch again"
assert "eventsBox" not in boot, "the disclosure's fetch-on-expand handler came back"
# One fetch, two lists, split on `kind` — and the URL is the phase-19 one verbatim.
feed = _event_feed_path(js)
assert _feed_fetch_url(js) == "/api/events?limit=${Dash.EVENT_FEED_LIMIT}&source=ups", \
"the feed's fetch is no longer the unchanged /api/events call"
for vocabulary in ("kind=", "page=", "severity="):
assert vocabulary not in _feed_fetch_url(js), \
f"/api/events grew {vocabulary[:-1]} vocabulary for a page-side split"
assert "const power = data.events.filter((e) => POWER_EVENT_KINDS.has(e.kind));" in feed, \
"the power list is no longer the filter's positive arm"
assert "const errors = data.events.filter((e) => !POWER_EVENT_KINDS.has(e.kind));" in feed, \
"the error list is no longer the filter's default arm (an unknown kind must be visible)"
# Stale-last-good: the catch clause does *nothing*, so a poll that never answered leaves
# both `<ul>`s holding the rows they already had (the pager rewrites a list only when its
# markup changed — invariant 4's shape on the page side).
opened = "} catch (err) {"
at = feed.index(opened)
catch_body = feed[at + len(opened):feed.index("}", at + len(opened))].strip()
assert catch_body == "", \
f"the failure path does something ({catch_body!r}) instead of leaving the last good rows"
def test_the_ups_event_rows_keep_the_date_time_and_severity_shape(client) -> None:
"""`TODO.md:4`'s other half: an expandable list is only *clear* if each row says when.
Phase 19's disclosure and phase 26's split are the new parts; this is the half that had to
survive both untouched. Every row still opens with a `<time>` carrying a month/day plus a
24-hour clock, then the UPS name inside the `sev <severity>` chip the stylesheet colours from
the payload's own verdict, then the message through `eventText()` (subject named once) with
the runtime footnote only when the event actually carries one. Pinned on the served script
because there is no JS test runner (a locked anchor): a "tidy-up" of the row template that
drops the timestamp, prints the raw message, or escapes nothing would ship silently.
Since phase 26 task 02 the renderer is **one const read by both lists**, so the two halves
of one feed cannot grow two shapes — which is what this reads, rather than a `row:` option
that could differ per list.
"""
js = _code(client.get("/static/page-ups.js").text)
feed = _event_feed_path(js)
at = js.index("const eventRow = (event) =>")
row = js[at:js.index("async function refreshEvents(", at)]
# the date/time: `<time>` first, month + day + hour + minute, 24-hour (the operator reads
# an incident log in the order it happened, and "14:05" on its own is not that)
assert "<time>" in row and "</time>" in row, "the row lost its timestamp element"
assert "new Date(event.ts * 1000).toLocaleString(" in row, \
"the row formats something other than the event's own timestamp"
for option in ("month: 'short'", "day: 'numeric'", "hour: '2-digit'", "minute: '2-digit'",
"hour12: false"):
assert option in row, f"the timestamp no longer asks for {option}"
assert row.index("<time>") < row.index('class="sev'), "the row no longer leads with the date"
# who: the host inside the verdict chip, escaped, and the severity is the API's word
assert 'class="sev ${event.severity}"' in row, \
"the row stopped taking its colour word from the stored severity"
assert "${DashCharts.escapeHtml(event.host)}</span>" in row, \
"the row no longer names the UPS it belongs to, escaped"
# what: the message through the shared subject-said-once cleaner, and escaped
assert "${DashCharts.escapeHtml(eventText(event))}" in row, \
"the message is no longer cleaned and escaped (raw HTML from an upstream is a bug)"
# the runtime footnote is conditional: an event with no runtime says nothing about one
assert "event.data && event.data.runtime_seconds" in row, \
"the runtime footnote is no longer gated on the event carrying a runtime"
assert "duration(event.data.runtime_seconds)} runtime)" in row, \
"the runtime footnote stopped printing the stored seconds as a duration"
# and the renderer is still handed to the shared pager over the bounded list, labelled.
# Per **call**, not per file: `#events-errors` is a list of its own now, and the claim is
# that it is drawn, named and emptied exactly like the power list — a count of `row:` in the
# file would still pass if one call lost its renderer to a default.
for selector, label, empty in (("#events", "Power events", "No power events yet"),
("#events-errors", "Connection & errors",
"No connection errors")):
call = _pager_call(feed, selector)
assert "row: eventRow," in call, f"the {selector} list is no longer drawn by the shared row"
assert f"label: '{label}'" in call, f"the {selector} list lost its accessible name"
assert empty in call, f"the {selector} list no longer says which feed is empty"
# one renderer, two lists: a template the two lists do not share would be two shapes for one
# event, and the row may not branch on which list it landed in — the only split is the kind set
assert js.count("const eventRow = (event) =>") == 1, \
"the /ups lists no longer share one row template"
assert "event.kind" not in row and "POWER_EVENT_KINDS" not in row, \
"the row template reads the kind — the two lists are growing two shapes"
# The card's visibility is the one state this page claims about a list it cannot see, it is
# decided by the **error** half alone, and only after both lists are painted (earlier would
# flash the `loading…` seed on a first fetch with no error rows). The markup's own `hidden`
# default is pinned in test_ups_power_events_feed_sits_behind_a_collapsed_disclosure.
hidden_at = feed.index("$('#ups-errors-card').hidden")
assert feed[:hidden_at].count("Dash.eventsPager(") == 2, \
"the card's visibility is decided before both lists are drawn"
assert feed[hidden_at:feed.index(";", hidden_at)].endswith("= errors.length === 0"), \
"the error card's visibility no longer follows the error half of the feed"
def test_the_ups_connection_errors_are_a_card_of_their_own_above_the_disclosure(client) -> None:
"""Phase 26 task 02: the error half of the feed is a card, not a second list in a `<details>`.
`TODO.md:6` is that a `NUT server NutError: cannot reach upsd …` line is not something to
open a summary for. So the error rows get their own `section.card` **above** the Power
events disclosure, built from the vocabulary every other card on this page uses
(`.card > header`, `.card > .body`, `ul.events`, the pager's strip) — **no new stylesheet
selector**, because a second style for a second list is a second thing to keep in sync with
the bounded 320px scroll and the `1\u201350 of N` footer.
`hidden` is the resting state in the markup and the script keeps it true while the feed has
no non-power kinds: a permanent empty "Connection & errors" card is exactly the ink the
page's empty-state rules exist to avoid, and the first `unreachable` row is what shows it.
"""
html = _code(client.get("/ups").text)
card_at = html.index('<section class="card" id="ups-errors-card"')
card_end = html.index("</section>", card_at)
card = html[card_at:card_end]
opening = html[card_at:html.index(">", card_at)]
assert "hidden" in opening, "the error card ships visible with nothing in it"
assert '<ul class="events" id="events-errors"' in card, "the error card has no bounded feed"
assert "<h3>Connection &amp; errors</h3>" in card, "the card lost its title"
# the whole point: this list is *not* behind a summary
assert "<details" not in card, "the error list is inside a disclosure again"
assert card_end < html.index('<details class="events-box">'), \
"the error card sits below the power-events disclosure it is meant to lead"
assert 'id="events"' in html[html.index('<details class="events-box">'):], \
"the power list left its disclosure"
js = _code(client.get("/static/page-ups.js").text)
feed = _event_feed_path(js)
assert "Dash.eventsPager($('#events-errors'), errors, {" in feed, \
"the error list is not rendered through the shared pager"
assert "label: 'Connection & errors'" in feed, "the error list has no accessible name"
assert "$('#ups-errors-card').hidden = errors.length === 0;" in feed, \
"the error card no longer appears and disappears with its rows"
# and the pager state is per `<ul>` (`app.js`: a `WeakMap` keyed by the list), so the two
# lists page independently: this is the contract that makes a second call site free
app_js = _code(client.get("/static/app.js").text)
assert "const feedPagers = new WeakMap();" in app_js
assert "feedPagers.get(list)" in app_js and "feedPagers.set(list, pager)" in app_js
def test_the_ups_error_card_adds_no_stylesheet_selector(client) -> None:
"""The second feed is markup + the existing vocabulary, and `dash.css` must stay that way.
Pinned as the *absence* of a rule, because the temptation this writes back into is a
`#ups-errors-card` block restating the feed's scroll or the pager's strip: `ul.events {` and
`.events-pager {` are each declared exactly once in the sheet (the disclosure pin already
holds that), and the error card carries its own gap as an inline `margin-top` like every
other section on this page rather than through an id rule.
"""
css = _css(client)
for selector in ("#ups-errors-card", "#events-errors"):
assert selector not in css, f"dash.css grew a rule for {selector}"
assert css.count("ul.events {") == 1 and css.count(".events-pager {") == 1
html = client.get("/ups").text
assert 'id="ups-errors-card" style="margin-top:.85rem"' in html, \
"the error card no longer carries the page's own section gap inline"
def test_the_ups_events_disclosure_is_a_quiet_disclosure_not_a_new_component(client) -> None:
"""The `/ups` summary borrows the card header's rhythm and no new palette (task 03).
Pinned on the served stylesheet because a CSS edit is invisible until someone looks: the
`\\25B8`/`\\25BE` caret pair says open/closed in a **shape** as well as a colour, the webkit
marker is hidden so the browser's own triangle does not double it, and this `<summary>`
carries the card header's ground and bottom rule because it *replaces* that header instead of
sitting inside the body like `charts-box`'s toggle does. The `> .body` padding restatement is
the quiet one: `.card > .body` is a **child** selector, so a body moved inside a disclosure
silently loses its padding and the feed renders flush to the card edge.
What the block must not do is grow a verdict colour (open/closed is not a grading) or
re-state the shared feed rules — `ul.events`' bounded scroll and `.events-pager`'s strip are
class-based and resolve inside the disclosure unchanged, so a second copy here would be a
second thing to keep in sync. The undeclared-token audit itself stays the existing
`test_no_stylesheet_rule_uses_an_undeclared_token`'s job, untouched.
"""
css = _css(client)
start = css.index("details.events-box { ")
block = css[start:css.index(".host-charts {", start)]
assert 'details.events-box > summary::before { content: "\\25B8"' in block, \
"the toggle has no caret shape"
assert 'details.events-box[open] > summary::before { content: "\\25BE"; }' in block, \
"open and closed differ only in colour"
assert "details.events-box > summary::-webkit-details-marker { display: none; }" in block, \
"the browser's own marker doubles the caret"
assert "cursor: pointer" in block and "list-style: none" in block, \
"the summary no longer reads as a control"
# it replaces the card header, so it carries the header's ground and bottom rule
assert "border-bottom: 1px solid var(--border)" in block, \
"a closed card no longer reads as its heading"
assert "color-mix(in srgb, var(--bg-raised) 70%, var(--bg-sunken))" in block, \
"the summary lost the header's sunken ground"
# open: the label steps back, and no margin — the `.body` below carries the spacing
assert "details.events-box[open] > summary { color: var(--text-faint); }" in block, \
"an open summary does not de-emphasise"
assert "margin-bottom" not in block, "the open summary stacks a gap on top of the body's"
# the child-selector gotcha that task 01's markup introduced
assert "details.events-box > .body { padding: .8rem; }" in block, \
"the feed body lost its padding inside the disclosure"
for token in ("var(--ok)", "var(--warn)", "var(--fail)", "var(--accent)"):
assert token not in block, f"the disclosure graded open/closed with {token}"
assert ".sev-" not in block, "the disclosure invented a verdict family"
# one copy of each shared feed rule, class-based, resolving inside the disclosure as elsewhere
assert css.count("ul.events {") == 1, "ul.events is styled twice — the bounded scroll drifted"
assert css.count(".events-pager {") == 1, "the pager strip is styled twice"
# the class this block styles is the class the page renders
assert 'class="events-box"' in client.get("/ups").text, \
"the stylesheet styles a disclosure no page renders"
# Phase 26's guard, here because it is the disclosure's own claim: the *second* feed this
# page renders borrowed `section.card` + `ul.events` + `.events-pager` and nothing else, so
# this block may not have grown a rule for it either. A second style for a second list is a
# second copy of the bounded scroll and the pager strip to keep in sync — the phase-19
# anti-pattern, one task later. (`test_the_ups_error_card_adds_no_stylesheet_selector` holds
# the same guard for the card's own section, plus the inline-gap pin.)
for selector in ("#ups-errors-card", "#events-errors"):
assert selector not in block and selector not in css, \
f"dash.css styles the second feed through {selector} instead of the shared classes"
def test_the_pager_returns_to_page_1_when_the_events_change(client) -> None:
"""New events reset the window; the same events do not.
Resetting on every poll would snap `next` back within one interval, so page 2 could never
be read; never resetting lands an operator on page 3 of a list that changed underneath
them. The page key is the set of events, not the clock.
"""
app_js = _code(client.get("/static/app.js").text)
body = app_js[app_js.index("function eventsPager("):]
assert "signature !== pager.signature" in body, "the pager no longer notices new events"
reset = body[body.index("signature !== pager.signature"):]
assert reset.index("pager.page = 0") < reset.index("paintEventFeed"), \
"new events do not reset the page before the feed is drawn"
assert "if (pager.markup !== markup)" in app_js, \
"every poll rewrites 50 rows and throws away the reader's scroll position"
def test_system_event_feed_queries_the_source_transitions_are_stored_under(client) -> None:
"""Host transitions are stored under the *dashboard* name, not the sample source.
`poller.py` logs `Event(ts, self.dashboard, …)` — host jobs write `system`, the way
the UPS job writes `ups` — while *samples* use `host`. The `/system` feed queried
`source=host` since the first commit and therefore always rendered "no transitions
yet", even with a real outage (a gateway disconnect) sitting in the store. Pinned on
the served bytes because there is no JS test runner (a locked anchor).
"""
js = client.get("/static/page-system.js").text
feed = _event_feed_path(js)
url = re.search(r"fetchJSON\([`'](/api/events[^`']*)[`']\)", feed)
assert url, "the /system page no longer fetches the event feed"
assert "source=system" in url.group(1), \
"the /system event feed must ask for the source the poller stores events under"
assert "source=host" not in url.group(1), \
"source=host matches no stored event — the feed is empty forever"
def test_system_footnote_prints_the_window_it_actually_measured(client) -> None:
"""`Δ<window>` is the page's promise that a rate is a measurement (invariant 2), so the
number behind it has to come from the measurement.
The `window_seconds` beside the fleet is `HOST_INTERVAL_SECONDS`, while the window behind a
rate is the estimator's `RATE_WINDOW_SECONDS` — equal at the defaults, which is why the
footnote could quote the poll interval and look right. Set `RATE_WINDOW_SECONDS=30` and it
would have labelled every rate on the page with a window it never used. The footnote now
reads each host's own measured `window_seconds`, and prints a range when they disagree.
"""
js = client.get("/static/page-system.js").text
assert "function windowLabel(" in js, "the footnote lost its measured-window helper"
footnote = js[js.index("$('#foot-note').textContent"):js.index("await Promise.all(chartJobs)")]
assert "windowLabel(" in footnote
assert "host.window_seconds" in footnote, \
"the footnote is quoting the poll interval as a measurement again"
# a footnote that says "window" and a page that says "poll" must stay two separate claims
assert "poll ${Math.round(state.pollMs / 1000)}s" in footnote
def test_no_per_core_visual_library_survives_the_page(client) -> None:
"""The per-core heat bars left `/system` and `charts.js` with them (PLAN.md *Dropped
visuals* is about what the pages draw, not what the store keeps).
`renderCores` had one caller — the hero-card per-core block deleted in task 02 — so
keeping it would be dead markup generating unstyled `.core` spans; `.cores` was the CSS
that block needed. Both are checked absent here so neither can come back as a
half-change, while `sparkline`/`LineChart` stay for phase 04 and the UPS page.
"""
assert "renderCores" not in client.get("/static/charts.js").text
css = client.get("/static/dash.css").text
assert ".cores" not in css
for asset in ASSETS:
assert "core_usage" not in _code(client.get(asset).text), f"{asset} plots per-core"
# what phase 04 and the other pages still chart from
for name in ("LineChart", "sparkline", "renderLegend"):
assert name in client.get("/static/charts.js").text
def test_sparkline_ships_the_fixed_scale_and_verdict_options(client) -> None:
"""Phase 04 turns `/llm` cells into sparkline rows, so the shared `sparkline()` must
behave like `llm_monitor`'s `drawSpark` (the task-02 contract, NOTES-chart-set).
Pinned on the served bytes because there is no JS test runner (a locked anchor). What
matters is not the option names but the semantics behind each one: a percent metric
that silently rescaled to its own data would make an 8 % KV cache and a 92 % spike the
same squiggle, and a stroke coloured from a page-invented threshold would break the
one-colour-language rule the same way.
"""
charts_js = client.get("/static/charts.js").text
body = charts_js[charts_js.index("function sparkline("):charts_js.index("window.DashCharts")]
# the phase grep criterion, as a test: the options are really in the shipped file
hits = [ln for ln in charts_js.splitlines()
if re.search(r"fixedMax|align|severity", ln)]
assert len(hits) >= 3, "the sparkline upgrade is not in the served charts.js"
for opt in ("fixedMax", "align", "labels", "severity", "minPoints", "span", "format",
"minSpan"):
assert opt in body, f"sparkline() lost its `{opt}` option"
# fixedMax: the data clamps to the axis, never the axis to the data
assert "Math.min(Math.max(v, lo), hi)" in body, \
"a fixedMax sparkline rescales to its own data again"
# align:'right' grows within a real time window (span seconds), not a sample count
assert "x1 - o.span" in body, "align:'right' forgot the window it grows inside"
# verdict colour comes from the theme tokens only; unrated is faint grey, never green
for token in ("cssVar('--ok')", "cssVar('--warn')", "cssVar('--fail')",
"cssVar('--text-faint')"):
assert token in body, f"sparkline() stopped taking its colour from {token}"
assert ".sev-" not in body, "sparkline invented a parallel severity vocabulary"
# the `now / max` foot llm_monitor prints, via the caller's format callback
assert "now ${fmt(last.value)}" in body and "max ${fmt(dataMax)}" in body
# never a confident line through one point, and the foot quotes data, not the padded axis
assert "minPoints: 2" in body and "no history" in body
# ASSUMPTION (task 02): no animation at all, so prefers-reduced-motion holds by design
assert "transition:" not in body and "animation:" not in body
def test_a_flat_series_keeps_the_axis_floor_the_page_declared(client) -> None:
"""`_yExtent()` pads a flat series so its line has a band to sit in, and a declared `yMin`
is not negotiable inside that padding.
Every idle gateway pipe publishes `0`, so the first `/gateway` charts were flat and the old
`lo === hi` shortcut answered `[lo - 1, hi + 1]` whatever the page passed: the axis grew a
`-1` tick, which is a scale describing a number of connections that cannot exist — the same
mistake as printing `0` where nothing measured, wearing an axis instead of a cell.
"""
charts = _code(client.get("/static/charts.js").text)
extent = charts[charts.index("_yExtent()"):charts.index(" draw()")]
assert "this.opts.yMin !== null ? Math.min(lo, this.opts.yMin) : lo - pad" in extent, \
"a flat series is padding below the floor the page declared"
# ---- the battery glyph (phase 05 task 02) ---------------------------------
#
# `DashCharts.battery()` is the one visual in this app whose *shape* is the data: the fill
# is the charge. That makes the honesty rules heavier here than anywhere else — a fill
# rounded up to look reassuring, a colour invented for an unrated cell, or a state only
# readable as green-vs-red would each turn a glance at the battery into a wrong claim.
# There is no JS test runner (a locked anchor), so the construction is pinned on the served
# bytes and shown in a browser (`data/logs/ups-glyph-*.png`).
def _battery_js(client: TestClient) -> str:
"""The glyph's code as served (state table + body), comments stripped.
Prose cannot satisfy these assertions, and the phrase table the accessible name is
built from is part of the component, so the slice starts above `function battery`.
"""
charts = client.get("/static/charts.js").text
body = charts[charts.index("const BATTERY_STATES"):charts.index("window.DashCharts")]
assert "function battery(container, opts)" in body
return _code(body)
def test_battery_glyph_is_exported_from_the_shared_chart_library(client) -> None:
"""The phase's own grep criterion, as a test: exactly one glyph, and it is public."""
charts = client.get("/static/charts.js").text
assert charts.count("function battery") == 1
exports = charts[charts.index("window.DashCharts"):]
assert re.search(r"\bbattery\b", exports), "DashCharts.battery is not exported"
# the wrapper the stylesheet sizes, added by the component rather than each caller
assert ".battery {" in _css(client) and ".battery svg" in _css(client)
def test_battery_fill_is_the_charge_and_is_never_padded(client) -> None:
"""2 % must look nearly empty, and 0 % must leave an empty shell, not a stray pixel."""
body = _battery_js(client)
# clamped to the scale, and the clamp is the only thing that touches the number: a
# minimum visible width would be a reassuring battery over a flat one
assert "Math.max(0, Math.min(100," in body
assert "(pctValue / 100) * inner.w" in body, "the fill stopped being a fraction of charge"
assert not re.search(r"Math\.max\(\s*[0-9.]*[1-9]\s*,", body), \
"the charge fill got a minimum width — a small charge would look bigger than it is"
# nothing is drawn for a cell at zero, and the plate it fills is a clip, so a low fill
# cannot leak outside the rounded interior
assert "pctValue > 0" in body
assert "<clipPath" in body and 'clip-path="url(#' in body
# an empty cell is the sunken plate, never a transparent hole in the card
assert "cssVar('--bg-sunken')" in body
def test_battery_colours_come_from_the_verdict_tokens_only(client) -> None:
"""The verdict paints; an unrated cell draws a grey shape and no colour at all."""
body = _battery_js(client)
for token in ("cssVar('--ok')", "cssVar('--warn')", "cssVar('--fail')"):
assert token in body, f"the glyph stopped taking its colour from {token}"
# no verdict → muted outline, and a measured charge in faint grey at half strength: a
# shape, not a judgement (an unknown number must never look green)
assert "cssVar('--text-muted')" in body and "cssVar('--text-faint')" in body
assert ' opacity=".5"' in body, "an unrated charge is drawn with the same weight as a rated one"
assert ".sev-" not in body, "the glyph invented a parallel severity vocabulary"
assert "severityFor(" not in body, "the glyph compares the charge to a threshold itself"
def test_battery_states_are_readable_without_colour(client) -> None:
"""Every state has a *shape*, so the glyph survives monochrome and colour-blindness.
on battery = dashed outline, charging = bolt, unknown = a dash pattern through the
body, online = a plain solid shell. A word the page did not intend falls back to
`unknown` rather than to a confident "on line".
"""
body = _battery_js(client)
assert 'BATTERY_STATES.indexOf(o.state) >= 0' in body, \
"an unrecognised state would be drawn as some other state"
assert 'state === \'onbattery\'' in body and 'stroke-dasharray="7 5"' in body, \
"the on-battery outline is no longer dashed"
assert "state === 'charging'" in body and 'd="M13 2 L3 14' in body, "the charging bolt is gone"
assert "state === 'unknown'" in body and 'stroke-dasharray="5 6"' in body, \
"an unknown charge no longer draws a dash through the body"
# and the unknown case really is *no fill*: the shape has to say it on its own
assert "pctValue != null" in body
def test_battery_glyph_names_itself_and_can_say_it_once(client) -> None:
"""`role="img"` + `<title>` built from the inputs, with an explicit `aria-hidden` opt-out."""
body = _battery_js(client)
assert 'role="img"' in body and "<title>" in body and "aria-label=" in body
for phrase in ("Charge ", " percent", "Charge unknown", "on line", "on battery",
"charging", "state unknown"):
assert phrase in body, f"the accessible name lost {phrase!r}"
# the label is a UPS name from upsd, so it goes through the one escaper
assert body.count("escapeHtml(name)") == 2, "<title> or aria-label stopped escaping the label"
assert 'aria-hidden="true"' in body, "the decorative opt-out for a duplicate hero number is gone"
def test_battery_glyph_scales_with_its_container_and_never_animates(client) -> None:
"""One `viewBox` + `preserveAspectRatio` for a phone and a 27" monitor; no motion at all."""
body = _battery_js(client)
assert 'viewBox="0 0 ${W} ${H}"' in body
assert 'preserveAspectRatio="xMidYMid meet"' in body, "the glyph stopped scaling with its box"
assert "ResizeObserver" not in body, "the glyph redraws on resize instead of scaling"
assert "transition" not in body and "animation" not in body
css = _code(_css(client))
wrapper = re.search(r"\.battery \{([^}]*)\}", css)
inner = re.search(r"\.battery svg \{([^}]*)\}", css)
assert wrapper and inner, "dash.css no longer sizes the glyph box"
assert "width: 100%" in wrapper.group(1) and "max-width" in wrapper.group(1), \
"the glyph is not capped inside its container"
assert "width: 100%" in inner.group(1) and "height: auto" in inner.group(1), \
"the svg would stretch instead of keeping the viewBox ratio"
both = wrapper.group(1) + inner.group(1)
assert "transition" not in both and "animation" not in both
# ---- the `/ups` hero row (phase 05 task 03) ------------------------------
#
# The card used to be nine equal `.metric` cells in which charge, load and runtime were the
# same size as the battery amperage. These pin the shape that replaced it on the served
# bytes (a locked anchor: no JS test runner), because the whole point of the row is a size
# and colour hierarchy a later "tidy up" can quietly flatten back to nine equal cells.
def _ups_card_js(client: TestClient) -> str:
"""The card markup as served, from `upsCard()` to `renderCard()`."""
js = client.get("/static/page-ups.js").text
return js[js.index("function upsCard("):js.index("function renderCard(")]
def test_ups_card_opens_with_the_glyph_and_three_heroes(client) -> None:
"""Charge / Load / Runtime are the card's three largest elements, next to the glyph.
`data-sev` names the verdict path each slot colours itself from, the same convention the
`/system` host card uses — so "which number is coloured by what" is readable in the
markup, and a rename that disconnects the page from `/api/ups/snapshot` fails here
instead of shipping a card of `n/a`.
"""
card = _ups_card_js(client)
assert '<div class="ups-hero">' in card, "the hero row left the top of the card"
assert 'data-k="glyph"' in card, "the battery glyph is not part of the hero row"
slots = re.findall(r'<div class="hero[^"]*" data-k="([^"]+)"', card)
assert slots == ["charge", "load", "runtime"], \
"the hero row is no longer charge / load / runtime in the agreed order"
for hook in ("severity.charge_pct", "severity.load_pct", "severity.battery_runtime"):
assert hook in card, f"the card no longer names {hook} as a colour source"
# the hero row is the first thing in the card body, above the health checks and commands
assert card.index("ups-hero") < card.index('data-k="checks"')
assert card.index("ups-hero") < card.index('data-k="commands"')
def test_ups_heroes_take_their_colour_from_the_payload(client) -> None:
"""Three `Dash.hero()` calls, three payload verdicts, no JS threshold.
The charge hero carries **no** `.bar`: the glyph beside it is that same value drawn as a
shape, and one value drawn twice in one row is the pattern this app avoids (load, which
has no other shape, keeps its bar). Runtime prose comes from `runtimeText()` alone, and
a runtime nobody estimated is passed as `null` so `hero()` renders the faint, uncoloured
unknown rather than the string `n/a` styled like a measurement.
"""
js = client.get("/static/page-ups.js").text
body = js[js.index("function renderCard("):js.index("// ---- command dialog")]
assert body.count("Dash.hero(q(") == 3, "the hero row is not three hero numbers"
for verdict in ("verdicts.charge_pct", "verdicts.load_pct", "verdicts.battery_runtime"):
assert verdict in body, f"{verdict} never reaches a hero"
charge = body[body.index("Dash.hero(q('charge')"):body.index("Dash.hero(q('load')")]
assert "bar" not in charge, "charge grew a second shape when the glyph already is one"
load = body[body.index("Dash.hero(q('load')"):body.index("Dash.hero(q('runtime')")]
assert "bar: true" in load, "load lost its bar"
assert "runtimeText(n.battery_runtime)" in body, "runtime prose bypasses runtimeText()"
runtime = body[body.index("Dash.hero(q('runtime')"):]
assert "n.battery_runtime == null ? null" in runtime, \
"an unmeasured runtime is rendered as a measurement"
assert "severityFor(" not in body
def test_ups_secondary_electrical_row_is_demoted_not_deleted(client) -> None:
"""Line in, output, battery volts and amps, battery °C: still there, visibly second.
These are the numbers you reach for *while* the UPS is on battery, so they were demoted
rather than folded into the disclosure — and they stay one step down (smaller, muted) so
the three verdicts remain the largest elements, which is also what keeps the card from
growing so tall that two stop fitting side by side on a laptop.
"""
card = _ups_card_js(client)
assert 'class="metrics ups-electrical"' in card
for key in ("line", "out", "batt", "amps", "temp"):
assert f'data-k="{key}"' in card, f"the electrical row lost {key}"
assert card.index("ups-electrical") > card.index("ups-hero")
css = _code(_css(client))
rule = re.search(r"\.ups-electrical \.metric \.value\.sm \{([^}]*)\}", css)
assert rule, "dash.css no longer styles the secondary row"
assert "var(--text-muted)" in rule.group(1), "the secondary row is not muted"
sizes = re.findall(r"font-size: ([\d.]+)rem", rule.group(1))
assert sizes and float(sizes[0]) < 1.3, "the secondary row competes with the heroes"
# `n/a` is fainter than a muted reading — neither real driver reports a battery temperature
null_rule = re.search(r"\.ups-electrical \.metric \.value\.sm\.is-null \{([^}]*)\}", css)
assert null_rule and "var(--text-faint)" in null_rule.group(1), \
"an unmeasured secondary value is not faint, so it reads like a number"
def test_ups_glyph_state_comes_from_the_api_fields(client) -> None:
"""`glyphState()` maps `on_battery` + `charger_status` — and `full` is **not** charging.
The mapping is presentation, so it lives in the page (task 02's assumption), but it is
still a mapping the real drivers exercise: neither UPS here exports
`battery.charger.status`, so the fallback that reads a state the API rated `ok` is what
draws a solid shell on both of them. Nothing here re-reads `ups.status` —
`test_the_page_does_not_re_parse_nut_status_flags` holds that line.
"""
js = _code(client.get("/static/page-ups.js").text)
glyph = js[js.index("function glyphState("):js.index("function drawGlyph(")]
assert "item.on_battery" in glyph and "'onbattery'" in glyph
assert "charger === 'chg'" in glyph and "'charging'" in glyph
assert "charger === 'dchg'" in glyph, "a discharging charger token is on battery too"
assert "charger === 'full'" in glyph and "'online'" in glyph, \
"a full, online battery must not be drawn as charging"
assert "item.states" in glyph, "a driver with no rich status lost its state entirely"
assert "'unknown'" in glyph, "an unknown state now degrades to a confident 'on line'"
def test_ups_glyph_repaints_only_when_something_visible_changes(client) -> None:
"""Charge jitters by a tenth of a percent every poll; the `<svg>` does not rebuild on it.
Rebuilding would re-resolve the theme tokens and re-announce the accessible name every
`NUT_INTERVAL_SECONDS` for a difference no eye can see. `decorative: true` is the
component's documented opt-out — the charge hero and the state chips already say
"Charge 100 percent, on line", and a screen reader should not hear it twice.
"""
js = _code(client.get("/static/page-ups.js").text)
draw = js[js.index("function drawGlyph("):js.index("function upsCard(")]
assert "DashCharts.battery(" in draw
assert "cell.dataset.drawn === drawn" in draw, "the glyph rebuilds on every poll"
assert "decorative: true" in draw, "the glyph duplicates the hero number in a11y"
assert "glyphState(item)" in draw
def test_ups_hero_row_makes_room_for_the_glyph(client) -> None:
"""The glyph gets a real column, and the row stacks rather than clipping on a phone.
The track floor is the widest runtime prose the page prints (`9h 59m`) — a number that
cannot wrap needs the floor to be its own width, or the row grows a clipped `1h 28`.
"""
css = _code(_css(client))
row = re.search(r"\.ups-hero \{([^}]*)\}", css)
assert row, "dash.css lost the UPS hero row"
assert "minmax(112px, 156px) minmax(0, 1fr)" in row.group(1), "the glyph has no column"
assert "align-items: center" in row.group(1)
heroes = re.search(r"\.ups-hero \.heroes \{([^}]*)\}", css)
assert heroes and "minmax(min(140px, 100%), 1fr)" in heroes.group(1), \
"a hero track can now clip the widest runtime prose"
at = css.index("@media (max-width: 620px)")
block = css[at:at + 240]
assert ".ups-hero" in block and "minmax(0, 1fr)" in block, "the row never stacks"
def test_every_ups_chart_names_itself_for_assistive_tech(client) -> None:
"""The four trend plots on `/ups` carry an accessible name, like every other chart here.
`AGENTS.md` states the convention as *every* chart names itself: `LineChart` puts
`role="img"` + `aria-label` on the `<svg>` when the caller passes `ariaLabel`, because
the legend is a sibling that labels nothing and an unlabelled plot is silence. `/system`
(phase 03) and `/llm` (phase 04) were pinned; these four were the stragglers, so the
rule is now pinned on every page that draws one.
"""
js = client.get("/static/page-ups.js").text
charts = js[js.index("function makeCharts("):js.index("function points(")]
assert charts.count("ariaLabel:") == 4, "a `/ups` chart lost its accessible name"
for phrase in ("Battery charge", "Battery runtime", "Battery and line voltage", "UPS load"):
assert phrase in charts, f"the {phrase!r} chart names nothing readable"
# ---- the `/llm` layout (phase 04 task 03) ---------------------------------
#
# The drift this phase fixes was a 14-cell grid where everything was the same size and
# nothing showed recent shape. These tests pin the *shape* the page committed to on the
# served bytes (a locked anchor: no JS test runner), so a regression to "all cells equal"
# fails a test rather than a screenshot.
def test_llm_page_colours_from_the_payload_not_from_js_thresholds(client) -> None:
"""`page-llm.js` paints the KV verdict `severity.py` produced; it owns no threshold.
Both `severityFor(value, 75, 90)` call sites are gone (the task's explicit work item):
the KV row takes `severity` from the rated ratio cell, the fleet KV bar was deleted
with the summed card, and every unrated number — throughput above all, because 0 tok/s
on an idle server is healthy — draws no colour at all.
"""
js = client.get("/static/page-llm.js").text
assert "severityFor(" not in js, "the LLM page re-invented a threshold"
assert "severityFrom" in js
# the KV verdict comes from the payload cells `sources/llm.py` rates
kv = js[js.index("case 'cache':"):js.index("case 'ttft':")]
assert "kv_cache_usage_ratio" in kv and "gpu_cache_usage" in kv and "severity" in kv
def test_llm_engine_panel_leads_with_two_throughput_heroes(client) -> None:
"""Decode and prompt are the largest elements per engine; the rest is visibly secondary.
The hero row is `Dash.hero()` markup (2.4rem via `.hero`), built from the `hero: true`
flags in `CELLS`, and throughput carries **no** severity — `severity.py` has no
throughput row and the page invents none (the neutral-ink decision in NOTES-chart-set,
mirroring the `/system` NET precedent). The provenance tag stays in the hero's `.sub`
with the exact `source` as the tooltip: removing it to look cleaner is a regression.
"""
js = client.get("/static/page-llm.js").text
panel = js[js.index("function enginePanel("):js.index("function renderPanel(")]
assert 'data-hero="${cell.key}"' in panel, "the hero row is not flag-driven markup"
assert '<div class="heroes">' in panel, "the hero row left the engine panel"
heroes = re.findall(r"{ key: '(\w+)'[^}]*?hero: true", js)
assert heroes == ["decode", "prompt"], "the hero row is no longer the two throughputs"
# driven by flags, not fourteen identical cells. Phase 07 task 03 lifted the summary rows
# out of the panel into the stats strip above it, so what is left here is the pair plus
# the latency rows the strip does not print — never a fourteen-cell grid again.
assert "SPARK_CELLS" in js and "ALARM_CELLS" in js
assert js.count("spark: '") >= 2, "the panel lost the latency sparkline rows it kept"
assert js.count("hero: true") == 2, "the hero row is no longer exactly two numbers"
# the hero call must not pass a severity — unrated by design
hero_call = js[js.index("const node = hero("):js.index("if (node) node.title")]
assert "severity" not in hero_call, "throughput invented a verdict"
# provenance intact: the tag is generated from the cell's `source`, never hardcoded
assert "provenance" in js and "Δ" in js
assert "node.title = value.source" in js
def test_llm_provenance_tag_prints_the_window_the_payload_sent(client: TestClient) -> None:
"""`Δ2.0s` on `/llm` is the cell's own `window`, never a config echo (phase 23).
The window moved from 10 s to one poll interval without touching the page: `provenance()`
reads `cell.window`, which is the number `tests/test_llm_source.py` pins to the LLM span, and
both spell the label with the same one-decimal rounding (`delta_label` in
`tests/rate_window_contract.py` is that rounding, used by both payload pins). A page printing
the configured interval instead would advertise a window no poll measured (rule 2), and a
literal `Δ10.0s` in the script is the same lie with the number typed in.
"""
js = client.get("/static/page-llm.js").text
prov = js[js.index("function provenance("):js.index("function humanNumber(")]
assert "`Δ${cell.window.toFixed(1)}s`" in prov, \
f"the Δ tag is no longer built from the window the cell carries: {prov}"
assert delta_label(LLM_WINDOW) == "Δ2.0s", "the rounding the page and the pins share moved"
assert "rate_window_seconds" not in js, "the page echoes a config window beside a measurement"
assert "Δ10.0s" not in js, "a hard-coded Δ is an invented window (rule 2)"
def test_every_llm_cell_carries_a_window_sparkline_with_percentile_feet(client) -> None:
"""Each panel row draws its own sparkline over the *selected* window and prints mean
**and** p95 without a hover.
The window is honest time (`align: 'right'` + `span`) and the p95 comes from the
snapshot's histogram cells — p95 needs no stored series. The `only` scopes are what keep
a gufo panel from showing a latency mystery and a vLLM strip card from showing a
spec-decode row it cannot fill.
Phase 07 task 03 moved the percent and count rows to the stats strip, so the plotted
percent series left the cell list and the axis guarantee moved with it: the KV trend
chart pins `yMax: 100` (an 8 % window that once held a 92 % spike must not read flat),
and the strip converts its 0-1 ratios to per cent in `cellValue()` — a bar cannot
autoscale, so it cannot lie that way either. The `fixedMax`/`minSpan` options stay
plumbed through `drawSparks()` and are pinned on the component itself by
`test_sparkline_ships_the_fixed_scale_and_verdict_options`: a count sparkline that comes
back must not get an axis finer than one request.
"""
js = client.get("/static/page-llm.js").text
sparks = js[js.index("function drawSparks("):js.index("function renderFleet(")]
assert "DashCharts.sparkline(" in sparks
for opt in ("align: 'right'", "span: state.range", "fixedMax", "labels: { max: true }"):
assert opt in sparks, f"cell sparklines lost `{opt}`"
# the one percent series still plotted owns a fixed 0-100 axis; seconds stay ms
charts = js[js.index("function makeCharts("):js.index("function pointsFor(")]
assert "yMax: 100" in charts, "the KV trend chart no longer pins its 0-100 axis"
assert "scale: 1000" in js and "cell.scale || 1" in sparks
# every spark cell declares its history series in the markup it is built from
assert 'data-spark data-metric="${cell.spark}"' in js
# percentile foot lines: mean big in the row, p95 beneath, n/a when nothing measured
assert "function p95Value(" in js
for name in ("time_to_first_token_seconds__p95", "time_per_output_token_seconds__p95",
"request_queue_time_seconds__p95"):
assert name in js, f"{name} no longer feeds a foot line"
assert "p95 n/a" in js
assert 'data-foot' in js
# engine-scoped rows: gufo never grows a TTFT row, vLLM never a spec-accept row
assert "only: ['vllm']" in js and "only: ['gufo']" in js
scoped = js[js.index("function sparkCellsFor("):js.index("function enginePanel(")]
assert "cell.only" in scoped
assert "minSpan: cell.minSpan" in sparks, "cell minSpan never reaches the sparkline"
def test_llm_sparklines_and_charts_cost_one_batched_history_request(client) -> None:
"""One `/api/llm/history` call per refresh feeds charts *and* every sparkline row.
The endpoint answers a metric list for every engine, and unknown names come back as
empty series on purpose — so the page asks for the full cell set once instead of one
XHR per cell, and the two series phase 04 added (`tpot_seconds`, `spec_accept_rate`)
must be in that list or those rows are permanent "no history" slots.
"""
js = client.get("/static/page-llm.js").text
# comments aside, exactly one fetch hits the endpoint: one request, every metric
calls = re.findall(r"fetchJSON\(\s*`/api/llm/history", _code(js))
assert len(calls) == 1, "the page makes more than one history request again"
assert "HISTORY_SERIES.join" in js and "SPARK_CELLS.map((cell) => cell.spark)" in js
request = js[js.index("async function refreshHistory("):js.index("function drawSparks(")]
assert "metrics=${HISTORY_SERIES.join" in request
assert "range=${state.range}" in request
# and the endpoint really answers every name the page asks for
for name in ("tpot_seconds", "spec_accept_rate", "ttft_seconds", "queue_seconds",
"kv_cache_usage", "requests_running", "requests_waiting",
"prefix_cache_hit_ratio"):
assert name in HISTORY_METRICS, f"llm.{name} is not stored, so its spark is dead"
def test_llm_alarm_cells_exist_only_while_there_is_an_incident(client) -> None:
"""`preemptions` / `device_lost` are absent at zero and red chips above zero.
The rule is the *existence of an incident*, not a graded threshold — `sources/llm.py`
already treats any non-zero `gufo_device_lost_total` as one. The chip reuses the shared
`.chip.fail` vocabulary; nothing new is painted, and a container emptied at zero means
the row does not exist rather than showing a permanent ignored 0.
Phase 07 task 03 renders them on the engine's strip card (the summary you read first)
instead of inside the panel, so the two `alarm: true` rows live in `STRIP_ROWS` and
`ALARM_CELLS` filters them out of it — counted with the comments stripped, because the
docstring on that table explains the flag by name.
"""
js = client.get("/static/page-llm.js").text
alarms = js[js.index("function renderAlarms("):js.index("function makeCharts(")]
assert "class=\"chip fail\"" in alarms, "the alarm chip left the shared fail vocabulary"
assert "value.value > 0" in alarms
assert "host.hidden = chips.length === 0" in alarms
assert 'data-alarm="${cell.key}"' in alarms
assert _code(js).count("alarm: true") == 2
assert "ALARM_CELLS = STRIP_ROWS.filter" in js, "the alarm rows are not strip rows"
# the panel no longer carries a second container for the same incident
panel = js[js.index("function enginePanel("):js.index("function renderPanel(")]
assert 'data-k="alarms"' not in panel, "the alarm chips are drawn twice"
strip = js[js.index("function stripCard("):js.index("function renderStripCard(")]
assert 'data-k="alarms"' in strip, "the alarm chips left the strip card"
# ---- the `/llm` stats strip (phase 07 task 03) ---------------------------
#
# `llm_monitor`'s shared strip (`index.html` ~L743), ported: one card per configured engine
# between the fleet block and the panels. Two things make it a summary instead of a second
# set of panels, and both are pinned below: every row is a cell `/api/llm/snapshot` really
# publishes, and no row is printed in the strip *and* in the panel under it.
#: row key -> the cells `cellValue()` resolves it from (`sessions.*` is the gufo slot count,
#: which is not a metrics cell). The served `STRIP_ROWS` table must match these keys exactly.
STRIP_ROW_PATHS: dict[str, tuple[str, ...]] = {
"prompt": ("prompt_tokens_per_second",),
"decode": ("decode_tokens_per_second",),
"tokens_total": ("tokens_total",),
"slots_busy": ("sessions.list", "sessions.active"),
"running": ("requests_processing", "num_requests_running"),
"waiting": ("requests_deferred", "num_requests_waiting"),
"cache_hit": ("prefix_cache_hit_ratio", "prompt_cache_hit_ratio"),
"spec_accept": ("spec_accept_rate",),
"queue_mean": ("request_queue_time_seconds__mean",),
"preemptions": ("num_preemptions_total",),
"device_lost": ("device_lost_total",),
}
#: rows that are incidents, not stats: absent where the engine does not count them.
ALARM_ROW_KEYS = ("preemptions", "device_lost")
def _strip_rows(js: str) -> dict[str, str | None]:
"""The served `STRIP_ROWS` table: row key -> its `only` scope (`None` = every engine)."""
body = _code(js)
start = body.index("const STRIP_ROWS = [")
table = body[start:body.index("\n];", start)]
rows: dict[str, str | None] = {}
for chunk in table.split("{ key: '")[1:]:
scoped = re.search(r"only: \[([^\]]*)\]", chunk)
rows[chunk.split("'")[0]] = scoped.group(1) if scoped else None
return rows
def _strip_render_path(js: str) -> str:
"""Just `renderStripCard()` — the whole code path that paints one strip card."""
js = _code(js)
return js[js.index("function renderStripCard("):js.index("function pruneEngines(")]
def test_llm_stats_strip_sits_between_the_fleet_block_and_the_panels(client) -> None:
"""The strip is there, ships hidden, names itself, and appears only with engines.
An unconfigured engine gets no card and no strip (invariant 3) — the section is empty in
the markup and `page-llm.js` unhides it only when the payload has an engine to draw.
"""
html = _code(client.get("/llm").text)
assert 'id="strip"' in html, "/llm lost the per-engine stats strip"
assert html.index('id="fleet"') < html.index('id="strip"') < html.index('id="engines"'), \
"the strip is no longer between the fleet block and the engine panels"
start = html.index('<section class="strip"')
tag = html[start:html.index(">", start)]
assert "hidden" in tag, "the strip no longer ships hidden"
assert 'aria-label="Engine summary"' in tag, "the strip is a region that names nothing"
js = client.get("/static/page-llm.js").text
assert "$('#strip').hidden = payloads.length === 0" in js
def test_llm_strip_card_is_one_card_per_engine_of_shared_vocabulary(client) -> None:
"""`stripCard()` = `.card` + `.kv` + `.bar` + `.chip.fail`: a layout, not a new component.
Dot, name and age in the head (reachability, the same distinction the panels and the
fleet block draw), the KV bar, the rows keyed by payload field, the alarm container.
"""
js = client.get("/static/page-llm.js").text
card = js[js.index("function stripCard("):js.index("function renderStripCard(")]
assert "card.className = 'card stat'" in card
assert "card.dataset.engine = engine" in card, "the card stopped naming its engine"
assert '<span class="dot idle"></span>' in card
assert '<span class="faint mono updated"></span>' in card, "the card lost its timestamp"
assert '<span class="bar" data-kv-bar><i></i></span>' in card, "the KV bar left `.bar`"
assert '<dl class="kv">' in card, "the strip rows left the shared `.kv` vocabulary"
assert 'data-row="${row.key}"' in card
assert 'data-k="alarms"' in card
# the wrapper is why the shared `.kv` grid has to move *inside* it: applied to the
# wrappers, `max-content 1fr` lays two rows side by side and the card reads as a
# column of orphaned labels (found in the browser, not in a test failure)
css = client.get("/static/dash.css").text
section = css[css.index("/* ---- engine stats strip"):css.index("/* ---- fleet totals")]
assert ".strip .kv { margin: 0; grid-template-columns: 1fr; }" in section
assert ".strip .kv > div {" in section
assert "grid-template-columns: max-content minmax(0, 1fr)" in section
refresh = js[js.index("async function refresh("):js.index("async function refreshEvents(")]
assert "state.strips.get(payload.engine)" in refresh, "the strip rebuilds its cards per poll"
assert "$('#strip').appendChild(strip)" in refresh
assert "renderStripCard(strip, payload, stateRow)" in refresh
prune = js[js.index("function pruneEngines("):js.index("function makeCharts(")]
assert "state.strips" in prune and "state.panels" in prune, \
"an engine that leaves the payload leaves a card behind"
def test_llm_strip_scopes_its_rows_to_the_engines_that_publish_them(client) -> None:
"""gufo gets slots and speculative decoding; vLLM gets running and queue time."""
rows = _strip_rows(client.get("/static/page-llm.js").text)
assert rows["slots_busy"] == "'gufo'" and rows["spec_accept"] == "'gufo'"
assert rows["running"] == "'vllm'" and rows["queue_mean"] == "'vllm'"
assert rows["waiting"] is None and rows["cache_hit"] is None and rows["tokens_total"] is None
js = client.get("/static/page-llm.js").text
scoped = js[js.index("function stripRowsFor("):js.index("function stripCard(")]
assert "row.only" in scoped and "row.only.indexOf(engine)" in scoped
def test_llm_strip_rows_name_cells_the_engines_publish(client, metrics_text) -> None:
"""The strip renders what the payload sends — no row whose cell no engine produces.
`STRIP_ROW_PATHS` mirrors the JS table; every metric cell is checked against the
capture-parsed metrics of each engine that would render that row, so a rename in
`sources/llm.py` fails here instead of printing a permanent `n/a` in the browser, and a
row scoped to one engine cannot leak into the other's card.
"""
rows = _strip_rows(client.get("/static/page-llm.js").text)
assert set(rows) == set(STRIP_ROW_PATHS), \
f"the served strip rows drifted: {set(rows) ^ set(STRIP_ROW_PATHS)}"
parsed = {"gufo": parse_gufo_metrics(metrics_text("gufo")),
"vllm": parse_vllm_metrics(metrics_text("vllm"))}
for key, only in rows.items():
paths = STRIP_ROW_PATHS[key]
if paths[0].startswith("sessions."):
continue # its own payload object; pinned on the JS below
shown = {engine: metrics for engine, metrics in parsed.items()
if not only or engine in only}
present = [engine for engine in shown if any(path in shown[engine] for path in paths)]
if key in ALARM_ROW_KEYS:
# an alarm the engine does not count simply is not there: the chip renders only
# while non-zero, so absence *is* the zero state rather than a dead row
assert present, f"no engine publishes {'/'.join(paths)} for the {key} alarm"
continue
assert len(present) == len(shown), (
f"the {sorted(set(shown) - set(present))} card would print `{key}` as n/a forever "
f"({'/'.join(paths)} is not published)")
def test_llm_strip_slot_count_counts_published_slots_and_never_invents_zero(client) -> None:
"""`slots busy` is a count of `/slots` rows; when /slots did not answer it is `n/a`.
Counting rows the server published is a count of published facts. An empty list from a
server that never answered is not a measurement of an idle engine, so the row falls back
to the published `sessions.active` carrying the API's own `detail` string (rule 2).
"""
js = _code(client.get("/static/page-llm.js").text)
slots = js[js.index("case 'slots_busy':"):js.index("case 'cache':")]
assert "sessions.source" in slots and "sessions.list" in slots
assert "is_processing" in slots, "the count ignores the authoritative busy verdict"
assert "sessions.detail" in slots, "the fallback lost the API's own provenance"
def test_llm_strip_kv_bar_is_coloured_by_the_payload_verdict_only(client) -> None:
"""The strip's KV bar is the page's one rated cell: `severity` in, class out.
`sources/llm.py` rates `llm.kv_cache_usage_pct`, the ratio→percent conversion the panel
cell used to make rides with it, and `Dash.severityFrom` is the only thing that decides a
colour — no threshold in the page, and the stylesheet adds no palette (`ok` stays quiet
ink, exactly like the `.cell` rows and the fleet table).
"""
body = _strip_render_path(client.get("/static/page-llm.js").text)
assert "severityFrom(kv.severity)" in body
assert "cellValue(engine, metrics, sessions, 'cache')" in body
assert "Math.max(0, Math.min(100, kv.value))" in body, "the fill stopped clamping"
assert body.count("severityFrom") == 1, "a second strip row invented a verdict"
css = client.get("/static/dash.css").text
section = css[css.index("/* ---- engine stats strip"):css.index("/* ---- fleet totals")]
assert ".stat-bar.warn .value { color: var(--warn); }" in section
assert ".stat-bar.fail .value { color: var(--fail); }" in section
assert ".stat-bar.ok" not in section, "a healthy KV reading must stay quiet ink"
assert ".prov:empty" in section, "an empty provenance tag draws a pill of nothing"
def test_llm_strip_throughput_carries_provenance_without_colour(client) -> None:
"""`prompt tok/s` and `decode tok/s` keep the `Δ<window>` tag and stay ink.
The tag is generated from the cell's `source` (never hardcoded), the exact source rides
in the row's tooltip, and `severity.py` rates no throughput — so nothing here may paint
it (0 tok/s on an idle engine is healthy).
"""
js = client.get("/static/page-llm.js").text
body = _strip_render_path(js)
assert "row.rate ? provenance(value) : ''" in body
assert "notes.join" in body and "value.source" in body
assert "severityFor" not in js
def test_llm_strip_and_panel_print_no_value_twice(client) -> None:
"""One value, one home — the UPS hero row's rule, applied across two components.
Task 03 lifted sessions, queue depth, KV pressure, the cache-hit and spec-accept percents,
the lifetime token counter and the alarm chips out of the panel cell list rather than
drawing them twice. The two throughput heroes are the exemption the task names: the panel
keeps its hero row (so does `llm_monitor`, which carries the pair in both places), and
everything else appears once. Queue time moved with its p95 foot, so the move lost nothing.
"""
js = _code(client.get("/static/page-llm.js").text)
start = js.index("const CELLS = [")
panel_keys = set(re.findall(r"\{ key: '(\w+)'", js[start:js.index("\n];", start)]))
assert panel_keys == {"decode", "prompt", "ttft", "tpot"}, \
f"the panel cell list changed shape: {sorted(panel_keys)}"
shared = set(_strip_rows(js)) & panel_keys
assert shared == {"decode", "prompt"}, f"printed in the strip and the panel: {sorted(shared)}"
body = _strip_render_path(js)
assert "p95Value(metrics, row.key)" in body, "queue time lost its p95 in the move"
def test_llm_strip_prints_no_peak_context_number(client) -> None:
"""The phase's assumption, pinned: `llm_monitor`'s peak-ctx row does not come over.
Its cell is `llamacpp:n_tokens_max`, a since-start maximum rather than a glance number,
and the task says this strip gets no peak-context cell. The series stays stored and
queryable from `/api/llm/history` — not plotting it is not removing it.
"""
js = _code(client.get("/static/page-llm.js").text)
assert "n_tokens_max" not in js, "/llm reads the peak-context gauge again"
assert not [key for key in _strip_rows(js) if "peak" in key]
def test_llm_fleet_block_leads_the_page_with_the_engine_sums(client) -> None:
"""The page opens with llm_monitor's aggregate block: the sums no panel can say.
Phase 04 retired these numbers to counts; the operator asked for the aggregate
throughput back (`TODO.md:5`), and phase 07 restores it in `llm_monitor`'s shape —
status dot, "all engines", an `N of M engines` tag, the note that the sums cover only
the engines answering now, then running / waiting / prompt / decode / total tokens.
The counts-only card it replaces (`#totals`, `f-live`, `f-active`) is gone, ids and all:
an element nothing writes is the orphan this repo pins against.
"""
html = _code(client.get("/llm").text)
assert 'id="fleet"' in html, "/llm lost the aggregate block"
assert html.index('id="fleet"') < html.index('id="engines"'), \
"the fleet block is no longer above the engine panels"
for part in ("fleet-dot", "fleet-count", "f-running", "f-waiting", "f-prompt",
"f-decode", "f-tokens"):
assert f'id="{part}"' in html, f"/llm lost the fleet block's {part}"
for words in ("all engines", "sums only engines answering now"):
assert words in html, f"/llm lost the fleet block's `{words}` line"
for gone in ('id="totals"', "f-live", "f-total", "f-active", "f-kv", "seam-note"):
assert gone not in html, f"/llm still carries {gone} from the retired counts card"
js = client.get("/static/page-llm.js").text
body = _fleet_render_path(js)
assert "block.hidden = total < 2" in body, "the fleet block no longer hides for one engine"
assert "hidden" in _fleet_tag(html), \
"the block no longer ships hidden, so a single engine would show a fleet row"
def test_llm_fleet_block_reads_only_paths_the_api_publishes(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Every number the block prints is a `fleet` key of `/api/llm/snapshot`.
The page sums nothing itself — `fleet_totals()` does — so the field table in
`page-llm.js` is a list of API paths, and a rename in `sources/llm.py` must fail here
rather than print a permanent `n/a` in the browser. Pinned with both engines configured
(the keys exist, empty-handed because nothing answered) and with neither (404
`configured: false` without dialling, invariant 3 — and the shipped block stays hidden).
"""
with build_client(tmp_path, monkeypatch, gufo_url=DEAD_URL,
vllm_url=DEAD_URL) as test_client:
fleet = test_client.get("/api/llm/snapshot").json()["fleet"]
assert set(fleet) >= FLEET_BLOCK_PATHS, FLEET_BLOCK_PATHS - set(fleet)
# the served field table is exactly the sum rows, in llm_monitor's order
js = test_client.get("/static/page-llm.js").text
table = _fleet_field_metrics(js)
assert table == ["requests_active", "requests_waiting",
"prompt_tokens_per_second", "decode_tokens_per_second",
"tokens_total"], "the fleet row changed shape without the API"
assert set(FLEET_SUM_PATHS) == set(table), "the block totals something the API does not"
# and the tag's two numbers are API paths too, not arithmetic the page invented
body = _fleet_render_path(js)
assert "fleet.engines_total" in body and "fleet.engines_live" in body
with build_client(tmp_path, monkeypatch) as bare:
response = bare.get("/api/llm/snapshot")
assert response.status_code == 404
assert response.json()["configured"] is False
html = _code(bare.get("/llm").text)
assert 'id="fleet"' in html and "hidden" in _fleet_tag(html), \
"an unconfigured engine must not leave a fleet row on the page"
def test_llm_fleet_totals_drop_an_engine_that_stopped_answering(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A total presented as current is current: a dead engine contributes nothing.
`fleet_totals()`'s docstring is the contract and the block is where it is read. The
injected payload survives the dead-port polls (a failed poll keeps the last good one), so
these are the numbers a browser would get with gufo answering and vLLM down — the live
engine's sums verbatim, `engines_live` one of two, and not one zero where a number the
dead engine never published would sit (rule 2).
"""
with build_client(tmp_path, monkeypatch, gufo_url=DEAD_URL,
vllm_url=DEAD_URL) as test_client:
hub = test_client.app.state.ctx.hub # type: ignore[attr-defined]
hub.jobs["llm:gufo"].state.payload = {
"engine": "gufo",
"metrics": {"decode_tokens_per_second": {"value": 41.0},
"prompt_tokens_per_second": {"value": 17.5},
"requests_processing": {"value": 2.0},
"requests_deferred": {"value": 1.0},
"tokens_total": {"value": 512.0}},
"sessions": {"active": 2, "list": []}}
fleet = test_client.get("/api/llm/snapshot").json()["fleet"]
assert fleet["engines_total"] == 2 and fleet["engines_live"] == 1
assert fleet["decode_tokens_per_second"] == pytest.approx(41.0)
assert fleet["prompt_tokens_per_second"] == pytest.approx(17.5)
assert fleet["requests_active"] == pytest.approx(2.0)
assert fleet["requests_waiting"] == pytest.approx(1.0)
assert fleet["tokens_total"] == pytest.approx(512.0)
# and with neither engine answering, every sum is null, never a confident 0
hub.jobs["llm:gufo"].state.payload = {"engine": "gufo", "metrics": {},
"sessions": {"active": None, "list": []}}
empty = test_client.get("/api/llm/snapshot").json()["fleet"]
assert empty["engines_live"] == 0
assert [empty[key] for key in FLEET_SUM_PATHS] == [None] * len(FLEET_SUM_PATHS)
def test_llm_fleet_block_proves_where_its_sums_come_from(client) -> None:
"""The seam tooltip, the excluded-engine tooltip, and an `n/a` with no unit.
Two provenance rules the rest of the app holds to, on the one block that adds engines
together: the prompt sum carries `fleet.seams` — the API's own words on why gufo's and
vLLM's prompt counters are not the same quantity — and the tag's tooltip names the
engines `fleet_totals()` left out, so a partial total says so in words and not only by
being dimmed. And a sum the API did not publish prints `n/a` with its unit cleared: "n/a
tok/s" would read as a measurement of nothing.
"""
js = _code(client.get("/static/page-llm.js").text)
body = _fleet_render_path(js)
assert "fleet.seams" in body, "the prompt sum lost the API's seam note"
assert "not answering — excluded from every sum below" in body, \
"the tag no longer names the engines left out of the totals"
assert "value == null ? ''" in body, "an n/a sum can still print a unit"
assert "value == null ? ''" in body, "an n/a sum can still print a unit"
def test_llm_fleet_block_colours_no_number(client) -> None:
"""No verdict on the aggregate row — and no dimming of anything but the tag.
`severity.py` rates no throughput and no queue depth (0 tok/s on an idle fleet is
healthy), so the block renders ink: the render path touches no `severity`, calls no
`Dash.hero()`, and the stylesheet section for the block reaches for no verdict token.
The one coloured thing on it is the shared **reachability** dot — the same distinction
the engine panels draw between their numbers and their header dot — and the partial-fleet
signal is the shared `.stale` dim on the count tag, which is opacity, not a verdict.
"""
js = _code(client.get("/static/page-llm.js").text)
body = _fleet_render_path(js)
assert "severity" not in body, "the fleet block invented a verdict"
assert "hero(" not in body, "the fleet block drew a hero number it cannot rate"
assert "setDot(" in body and "markStale(tag" in body, \
"a dead engine no longer dims the tag or darkens the block's dot"
css = client.get("/static/dash.css").text
section = _css_section(css, "fleet totals")
for token in ("var(--ok)", "var(--warn)", "var(--fail)"):
assert token not in section, f"the fleet block coloured a number with {token}"
assert ".fleet-head" in section and ".fleet-note" in section, \
"the fleet block's head row lost its styling"
#: Elements the browser treats as empty, so they never open a nesting level.
_VOID_TAGS = {"meta", "link", "br", "img", "input", "hr", "source", "track", "wbr"}
def _depth_at(html: str, needle: str) -> int:
"""How deeply the `needle` sits inside `<main>` (1 = a direct child of it).
The `~` in `#banners:empty ~ #fleet` only resolves between **siblings**, so the gap rule
depends on the markup as much as on the stylesheet. Counting tags is enough here and keeps
the check on the served bytes: no HTML parser, no dependency, and comments are already
gone by the time `_code` hands the page over.
"""
body = html[html.index("<main>"):html.index(needle, html.index("<main>"))]
depth = 0
for closing, tag, _attrs, self_closed in re.findall(
r"<(/?)([a-zA-Z][\w-]*)([^>]*?)(/?)>", body):
if self_closed or tag.lower() in _VOID_TAGS:
continue
depth += -1 if closing else 1
return depth
def test_llm_section_stack_keeps_the_shared_vertical_rhythm(client) -> None:
"""Fleet block → engine summaries → engine panels sit the shared `.85rem` apart.
`TODO.md:4`: the fleet section had no spacing between it and the engine summaries. Every
boundary below it was already handled inline — the section under `#strip` carries
`margin-top:.85rem` — so `#fleet` and `#strip` skipped the rhythm and the top of the page
read as one unbroken wall. Measured in Chromium at the 16px root (`data/logs/`
`llm-fleet-spacing*.png`), before → after:
| gap | before | after |
|-----|--------|-------|
| `#fleet` → `#strip` | **0 (flush)** | 13.59px |
| `#strip` → engine panels | 13.59px | 13.59px |
| banner slot → `#fleet`, no banner | 13.59px | 13.59px |
| banner slot → `#fleet`, one banner up | 13.59px | 13.59px (`margin-top: 0px`) |
One rendered distance moves, and it is the one the operator named. What the *shape* of the
two rules buys is that no other state can put it back:
* `#strip`'s gap is **unconditional**, because below two engines `#fleet` is `[hidden]`
(`display:none !important`, so it contributes nothing) and the strip is then the first
visible section: the 13.59px below the banner slot in that state is now the strip's own
margin rather than one borrowed from a slot above it, which is what `#fleet ~ #strip`
would have been;
* `#fleet`'s gap is guarded by `#banners:empty`, so the block owns its top gap exactly when
nothing occupies the slot above it, and a banner's own `margin-bottom:.85rem` stays the
single gap under it. An unguarded `#fleet { margin-top }` is a second rule for one gap:
margin collapsing hides it at `.85rem`/`.85rem` today, and it doubles the moment either
margin is changed. The empty slot keeps its inline bottom margin, so head → fleet stays
the 1rem + .85rem the other two banner pages (`/`, `/ups`) already render.
Pinned on the served bytes because a stylesheet "cleanup" that drops either rule is
invisible in a diff and only shows up as a page that reads worse.
"""
raw = _css(client)
section = _css_section(raw, "fleet totals")
assert "#strip { margin-top: .85rem; }" in section, \
"the fleet block and the engine summaries render flush again (TODO.md:4)"
assert "#banners:empty ~ #fleet { margin-top: .85rem; }" in section, (
"the gap above the fleet block stopped belonging to the block: a banner-less page "
"would then have it only as long as the empty banner slot keeps its inline margin")
# one rule per element: a second one (or an unguarded `#fleet`) is the double gap
css = _code(raw)
for element, expected in (("#strip", {"#strip"}),
("#fleet", {"#banners:empty ~ #fleet"})):
selectors = {s.strip() for s in re.findall(rf"([^{{}}]*{element}[^{{}}]*)\{{", css)}
assert selectors == expected, (
f"dash.css styles {element} with {sorted(selectors)} — exactly one rule, the "
"guarded one, or the page grows a double gap or loses the single one")
html = _code(client.get("/llm").text)
for element in ("banners", "fleet", "strip"):
assert _depth_at(html, f'id="{element}"') == 1, (
f"`#{element}` is no longer a direct child of <main>, so the `~` gap rule cannot "
"match: rewrite the selectors for the new structure instead of dropping the gap")
assert html.index('id="banners"') < html.index('id="fleet"') < html.index('id="strip"') \
< html.index('id="engines"'), "the section stack was re-ordered"
# the inline half of the rhythm, which the two rules are written around
assert 'style="margin-bottom:.85rem"' in html[:html.index('id="fleet"')], (
"#banners lost the inline bottom margin that is the banner → fleet gap, and today the "
"only thing spacing the page head from the block")
after_strip = html[html.index('id="strip"'):]
assert after_strip.count('style="margin-top:.85rem"') == 3, \
"a section below the strip lost the inline gap the strip's rule hands off to"
# ---- the `/llm` gufo slot grid (phase 07 task 04) -------------------------
#
# `llm_monitor`'s slot cards (`index.html` ~L1179), rebuilt against the capture instead of a
# memory: one keyed card per gufo slot, every cell naming **one** key of
# `tests/fixtures/slots_gufo.json`. Two things are pinned below, because they are the two ways
# this component failed before: a card that reads a key the server does not publish (dashes
# forever), and a card that is rebuilt every poll so a busy slot flickers in front of you.
#: The keys a slot card reads, and where they live: `field` (and its optional `sub` /
#: `ratioAgainst` partners) index the row, or its first `next_token` entry when `path` says so.
SLOT_ROW_PATHS = ("field", "sub", "ratioAgainst")
def _slot_table(js: str) -> list[dict[str, str | None]]:
"""The served `SLOT_FIELDS` table, one dict per printed cell."""
body = _code(js)
start = body.index("const SLOT_FIELDS = [")
table = body[start:body.index("\n];", start)]
cells: list[dict[str, str | None]] = []
for chunk in table.split("{ key: '")[1:]:
cell: dict[str, str | None] = {"key": chunk.split("'")[0]}
for option in (*SLOT_ROW_PATHS, "path"):
found = re.search(rf"{option}: '([a-z_0-9]+)'", chunk)
cell[option] = found.group(1) if found else None
cells.append(cell)
return cells
def _slot_js(js: str) -> str:
"""The slot grid's whole code path, comments out: `slotValue()` to the end of the block."""
body = _code(js)
return body[body.index("function slotValue("):body.index("function makeCharts(")]
def _slot_markup(js: str) -> str:
"""Just `slotSection()` — the markup the grid, its label and its empty states are built from."""
body = _code(js)
return body[body.index("function slotSection("):body.index("function renderPanel(")]
def test_llm_gufo_panel_holds_the_slot_grid_under_its_label(client) -> None:
"""`sessions (GET /slots)`, then the keyed grid, then the note about what it cannot contain.
The label is `llm_monitor`'s, the grid ships `hidden` (the empty state is a sentence, never
a card of nothing), and the note stays because a queued (deferred) request is not in
`/slots` at all — without that line an idle grid reads as "nothing waiting".
"""
js = client.get("/static/page-llm.js").text
markup = _slot_markup(js)
assert "sessions (GET /slots)" in markup, "/llm lost the slot grid's label"
# label, grid, empty state, note — in that order, and the grid and the empty state both
# ship hidden so nothing paints before the payload says which of them is true
order = ['class="section-label" data-slots-label', "sessions (GET /slots)",
'<div class="slots" data-slots role="list" hidden>',
'class="slots-empty" data-slots-empty hidden',
'class="slots-note" data-slots-note hidden']
position = -1
for needle in order:
found = markup.index(needle)
assert found > position, f"`{needle}` is out of order in the slot section"
position = found
assert "queued (deferred) requests are not in /slots" in js, "the queued-request note is gone"
assert "renderSlotGrid(card, enginePayload)" in js, "nothing draws the grid any more"
# the label is a heading and the grid a list: a screen reader gets the block's name and
# its item count, which a stack of divs does not say
assert '<h4 class="section-label" data-slots-label>' in markup, \
"the slot section's label stopped being a heading"
assert 'data-slots role="list"' in markup, "the grid lost its list semantics"
# the grid's own markup: a keyed card, the state pill, and one dd per printed cell
card = js[js.index("function slotCard("):js.index("function slotSub(")]
assert "setAttribute('role', 'listitem')" in card, "the slot cards left the list"
assert 'data-slot-cell="${cell.key}"' in card, "the card cells are not table-driven"
assert 'data-pill' in card and 'data-mark' in card, "the card lost its state pill or glyph"
def test_llm_slot_grid_style_is_a_marker_and_not_a_verdict(client) -> None:
"""A busy slot is a border, a glyph and a word — `severity.py` rates no slot.
Phase 06 settled this argument for GPU utilisation on an LLM server: a slot generating
tokens is the job, so the accent colour says *this is the live one* and no `--ok/--warn/
--fail` token appears anywhere in the section. The glyph (`○`/`●`) and the pill's word mean
the marker survives monochrome, which is the battery glyph's rule applied here.
"""
css = client.get("/static/dash.css").text
section = _css_section(css, "engine slot grid")
for selector in (".slots {", ".slot {", ".slot.busy", ".slot-top", ".slot-id",
".slot-model", ".slot dt", ".slot dd", ".slots-empty", ".slots-note",
".section-label"):
assert selector in section, f"dash.css lost the {selector} rule"
assert "var(--accent)" in section, "the busy marker stopped using the non-verdict accent"
for token in ("var(--ok)", "var(--warn)", "var(--fail)"):
assert token not in section, f"the slot grid coloured a state with {token}"
js = client.get("/static/page-llm.js").text
assert "'\\u25cf'" in js or "'●'" in js, "the busy glyph left the card"
assert "processing" in _slot_js(js), "the pill stopped saying the state in a word"
def test_llm_slot_cells_each_name_one_key_the_capture_publishes(client) -> None:
"""Every printed cell traces to a key of `tests/fixtures/slots_gufo.json`.
One `field` per cell and no `a ?? b`: the deleted table's `slot.n_prompt ??
slot.prompt_tokens` guessing is what made it print dashes and then plausible noise. A key
the capture does not have fails here, and the alias the capture answers beside `id_task`
must not appear in the page at all — the contract names the canonical one.
"""
# comments out: this test bans names the prose is allowed to explain
js = _code(client.get("/static/page-llm.js").text)
cells = _slot_table(js)
assert cells, "the slot grid has no cell table to read"
assert [cell["key"] for cell in cells] == ["req", "prompt", "cache", "generated",
"remain", "ctx"], \
"the slot card's cell set changed without the capture"
for cell in cells:
assert cell["field"], f"the {cell['key']} cell names no row key"
rows = load_slots_capture()
assert rows, "the capture is empty, so this test would prove nothing"
for cell in cells:
for option in SLOT_ROW_PATHS:
key = cell[option]
if key is None:
continue
in_next = cell["path"] == "next_token"
required = REQUIRED_NEXT_TOKEN_FIELDS if in_next else REQUIRED_ROW_FIELDS
optional = OPTIONAL_NEXT_TOKEN_FIELDS if in_next else OPTIONAL_ROW_FIELDS
assert key in required or key in optional, (
f"the {cell['key']} cell reads {key!r}, which no contract table declares — the "
"page would be reading a field nobody pinned")
for row in rows:
source = row["next_token"][0] if in_next else row
if key in required:
assert key in source, (
f"the {cell['key']} cell reads {key!r}, which "
f"{'next_token[0]' if in_next else 'the row'} does not publish — this is "
"how the deleted table rendered dashes")
elif key in source: # optional: `prob` is on no captured row, and that is legal
assert isinstance(source[key], (int, float)), (
f"{key!r} is {source[key]!r}, which the cell cannot format")
assert cells[0]["field"] == CANONICAL_TASK_ID_FIELD, \
"the request cell stopped reading the canonical task id"
for alias in TASK_ID_ALIASES:
assert alias not in js, f"page-llm.js renders the {alias} alias the contract does not name"
# the old table's invented names stay invented, and so does its per-slot speed
for gone in ("n_predict", "decode_per_second", "cache_usable", "n_ctx_used"):
assert gone not in js, f"page-llm.js reads the invented {gone} again"
def test_llm_slot_card_prints_no_speed_it_cannot_measure(client) -> None:
"""No per-slot speed cell: the capture publishes no per-slot rate key.
`llm_monitor`'s grid has one, and so did the deleted table (`decode_per_second`) — which is
why it was dashes. A page-side Δn_decoded/Δt would be a measurement with no estimator, no
`Δ<window>` label and no `source` string, i.e. rule 2 with a confident number printed on it,
so the number stays on the engine's strip card and panel, where `rates.py` owns it.
"""
js = _code(client.get("/static/page-llm.js").text)
assert not [cell for cell in _slot_table(js)
if cell["key"] in {"speed", "tok_s", "decode"}], "a slot cell grew a rate"
assert "decode_per_second" not in js
def test_llm_slot_sentinels_print_a_word_or_na_never_a_negative_number(client) -> None:
"""`-1` is llama.cpp's "nothing in this slot", and it never becomes a countdown.
An idle slot answers `id_task: -1` and `next_token[0].n_remain: -1` (both in the capture).
The request cell says `idle` in a word; the remaining cell prints `n/a`. Neither prints a
negative number, and neither prints the `0` that would read as "one token from done".
"""
body = _slot_js(client.get("/static/page-llm.js").text)
assert "value < 0 ? 'idle'" in body, "the request cell prints the -1 sentinel again"
assert "value >= 0 ? value : null" in body, "the remaining cell prints the -1 sentinel"
def test_llm_slot_card_header_reads_only_fields_the_contract_declares(client) -> None:
"""The card top prints `model`, `speculative`, `id` and the state pair — and nothing else.
`id` keys the card (escaped once at build time), `is_processing` is the authoritative pill,
`state` labels a number the boolean cannot express, and `model` + `speculative` are the
footnote beside them. Each is declared in a contract table, so a rename fails there. The
user-content field a busy row carries (`prompt`) is the one key this page must never read.
"""
js = client.get("/static/page-llm.js").text
header = js[js.index("function renderSlotCard("):js.index("function renderSlotGrid(")]
read = {"is_processing", "state", "model", "speculative"}
for key in read:
assert f"row.{key}" in header, f"the slot card header stopped reading {key}"
declared = set(REQUIRED_ROW_FIELDS) | set(OPTIONAL_ROW_FIELDS)
assert read <= declared, "the header reads a field no contract table declares"
for field in USER_CONTENT_FIELDS:
assert field not in header, f"the slot card prints the user's {field}"
assert "escapeHtml(id)" in js, "the slot id reached the markup unescaped"
def test_llm_slot_cards_are_keyed_created_once_and_patched_in_place(client) -> None:
"""A busy slot does not get rebuilt every poll — and a slot that leaves loses its card.
`appendChild` of the existing node keeps DOM order == slot order; everything else is a text
patch, so a poll cannot flicker a card or re-announce it to assistive tech for a difference
of a few tokens. An engine that leaves the payload loses its card map with its panel, or the
refs of removed nodes would be re-appended when it came back.
"""
js = client.get("/static/page-llm.js").text
body = _slot_js(js)
assert "state.slots.get(enginePayload.engine)" in body, "the grid stopped keying its cards"
assert "if (!refs) {" in body and "refs = slotCard(id)" in body
assert "grid.appendChild(refs.root)" in body, "slot order is no longer kept by moving nodes"
assert "byId.delete(id)" in body, "a slot that left keeps its card forever"
assert "byId.forEach((refs) => refs.root.remove())" in body, \
"the empty state left cards on the page"
assert "setTxt(box.value" in body, "the cell values stopped being patched in place"
assert "innerHTML" not in body.split("function renderSlotCard(")[1], \
"a card is rebuilt from markup on a poll"
prune = js[js.index("function pruneEngines("):js.index("/* ---- the gufo slot grid")]
assert "state.slots.delete(engine)" in prune, "a pruned engine kept its slot cards"
def test_llm_slot_grid_says_why_there_are_no_rows(client) -> None:
"""Two honest empty states, and neither of them is a card or a 0-count claim.
`/slots` did not answer → the API's own `sessions.detail` sentence (the same words the
strip's session row carries). It answered with no rows → "engine reports 0 slots", which is
a fact about that answer. `sessions.source` is the gate that tells the two apart.
"""
body = _slot_js(client.get("/static/page-llm.js").text)
assert "Boolean(sessions.source)" in body, "the grid cannot tell no-answer from no-rows"
assert "engine reports 0 slots" in body
assert "sessions.detail ||" in body, "the no-answer state lost the API's own sentence"
assert "no /slots answer from this engine" in body, "the no-answer state can print undefined"
def test_llm_vllm_panel_says_there_is_no_per_slot_json(client) -> None:
"""vLLM gets one line and no grid — no fake rows for a server that cannot answer.
`sources/llm.py`'s `_sessions()` publishes counts for vLLM and says so in `detail`; the
panel turns that into the `llm_monitor` line, with the API's sentence as the tooltip. The
gate is the engine's name (`SLOT_ENGINES`), not `sessions.list.length`, so an engine that
has not answered yet still shows its label and the honest empty sentence.
"""
js = client.get("/static/page-llm.js").text
markup = _slot_markup(js)
branch = markup.split("if (SLOT_ENGINES.indexOf(engine) < 0) {")[1].split("}")[0]
assert 'class="slots-note" data-slots-note' in branch, "the vLLM line lost its element"
assert "NO_SLOT_JSON" in branch and "vLLM exposes no per-slot JSON" in js
assert '<div class="slots"' not in branch, "the vLLM panel grew a grid it cannot fill"
body = _slot_js(js)
assert body.startswith("function slotValue("), "the grid's code path moved"
assert "if (!grid) {" in body, "renderSlotGrid no longer leaves a panel with no grid alone"
assert "note.title = enginePayload.sessions.detail" in body, \
"the vLLM line stopped carrying the API's own words"
def test_the_captured_slot_rows_fill_the_cells_the_grid_prints(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The capture, through the API, into the keys the served cell table reads.
The fixture rows are injected as a live job payload (the `/slots` passthrough verbatim), so
this is the render path's input as the browser would receive it: `sessions.source` set (the
grid's gate), every row present, and every key `SLOT_FIELDS` names resolvable in it. A
rename in `sources/llm.py` or in gufo fails here rather than leaving a card of `n/a`s.
"""
rows = load_slots_capture()
with build_client(tmp_path, monkeypatch, gufo_url=DEAD_URL,
vllm_url=DEAD_URL) as test_client:
hub = test_client.app.state.ctx.hub # type: ignore[attr-defined]
hub.jobs["llm:gufo"].state.payload = {
"engine": "gufo", "metrics": {"requests_processing": {"value": 1.0}},
"sessions": {"active": 1, "list": rows, "source": "gufo /slots", "detail": None},
}
engines = test_client.get("/api/llm/snapshot").json()["engines"]
gufo = next(row for row in engines if row["engine"] == "gufo")
sessions = gufo["sessions"]
assert sessions["source"] == "gufo /slots", "the grid's gate is gone from the payload"
assert sessions["list"] == rows, "the rows are no longer the passthrough the grid reads"
cells = _slot_table(test_client.get("/static/page-llm.js").text)
for cell in cells:
for option in SLOT_ROW_PATHS:
key = cell[option]
if key is None:
continue
in_next = cell["path"] == "next_token"
required = REQUIRED_NEXT_TOKEN_FIELDS if in_next else REQUIRED_ROW_FIELDS
for row in sessions["list"]:
source = row["next_token"][0] if in_next else row
if key not in required and key not in source:
continue # an optional sub-line this build does not publish
value = source[key]
assert isinstance(value, (int, float)) and not isinstance(value, bool), (
f"the {cell['key']} cell reads {key!r} = {value!r}, which the number "
"formatter cannot print")
def test_llm_charts_are_secondary_and_named(client) -> None:
"""The four trend charts stay, at half height, *below* the engine panels, each named.
Half of the old 165px and moved after `#engines` is the "cells read first" decision;
every `LineChart` gets an `ariaLabel` because the legend is a sibling that labels
nothing (`/system` did this in phase 03, `/llm` never had).
"""
html = _code(client.get("/llm").text)
assert html.index('id="engines"') < html.index('id="chart-decode"'), \
"the charts are back on top of the engine panels"
for chart in ("chart-decode", "chart-prompt", "chart-kv", "chart-queue"):
assert f'id="{chart}"' in html, f"/llm lost the {chart} chart"
js = client.get("/static/page-llm.js").text
assert "height: 110" in js and "height: 165" not in js
charts = js[js.index("function makeCharts("):js.index("function pointsFor(")]
assert charts.count("ariaLabel:") == 4, "an /llm chart lost its accessible name"
def test_llm_engine_panels_are_not_stretched_to_their_tallest_sibling(client) -> None:
"""A 5-row gufo panel beside a 7-row vLLM panel keeps its own height.
Found while comparing against `llm_monitor`: the shorter engine card grew 172 px of
blank card, and a blank panel reads as "no data" — the exact misreading the unconfigured
rules exist to prevent. Pinned on the served markup so the `.grid.items-start` rule in
`dash.css` cannot silently become dead CSS (and the void cannot silently come back).
"""
html = _code(client.get("/llm").text)
engines_div = html[html.index('<div class="grid'):html.index('id="engines"') + 40]
assert "items-start" in engines_div, "the engine grid stretches its cards again"
css = client.get("/static/dash.css").text
assert ".grid.items-start" in css, "the rule the engine grid relies on is gone"
def test_overview_is_exactly_two_panels(client) -> None:
"""The landing page answers one question: what is wrong, and what is the fleet doing.
Asserted on the served bytes because there is no JS test runner (a locked anchor):
the summary cards and the raw event feed are what made this page busy, and once a
panel is gone a browser is the only thing that would notice its return.
"""
html = client.get("/").text
assert html.count("<section") == 2, "the overview grew a third panel"
assert 'id="alerts"' in html and 'id="alerts-clear"' in html
assert 'aria-live="polite"' in html
assert 'id="fleet-table"' in html
for gone in ("sys-cpu", "sys-mem", "sys-disk", "sys-gpus", "llm-decode", "llm-kv",
"ups-list", 'id="events"'):
assert gone not in html, f"the overview still renders {gone}"
# the header's source dots are chrome owned by app.js, not the deleted card dots
assert 'id="dot-system"' not in html and 'id="nav-status"' in html
def test_overview_alert_rows_link_to_the_dashboard_that_explains_them(client) -> None:
"""Every alert carries the `href` its row turns into a link; the page invents none."""
page_js = client.get("/static/page-index.js").text
assert "alert.href" in page_js
css = client.get("/static/dash.css").text
for selector in ("ul.alerts li.ok .src", "ul.alerts li.warn .src", "ul.alerts li.fail .src",
"table.data td.warn", "table.data td.fail"):
assert selector in css, f"dash.css lost {selector}"
# the swap that puts a live problem above the fleet table
assert 'data-state="alerting"' in css
def test_overview_dims_the_fleet_row_of_a_host_that_stopped_answering(client) -> None:
"""A dead host keeps its last good numbers, so the row has to say they are stale.
`AGENTS.md` invariant 4: keep the payload and mark it stale, never zero it. The
overview's table is the one place those last good numbers still print, so it reads the
per-source verdicts `/api/summary` sends (`system.states`) and applies the shared
`.stale` class the other pages already use — no new palette, no JS threshold.
"""
page_js = client.get("/static/page-index.js").text
assert "system.states" in page_js, "the fleet table stopped reading reachability"
assert 'class="stale"' in page_js
assert ".stale" in client.get("/static/dash.css").text
# Which script owns which page, for the orphan check below. `charts.js` and `app.js` are
# loaded by every page, so an id either page code or shared chrome may read.
PAGE_SCRIPTS = {
"/": "page-index.js",
"/system": "page-system.js",
"/disks": "page-disks.js",
"/llm": "page-llm.js",
"/ups": "page-ups.js",
# `/gateway` (phase 29 task 04): the pipe map is built entirely in JS over the payload, so
# every `id` the page ships is written by this script or by a stylesheet rule.
GATEWAY_PAGE: "page-gateway.js",
# `/settings` is audited like a dashboard even though it is not one: an id nothing reads is
# dead markup whatever page it sits on, and this page has five new ones.
SETTINGS_PAGE: "page-settings.js",
# `/certs` too (phase 15 task 05): an authority panel and an agents table are markup, and
# every id in them is written by this page's own script.
CERTS_PAGE: "page-certs.js",
}
# `/login` is deliberately **not** in this table even though it is audited (phase 11): a
# signed-in client asking for it gets a 303 to the dashboard, so this helper's own
# redirect-following `get` would audit `/`'s markup and call it `/login`. The login page's
# orphan-`id`, CDN and masthead checks live in `tests/test_auth_api.py`, fetched unsigned, and
# `/static/page-login.js` is in `ASSETS` above.
SHARED_SCRIPTS = ("app.js", "charts.js")
def test_no_page_carries_an_id_nothing_reads(client) -> None:
"""An id no script queries, no stylesheet styles and no attribute points at is dead markup.
The summary cards were retired this way — a `<span id="sys-cpu">` left behind after its
writer was deleted is the same mistake in slower motion. Asserted on served bytes: the
browser console is otherwise the only thing that would notice.
"""
scripts = {name: client.get(f"/static/{name}").text for name in
(*SHARED_SCRIPTS, *PAGE_SCRIPTS.values(), "dash.css")}
for path, own_script in PAGE_SCRIPTS.items():
html = _code(client.get(path).text)
corpus = "".join(_code(scripts[name])
for name in (*SHARED_SCRIPTS, own_script, "dash.css"))
# ids the markup uses itself: labels, targets, `<label for>`
anchors = set(re.findall(
r'(?:aria-labelledby|aria-describedby|aria-controls|for)="([^"]+)"', html))
for element_id in re.findall(r'id="([^"]+)"', html):
quoted = (f'"{element_id}"' in corpus or f"'{element_id}'" in corpus
or f"#{element_id}" in corpus or element_id in anchors)
assert quoted, f"{path} has id=\"{element_id}\" that nothing reads"
# ---- the flavour-text audit (phase 08 task 02) ----------------------------
#
# `TODO.md:7`: "Get rid of most of the flavor text and add a note to AGENTS.md to not add it
# unless necessary. It just comes off as LLM generated slop, not helpful information." The
# test is one question per string — *would the operator act differently without it?* — and
# the answer decided the deletion. Copy that explains how the app builds its numbers is not
# helpful information; a provenance tag, a measured Δ window, and the sentence beside an
# empty panel are. `AGENTS.md` → *UI conventions* now carries the rule; the pins below are
# what stop the deleted sentences crawling back, because a screenshot only shows them once
# somebody already knows to look.
#: Fragments deleted from the served pages. The first two are the lines the operator quoted.
DELETED_FLAVOUR = (
# the overview's footer
"every number is scraped from the box",
"Sources the operator never configured are not problems",
# /system's "Per host" sub-line and its footer
"agent-reported values",
"each panel's charts cover only that host",
"numbers are measured, not inferred",
# /llm's footer, the one half-sentence of it that lived in the script too
"each throughput number carries its provenance",
"rates over the Δ window in each tag",
"a server you do not run",
# /ups's footer and its empty state
"commands are the only write path in this app",
"The dashboard speaks the NUT protocol",
# the three event feeds' header captions, all duplicated by an empty-state sentence
"unreachable / reachable / reboots",
"engine up / down",
"state changes, self-test results, commands sent",
)
@pytest.mark.parametrize("fragment", DELETED_FLAVOUR)
def test_no_page_narrates_its_own_pipeline(client, fragment: str) -> None:
"""The deleted copy is gone from every served byte, markup and script alike.
Asserted across all four pages and all five scripts rather than the file it came from:
the same sentence migrating into another page's card is the same slop, and a pin that
only guards its original file would let that through.
"""
for path in (*MARKUP_PAGES, *ASSETS):
body = client.get(path).text
assert fragment not in body, f"{path} narrates the app again: {fragment!r}"
def test_pages_leave_no_empty_narration_container(client) -> None:
"""A deleted sub-line removes its container, not just its words.
An empty `<span class="sub">` is markup that reserves a line for nothing — the page head
renders a gap where the sentence used to be. Elements the scripts fill (`#sub`,
`#alerts-count`) carry an `id` and are not in this category.
"""
for path in MARKUP_PAGES:
html = _code(client.get(path).text)
for cls in ("sub", "faint", "mono"):
assert f'<span class="{cls}"></span>' not in html, \
f"{path} keeps an empty .{cls} container"
def test_the_copy_the_operator_acts_on_survives_the_audit(client) -> None:
"""The other half of the deletion: functional copy stays.
Stripping flavour must not become stripping information. Every string here answers a
question the operator is asking while looking at the page — why is this panel empty,
is this number a measurement, is this battery real, is this host's data last-good —
and each one is the reason the audit deletes sentences and not labels.
"""
pages = {path: client.get(path).text for path in PAGES}
scripts = {name: client.get(f"/static/{name}").text for name in PAGE_SCRIPTS.values()}
# an empty panel says why, in terms the operator can act on
assert "No hosts configured" in pages["/"]
assert "HOST_TARGETS" in pages["/system"]
# …including the fourth page's: a `n/a` drive card is only actionable if the page names
# the two things that make SMART readable (the binary and the device-read grant).
assert "smartmontools" in pages["/disks"] + scripts["page-disks.js"]
assert "disk group" in scripts["page-disks.js"]
assert "No engines configured" in pages["/llm"]
assert "No UPS reporting" in pages["/ups"]
assert "instcmds" in pages["/ups"], "the UPS setup note stopped naming the grant that matters"
assert "3493" in pages["/ups"], "the page stopped warning that upsd's port is not always 3493"
# …and the sixth dashboard's empty state names the two things that make it read at all: the
# `HOST_TARGETS` name of the machine that routes, and an agent new enough to have read the
# proxy configs off it. A page that said "nothing to show" without either is not actionable.
assert "GATEWAY_HOST" in pages[GATEWAY_PAGE]
assert "HOST_TARGETS" in pages[GATEWAY_PAGE]
# …and the setup copy names the grant NUT actually checks. `README.md` is explicit: a
# dashboard user gets `instcmds`, never `actions = -`, so a page that told the operator
# to write that was sending them to fix the wrong line in `upsd.users`.
assert "actions = -" not in pages["/ups"] + scripts["page-ups.js"] + scripts["page-index.js"]
assert "instcmds" in scripts["page-ups.js"] and "instcmds" in scripts["page-index.js"]
# a Δ label is a measurement, and the rate's provenance travels with the rate
assert "windowLabel(" in scripts["page-system.js"]
assert "Δ" in scripts["page-llm.js"] and "function provenance(" in scripts["page-llm.js"]
assert "at the current load" in scripts["page-ups.js"]
# modelled data is labelled, and so is a feed that cannot contain what you are hunting
assert "SIMULATED" in scripts["page-index.js"] and "SIMULATED" in scripts["page-ups.js"]
assert "queued (deferred) requests are not in /slots" in scripts["page-llm.js"]
assert "Last good values" in scripts["page-index.js"]
def test_api_responses_are_not_cacheable(client) -> None:
for path in ("/api/config", "/api/health", "/api/summary"):
assert "no-store" in client.get(path).headers.get("cache-control", "")
# ---- history -------------------------------------------------------------- #
def test_history_returns_seeded_samples(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
db = str(tmp_path / "hist.sqlite3")
store = SampleStore(db)
now = time.time()
for i in range(30):
store.add("host", "driveripper", "cpu.usage_pct", 10.0 + i, ts=now - 300 + i)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=(Target("driveripper", DEAD_URL),)), store)
with TestClient(app) as client:
sign_in(client)
body = client.get("/api/history", params={"source": "host", "host": "driveripper",
"metric": "cpu.usage_pct", "range": "15m"})
assert response_ok(body)
points = body.json()["points"]
assert len(points) >= 25
assert points[-1]["value"] == pytest.approx(39.0)
store.close()
def test_history_rejects_an_unknown_range(client) -> None:
response = client.get("/api/history", params={"source": "host", "host": "h",
"metric": "m", "range": "banana"})
assert response.status_code == 400
def test_history_requires_the_series_coordinates(client) -> None:
assert client.get("/api/history").status_code == 422
@pytest.mark.parametrize(("raw", "seconds"), [
*[(key, float(seconds)) for key, seconds in RANGE_SECONDS.items()],
("45s", 45.0), ("13h", 46_800.0), ("7200", 7_200.0), (" 2D ", 172_800.0),
(None, 3_600.0), ("", 3_600.0),
])
def test_parse_range_reads_every_window_the_ui_offers(raw: Any, seconds: float) -> None:
"""Every chip key (5m…90d), any `<n><s|m|h|d>`, raw seconds, and the 1h default.
`parse_range` is the only thing between a typo'd query string and a chart of the wrong
window, and a wrong window is worse than an error: it still looks like data.
"""
assert parse_range(raw) == seconds
@pytest.mark.parametrize("raw", ["banana", "1w", "h", "-4h", "one"])
def test_parse_range_refuses_a_window_it_cannot_interpret(raw: str) -> None:
"""A 400 that names the value, never a silent fallback to 1h."""
with pytest.raises(HTTPException) as excinfo:
parse_range(raw)
assert excinfo.value.status_code == 400
assert raw in str(excinfo.value.detail)
# ---- the routes the *other* pages chart from -------------------------------- #
#
# `/` renders two panels now; `/system`, `/llm` and `/ups` still need these, and so does
# anything outside the browser. Retiring overview code must not retire them, so the shapes
# are pinned here rather than discovered by a page three phases from now.
def test_system_history_charts_every_configured_host_in_one_request(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""N hosts x M metrics, one round trip.
`/system` no longer uses the N-hosts half of this (every chart belongs to one host and
the page issues one request per host, pinned by
`test_every_system_chart_is_drawn_inside_its_own_host_panel`); the shape is kept because
the metric-list half is exactly what the per-host query costs one request with, and
because the endpoint is a public interface for programmatic consumers.
"""
with build_client(tmp_path, monkeypatch,
host_targets=(Target("driveripper", DEAD_URL),
Target("deskwork", DEAD_URL))) as test_client:
body = test_client.get("/api/system/history",
params={"metrics": "cpu.usage_pct,mem.used_pct",
"range": "1h"}).json()
assert body["hosts"] == ["driveripper", "deskwork"]
assert body["range_seconds"] == 3_600.0
assert len(body["series"]) == 4
assert {(item["host"], item["metric"]) for item in body["series"]} == {
("driveripper", "cpu.usage_pct"), ("driveripper", "mem.used_pct"),
("deskwork", "cpu.usage_pct"), ("deskwork", "mem.used_pct")}
# an explicit `hosts` list narrows it instead of adding to the default
narrowed = test_client.get("/api/system/history",
params={"metrics": "cpu.usage_pct",
"hosts": "deskwork"}).json()
assert narrowed["hosts"] == ["deskwork"]
def test_one_request_per_host_answers_every_per_host_chart(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The exact query `/system` issues per host returns every series that panel draws.
`page-system.js` sends one request per host carrying the utilisation trio
(`cpu.usage_pct`, `mem.used_pct`, `disk.used_pct`), both network rates, and one
`gpu.busy_pct@<index>` per card the payload reported — so a name missing here is a chart
that silently reads "no samples yet" in the browser, and a renamed series in
`sources/host.py` fails here rather than in a screenshot. This is the metric-name half of
the contract; `tests/test_system_contract.py` holds the payload-path half.
"""
with build_client(tmp_path, monkeypatch,
host_targets=(Target("driveripper", DEAD_URL),)) as test_client:
store = test_client.app.state.ctx.store # type: ignore[attr-defined]
now = time.time()
metrics = ["cpu.usage_pct", "mem.used_pct", "disk.used_pct",
"net.rx_bytes_per_s", "net.tx_bytes_per_s", "gpu.busy_pct@0"]
for metric in metrics:
store.add("host", "driveripper", metric, 12.5, ts=now - 60)
store.add("host", "driveripper", metric, 25.0, ts=now - 30)
body = test_client.get("/api/system/history", params={
"hosts": "driveripper", "metrics": ",".join(metrics), "range": "1h"}).json()
assert body["hosts"] == ["driveripper"], "one panel, one host"
assert [item["metric"] for item in body["series"]] == metrics
assert [len(item["points"]) for item in body["series"]] == [2] * len(metrics)
# a second card the host does not have answers empty, never zero and never 404
gap = test_client.get("/api/system/history",
params={"hosts": "driveripper", "metrics": "gpu.busy_pct@1"}).json()
assert gap["series"][0]["points"] == []
def test_every_history_endpoint_answers_exactly_what_the_per_series_path_did(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 14 task 02: the batched store door moved the cost of an N x M request, not its answer.
All four history routes now ask `store.history_many` for the whole batch instead of looping
`store.history` per pair. That is only safe if the JSON is the same document the loop built,
so this compares every route against points read straight out of the store with `query()` —
a third path, in neither the loop nor the batch — and pins the series order the pages parse
(host-major, metric-minor) plus the exact key set of one series entry.
"""
with build_client(tmp_path, monkeypatch, simulate=True, nut_host="127.0.0.1",
host_targets=(Target("driveripper", DEAD_URL),
Target("deskwork", DEAD_URL)),
gufo_url=DEAD_URL) as test_client:
store = test_client.app.state.ctx.store # type: ignore[attr-defined]
now = time.time()
writes = [
("host", "driveripper", "cpu.usage_pct", [1.0, 2.0, 3.0]),
("host", "driveripper", "mem.used_pct", [10.0, 20.0]),
("host", "deskwork", "cpu.usage_pct", [4.0, 5.0, 6.0]),
("host", "deskwork", "mem.used_pct", [30.0]),
("disks", "driveripper", "disk.temp_c@nvme0n1", [38.0, 39.0]),
("llm", "gufo", "llm.decode_tokens_per_second", [40.0, 45.0]),
# a UPS name the simulator does not export, so its 5-second poll cannot add rows to
# the series this assertion is about
("ups", "notaserial", "ups.charge_pct", [99.0, 100.0]),
]
for source, host, metric, values in writes:
store.add_many([Sample(source, host, metric, value, now - (len(values) - index) * 60)
for index, value in enumerate(values)])
def expected(source: str, host: str, metric: str) -> list[dict[str, float]]:
rows = store.query(source, host, metric, res=RES_RAW, start=now - 3_600.0, tail=True)
return [{"ts": ts, "value": value} for ts, value in rows]
routes = [
("/api/system/history", {"hosts": "driveripper,deskwork",
"metrics": "cpu.usage_pct,mem.used_pct", "range": "1h"},
[("host", host, metric) for host in ("driveripper", "deskwork")
for metric in ("cpu.usage_pct", "mem.used_pct")]),
("/api/disks/history", {"hosts": "driveripper",
"metrics": "disk.temp_c@nvme0n1", "range": "1h"},
[("disks", "driveripper", "disk.temp_c@nvme0n1")]),
("/api/llm/history", {"engines": "gufo", "metrics": "decode_tokens_per_second",
"range": "1h"},
[("llm", "gufo", "llm.decode_tokens_per_second")]),
("/api/ups/history", {"ups": "notaserial", "metrics": "ups.charge_pct",
"range": "1h"},
[("ups", "notaserial", "ups.charge_pct")]),
]
for path, params, pairs in routes:
body = test_client.get(path, params=params).json()
assert len(body["series"]) == len(pairs), path
for series, (source, host, metric) in zip(body["series"], pairs, strict=True):
assert set(series) == {"source", "host", "metric", "range_seconds", "points"}
assert (series["source"], series["host"], series["metric"]) == (source, host, metric)
assert series["range_seconds"] == 3_600.0
assert series["points"] == expected(source, host, metric), f"{path} {metric}"
# and an empty answer is still an answer: a name with no rows, in a batch with names that
# do, comes back as an empty series rather than a missing entry or a 404
body = test_client.get("/api/system/history", params={
"hosts": "driveripper", "metrics": "cpu.usage_pct,gpu.busy_pct@7",
"range": "1h"}).json()
assert [series["points"] for series in body["series"]] == [expected(
"host", "driveripper", "cpu.usage_pct"), []]
# no metric named is no series asked for: the batch door is never opened with an empty
# list (SQL `IN ()` is a syntax error waiting for that, and 500 is not the answer anyway)
empty = test_client.get("/api/system/history",
params={"hosts": "driveripper", "metrics": ""}).json()
assert empty["hosts"] == ["driveripper"] and empty["series"] == []
no_hosts = test_client.get("/api/system/history",
params={"hosts": "", "metrics": "cpu.usage_pct"}).json()
assert no_hosts["hosts"] == ["driveripper", "deskwork"], "empty means every configured host"
assert len(no_hosts["series"]) == 2
def test_llm_snapshot_keeps_an_engine_that_never_answered_in_the_list(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A configured-but-dead engine is listed as unreachable, not dropped (invariant 4)."""
# a host target too, so the snapshot's job filter has to skip a non-LLM job
with build_client(tmp_path, monkeypatch, vllm_url=DEAD_URL,
host_targets=(Target("driveripper", DEAD_URL),)) as test_client:
body = test_client.get("/api/llm/snapshot").json()
assert [engine["engine"] for engine in body["engines"]] == ["vLLM"]
assert body["engines"][0]["unreachable"] is True
assert body["fleet"]["engines_total"] == 1
assert body["fleet"]["engines_live"] == 0
assert [state["key"] for state in body["states"]] == ["llm:vllm"]
def test_llm_history_charts_both_engines_by_default(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
with build_client(tmp_path, monkeypatch, gufo_url=DEAD_URL,
vllm_url=DEAD_URL) as test_client:
body = test_client.get("/api/llm/history",
params={"metrics": "decode_tokens_per_second,kv_cache_usage_ratio",
"range": "15m"}).json()
assert body["engines"] == ["gufo", "vllm"]
assert body["range_seconds"] == 900.0
assert {(item["host"], item["metric"]) for item in body["series"]} == {
("gufo", "llm.decode_tokens_per_second"),
("gufo", "llm.kv_cache_usage_ratio"),
("vllm", "llm.decode_tokens_per_second"),
("vllm", "llm.kv_cache_usage_ratio")}
def test_one_batched_llm_history_request_answers_every_cell_series(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The exact query `/llm` issues per refresh returns every chart *and* sparkline series.
Phase 04 sends one request carrying the chart trio and the whole spark cell list —
including the two series the layout required (`tpot_seconds` for the vLLM TPOT row,
`spec_accept_rate` for the gufo spec row). A name missing here is a cell that reads
"no history" forever in the browser, which is the dead-cell anti-pattern the phase
wrote NOTES-chart-set to prevent; a rename in `sources/llm.py` fails here first.
"""
with build_client(tmp_path, monkeypatch, gufo_url=DEAD_URL,
vllm_url=DEAD_URL) as test_client:
store = test_client.app.state.ctx.store # type: ignore[attr-defined]
now = time.time()
metrics = ["decode_tokens_per_second", "prompt_tokens_per_second",
"kv_cache_usage", "requests_running", "requests_waiting",
"ttft_seconds", "tpot_seconds", "queue_seconds",
"prefix_cache_hit_ratio", "spec_accept_rate"]
for metric in metrics:
store.add("llm", "gufo", f"llm.{metric}", 1.5, ts=now - 60)
store.add("llm", "vllm", f"llm.{metric}", 2.5, ts=now - 30)
body = test_client.get("/api/llm/history", params={
"engines": "gufo,vllm", "metrics": ",".join(metrics), "range": "1h"}).json()
assert body["range_seconds"] == 3_600.0
by_key = {(item["host"], item["metric"]) for item in body["series"]}
assert by_key == {(engine, f"llm.{metric}")
for engine in ("gufo", "vllm") for metric in metrics}
points = {(item["host"], item["metric"]): item["points"] for item in body["series"]}
assert points[("gufo", "llm.spec_accept_rate")][0]["value"] == pytest.approx(1.5)
assert points[("vllm", "llm.tpot_seconds")][0]["value"] == pytest.approx(2.5)
def test_metrics_endpoint_lists_what_actually_has_history(client) -> None:
body = client.get("/api/metrics").json()
assert body["metrics"] == []
# ---- the `/disks` API (phase 09 task 03) ---------------------------------- #
#
# The drive-health surface answers like every other dashboard's: GETs only, the same
# stale/stub semantics for a host that is not answering, the same history mechanism for the
# `disk.*@dev` series, and the same 404-`configured: false` sentence when no agent is
# configured at all — the last one *without dialling*, because "not deployed here" must not
# cost a connect timeout or a red dot (invariant 3).
@pytest.fixture
def disks_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
disk_snapshot: Any) -> Iterator[TestClient]:
"""Two hosts configured; one of them answering SMART, through the production parser.
The payload and the stored samples are what `parse_disks_snapshot` makes of the
fixture-shaped capture (the agent's whole-disk rows plus the recorded `smartctl` shapes),
so every path a `/disks` card will read is a path the source really emits. The other host
keeps its `unreachable` stub, which is the shape a dead agent must keep (invariant 4).
Both targets point at a dead port, so nothing dials.
"""
db = str(tmp_path / "disks.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=(Target("driveripper", DEAD_URL),
Target("deskwork", DEAD_URL))), store)
with TestClient(app) as test_client:
sign_in(test_client)
now = time.time()
snapshot = disk_snapshot("driveripper")
snapshot["ts"] = now # "the agent answered just now", so charts are in range
result = parse_disks_snapshot("driveripper", snapshot, DisksCollector(), now=now)
store.add_many(result.samples)
app.state.ctx.hub.jobs["disks:driveripper"].state.payload = result.payload
yield test_client
store.close()
@pytest.fixture
def mixed_disks_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
disk_snapshot: Any) -> Iterator[TestClient]:
"""Four configured hosts: one per answer an agent can give about `smartctl`, plus a stub.
Phase 22's visibility gate needs all three arms of `smartctl_available` in **one** fleet,
because "hidden" only means something against what stays:
* `driveripper` — every drive read: `smartctl_available: true`
* `deskwork` — every drive `null` with no error line: `null` (`--no-smart`, or an agent
predating the SMART pass — it answered, it just cannot say)
* `gateway` — every drive `null` plus the agent's own "not found" line: `false`, which is
the one arm that hides a host (`api._disk_visible`)
* `deadhost` — never answered: the `unreachable` stub, whose flag is `null` by shape
Each payload is `parse_disks_snapshot`'s own output over the fixture captures, and every
target points at a dead port, so nothing dials and no poll can overwrite what is set here.
"""
db = str(tmp_path / "mixed-disks.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=(Target("driveripper", DEAD_URL),
Target("deskwork", DEAD_URL),
Target("gateway", DEAD_URL),
Target("deadhost", DEAD_URL))), store)
with TestClient(app) as test_client:
sign_in(test_client)
now = time.time()
for name, mode in (("driveripper", "healthy"), ("deskwork", "silent"),
("gateway", "no_binary")):
snapshot = disk_snapshot(name, mode=mode)
snapshot["ts"] = now # "the agent answered just now"
result = parse_disks_snapshot(name, snapshot, DisksCollector(), now=now)
app.state.ctx.hub.jobs[f"disks:{name}"].state.payload = result.payload
yield test_client
store.close()
#: (`smart` field, severity metric) for the two drive numbers the table rates — the same
#: pair `sources/disks.py:SEVERITY_FIELDS` declares, restated here so a rename that only
#: touches one side fails rather than colouring nothing.
RATED_CELLS: tuple[tuple[str, str], ...] = (
("temp_c", "disk.temp_c"),
("percentage_used", "disk.percentage_used"),
)
def disk_rows(body: dict[str, Any], host: str = "driveripper") -> list[dict[str, Any]]:
"""The disk rows of one host out of a `/api/disks/snapshot` body."""
return next(row for row in body["hosts"] if row["name"] == host)["disks"]
def flat_routes(app: Any) -> list[Any]:
"""Every route the app serves, with `include_router()` results flattened to their routes.
The FastAPI this repo pins keeps an included router as an `_IncludedRouter` wrapper with
no `path` of its own, so walking `app.routes` alone sees five anonymous objects instead of
the API surface — and the verb assertion below is only as good as this walk.
"""
out: list[Any] = []
for route in app.routes:
inner = getattr(route, "original_router", None)
out.extend(inner.routes if inner is not None else [route])
return out
def series_source(body: dict[str, Any]) -> set[str]:
"""Which sample family a history answer came from: `disks`, never the 10-second `host`."""
return {series["source"] for series in body["series"]}
def test_disks_snapshot_answers_one_entry_per_host_with_its_physical_disks(
disks_client) -> None:
"""`GET /api/disks/snapshot` is per host: rows + `smart` + `smartctl_available`.
"Per host" means per **visible** host since phase 22 — this fleet has no `false` host in
it, so both stub and answering host are listed; the hidden case is pinned on a fleet that
has one (`mixed_disks_client`).
"""
body = disks_client.get("/api/disks/snapshot")
assert response_ok(body)
payload = body.json()
assert [host["name"] for host in payload["hosts"]] == ["deskwork", "driveripper"], (
"every configured host is a row, in name order, answering or not")
rows = disk_rows(payload)
assert rows, "the capture has 16 whole-disk devices, so the page has 16 cards"
assert all("/" not in row["device"] for row in rows), "a mount leaked into the disk list"
first = rows[0]
assert first["path"] == f"/dev/{first['device']}"
assert set(first) >= {"device", "path", "model", "size_bytes", "rotational",
"health", "smart", "severity"}
assert first["smart"]["power_on_hours"] > 0, "the drive's own counters travel verbatim"
live = next(host for host in payload["hosts"] if host["name"] == "driveripper")
assert live["smartctl_available"] is True
assert live["smart_errors"] == []
assert live["summary"]["disks"] == len(rows)
assert "unreachable" not in live, "answering is the absence of the flag, as on /system"
assert [state["key"] for state in payload["states"]] == ["disks:driveripper",
"disks:deskwork"], (
"the page's dots come from the disks jobs, never the system ones")
def test_disks_snapshot_carries_the_verdicts_the_page_will_paint(disks_client) -> None:
"""Colour is in the payload: each row's `severity` is what `severity.classify` says.
This is the pin that keeps the threshold out of the page — `page-disks.js` reads these
and never compares a drive temperature to a bound.
"""
rows = disk_rows(disks_client.get("/api/disks/snapshot").json())
assert "ok" in {row["severity"]["temp_c"] for row in rows}, "the capture is healthy"
for row in rows:
smart = row["smart"] or {}
assert set(row["severity"]) == {field for field, _metric in RATED_CELLS} | {"health"}
for field, metric in RATED_CELLS:
assert row["severity"][field] == classify(metric, smart.get(field)), (
f"{row['device']}.{field} is coloured by something other than the one table")
assert not {key for row in rows for key in row["severity"]} & {
"reallocated_sector_ct", "pending", "uncorrectable", "power_on_hours"}, (
"a counter is an alarm chip on presence, never a graded cell")
def test_a_configured_host_with_no_payload_yet_is_a_row_of_nas(disks_client) -> None:
"""The stub shape `/api/system/snapshot` hands out, so `n/a` is never a missing row.
A host in `HOST_TARGETS` that has not answered is still in the fleet (invariants 3 and
4) with its counts `None` rather than `0`: "unknown" and "no drives" are different
sentences (rule 2), and the fleet header counts it without summing it.
"""
payload = disks_client.get("/api/disks/snapshot").json()
stub = next(host for host in payload["hosts"] if host["name"] == "deskwork")
assert stub["unreachable"] is True
assert stub["disks"] == [], "nothing to draw, but the host is still listed"
assert stub["smartctl_available"] is None, "an agent that never answered says nothing"
assert stub["smart_errors"] == []
assert set(stub["summary"]) == {"disks", "with_smart", "health_failed", "alarm_disks"}
assert all(value is None for value in stub["summary"].values()), "no invented zeros"
fleet = payload["fleet"]
assert (fleet["hosts_total"], fleet["hosts_live"]) == (2, 1)
assert fleet["disks"] == len(disk_rows(payload))
def test_the_disks_fleet_header_is_summed_by_the_api_not_the_page(disks_client) -> None:
"""`fleet` is the header's only arithmetic source, and it carries no verdict."""
payload = disks_client.get("/api/disks/snapshot").json()
live = next(host for host in payload["hosts"] if host["name"] == "driveripper")
fleet = payload["fleet"]
assert fleet["disks"] == live["summary"]["disks"]
assert fleet["alarm_disks"] == live["summary"]["alarm_disks"] > 0, (
"the recorded ATA shape carries reallocated sectors, so the fleet has chips to draw")
assert "severity" not in fleet, "a count of drives is not a graded number"
# ---- the `/disks` visibility gate (phase 22 task 01) ----------------------- #
#
# `TODO.md:13`: "VMs that don't have smartctl enabled disks should not show up. Gateway
# shouldn't even be a host on the disks page." One predicate (`api._disk_visible`) over the
# three arms of `smartctl_available`, applied to the assembled host list in both endpoints, so
# the page's rows, that block's rows and the fleet sums cannot describe three fleets. The
# source, the store, the `disks:<host>` job and its health record still see every host.
def test_the_disk_visibility_gate_reads_only_the_explicit_false() -> None:
"""Three arms, one hide condition — and the test is `is False`, not falsy.
`None` means "nobody knows" (an agent predating the SMART pass, `--no-smart`, or a payload
with no such key at all) and an agent that cannot say is not an agent that said *no*; a
dead agent's stub stays too, because a machine that stopped answering must never read as
"never configured" (invariant 3).
"""
assert _disk_visible({"smartctl_available": True}) is True
assert _disk_visible({"smartctl_available": None}) is True
assert _disk_visible({}) is True, "a payload that never said anything is not a 'no'"
assert _disk_visible({"unreachable": True, "smartctl_available": None}) is True, (
"a dead agent is not a host without smartmontools")
assert _disk_visible({"smartctl_available": 0}) is True, (
"falsy is not the predicate — only the value the agent answered with")
assert _disk_visible({"smartctl_available": False}) is False
def test_a_host_whose_agent_has_no_smartctl_is_not_a_host_on_the_disks_page(
mixed_disks_client) -> None:
"""The absent host and the two `unknown` hosts, pinned in the same response.
gateway's VM answers "`smartctl` not found", so it has no card and no row; deskwork's
agent cannot say either way and deadhost's never answered, and both keep their dimmed
`n/a` rows exactly as before this gate existed.
"""
payload = mixed_disks_client.get("/api/disks/snapshot").json()
visible = {host["name"]: host for host in payload["hosts"]}
assert list(visible) == ["deadhost", "deskwork", "driveripper"], (
"only the host whose agent said 'no smartctl' is gone")
assert visible["driveripper"]["smartctl_available"] is True
assert visible["driveripper"]["disks"], "the host that stayed kept every drive row"
assert visible["deskwork"]["smartctl_available"] is None, (
"an agent that cannot say is still a card, with its own why-line")
assert "unreachable" not in visible["deskwork"], "it answered — silently, but it answered"
assert visible["deadhost"]["unreachable"] is True
assert visible["deadhost"]["smartctl_available"] is None
def test_the_disks_fleet_header_counts_only_the_hosts_the_page_lists(
mixed_disks_client) -> None:
"""`hosts_total` is the page's row count, and a hidden host lends it no drives."""
payload = mixed_disks_client.get("/api/disks/snapshot").json()
fleet = payload["fleet"]
rows_by_host = {host["name"]: host["disks"] for host in payload["hosts"]}
assert fleet["hosts_total"] == len(payload["hosts"]) == 3
assert fleet["hosts_live"] == 2, "the stub host is counted and never summed"
assert fleet["disks"] == len(rows_by_host["driveripper"]) + len(rows_by_host["deskwork"]), (
"the hidden host's `vda` is in no fleet count")
assert fleet["with_smart"] == len(rows_by_host["driveripper"])
assert "severity" not in fleet
def test_hiding_a_diskless_host_leaves_its_health_record_and_the_fleet_alone(
mixed_disks_client) -> None:
"""The gate reads a payload; it does not unwatch the machine.
`states` still lists `disks:gateway` (the job polls, and healthy is healthy), `/system`
still lists gateway as a host, and `/api/health` still answers for the job — a box with no
smartmontools is missing from the **drive** page and from nothing else (invariant 3).
"""
snapshot = mixed_disks_client.get("/api/disks/snapshot").json()
summary = mixed_disks_client.get("/api/summary").json()
assert {state["key"] for state in snapshot["states"]} == {
"disks:deadhost", "disks:deskwork", "disks:driveripper", "disks:gateway"}
assert "gateway" in {row["name"] for row in summary["system"]["hosts"]}, (
"the system page lost a machine over a missing binary")
health = {row["key"]: row for row in
mixed_disks_client.get("/api/health").json()["sources"]}
assert "disks:gateway" in health
def test_summary_and_the_disks_page_hide_the_same_hosts(mixed_disks_client) -> None:
"""One predicate, two endpoints: `/` and `/disks` may not list different machines."""
body = mixed_disks_client.get("/api/summary").json()
snapshot = mixed_disks_client.get("/api/disks/snapshot").json()
disks = body["disks"]
# the **set**, not the list: `/api/disks/snapshot` sorts by name, `/api/summary` answers
# rows in hub order and always has
assert {row["name"] for row in disks["hosts"]} == {
host["name"] for host in snapshot["hosts"]} == {
"deadhost", "deskwork", "driveripper"}
assert disks["fleet"] == snapshot["fleet"], "the header would disagree with the page"
assert disks["fleet"]["hosts_total"] == 3
assert disks["enabled"] is True
assert disks["fleet"]["hosts_total"] != len(body["system"]["hosts"]), (
"four machines configured, three drive rows — the two numbers are different facts")
# the rows that stayed carry the shape they always did, `unknown` and all (rule 2)
rows = {row["name"]: row for row in disks["hosts"]}
assert rows["deskwork"]["smartctl_available"] is None
assert rows["deskwork"]["unreachable"] is False
assert rows["deadhost"]["unreachable"] is True and rows["deadhost"]["disks"] is None
def test_the_disk_visibility_gate_applies_with_only_one_host_configured(
tmp_path: Path, disk_snapshot: Any) -> None:
"""A fleet of nothing but a `false` host is an empty drive page, not a 404.
Unconfigured still means "no `HOST_TARGETS`" (a 404 that dials nothing, invariant 3);
configured-and-hidden answers with an empty list and a zero fleet, which is what the page
renders as its empty state.
"""
db = str(tmp_path / "one-hidden-host.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=(Target("gateway", DEAD_URL),)), store)
with TestClient(app) as test_client:
sign_in(test_client)
now = time.time()
snapshot = disk_snapshot("gateway", mode="no_binary")
snapshot["ts"] = now
result = parse_disks_snapshot("gateway", snapshot, DisksCollector(), now=now)
assert result.payload["smartctl_available"] is False, "the arm this test is about"
app.state.ctx.hub.jobs["disks:gateway"].state.payload = result.payload
body = test_client.get("/api/disks/snapshot")
assert body.status_code == 200, "configured-and-hidden is not unconfigured"
payload = body.json()
assert payload["hosts"] == []
assert payload["fleet"]["hosts_total"] == 0
assert payload["fleet"]["disks"] == 0
assert [state["key"] for state in payload["states"]] == ["disks:gateway"]
summary = test_client.get("/api/summary").json()["disks"]
assert summary["enabled"] is True
assert summary["hosts"] == []
assert summary["fleet"] == payload["fleet"], (
"an all-hidden fleet must look the same from both endpoints")
store.close()
def test_the_disks_surface_is_gets_only_and_the_write_stays_the_ups_one(
disks_client) -> None:
"""Invariant 1: the app writes to an **upstream** from exactly one route."""
routes = flat_routes(disks_client.app)
disks = [route for route in routes
if str(getattr(route, "path", "")).startswith("/api/disks")]
assert {route.path for route in disks} == {"/api/disks/snapshot", "/api/disks/history"}
verbs = {verb for route in disks for verb in (getattr(route, "methods", None) or set())}
assert verbs == {"GET"}, "a write appeared on the drive-health surface"
for verb in ("POST", "PUT", "PATCH", "DELETE"):
response = disks_client.request(verb, "/api/disks/snapshot", json={})
assert response.status_code == 405, f"{verb} /api/disks/snapshot was accepted"
writes = sorted(str(route.path) for route in routes
if "POST" in (getattr(route, "methods", None) or set()))
# Invariant 1 is about the fleet. The `/auth/` writes create, re-credential, and clear a
# session in this process; the `/certs/` writes (phase 15) mint, reissue, and revoke a
# certificate in this process's `meta` table. Neither dials anything — and a revocation's whole
# effect is that the *next poll* refuses to dial — so they are named here rather than filtered
# out of sight, because the failure this assertion catches is a POST arriving quietly.
authority = [path for path in writes if path.startswith("/api/certs/")]
session = [path for path in writes if path.startswith("/api/auth/")]
upstream = [path for path in writes
if not path.startswith(("/api/auth/", "/api/certs/"))]
assert upstream == ["/api/ups/command"], "one write endpoint, four gates, unchanged"
assert session == ["/api/auth/login", "/api/auth/logout", "/api/auth/password"]
assert authority == ["/api/certs/root/rotate", "/api/certs/{name}/revoke",
"/api/certs/{name}/rotate"], "the CA surface grew a write nobody named"
# …and nothing anywhere grew the verbs this app has no use for, session included.
assert not sorted(path for route in routes
for path in [str(route.path)]
if set(getattr(route, "methods", None) or ()) & {"PUT", "PATCH", "DELETE"})
# The session writes are not upstream writes, but they are still the only ones: `/login`
# is the page, not a POST target, and `/api/auth/status` stays a read.
pages = sorted(str(route.path) for route in routes
if "GET" in (getattr(route, "methods", None) or set())
and not str(route.path).startswith("/api/"))
# `/settings` (phase 11) is a fifth dashboard-shaped **page** and it grew no verb: a page
# route is a GET, so the write surface this test guards stays the one UPS command. Same for
# `/gateway` (phase 29 task 04): the connection map is a read of one job's payload, and the
# app still dials an upstream on exactly one verb.
assert pages == ["/", "/certs", "/disks", "/gateway", "/llm", "/login", "/settings",
"/system", "/ups"]
def test_config_prints_the_disks_cadence_the_page_explains_itself_with(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A card four minutes old is behaving, not broken — and the interval is the proof.
`/disks` has no upstream of its own, so `enabled` follows `HOST_TARGETS` and the cadence
is its own. Both numbers come from config rather than a constant in the page, so changing
`DISKS_INTERVAL_SECONDS` moves the sentence and the poller together.
"""
with build_client(tmp_path, monkeypatch, host_targets=(Target("driveripper", DEAD_URL),),
disks_interval=120.0, disks_timeout=45.0) as test_client:
disks = test_client.get("/api/config").json()["disks"]
assert disks["enabled"] is True
assert disks["interval_seconds"] == 120.0
assert disks["timeout_seconds"] == 45.0
def test_disks_snapshot_404s_without_dialling_a_host_that_was_never_configured(
client, monkeypatch: pytest.MonkeyPatch) -> None:
"""The gate runs before anything that could reach the network (invariant 3).
Both spies raise, so a route that constructed a client or fetched a snapshot before
checking `HOST_TARGETS` fails here instead of costing a timeout and a red dot. The hub
assertion is the structural half: with no targets there is no `disks` job to dial with.
"""
calls: list[str] = []
def spy_get(self: Any, url: Any, **kwargs: Any) -> Any:
calls.append(f"GET {url}")
raise AssertionError(f"the unconfigured disks route dialled {url}")
def spy_fetch(*args: Any, **kwargs: Any) -> Any:
calls.append("fetch_snapshot")
raise AssertionError("the unconfigured disks route fetched an agent snapshot")
monkeypatch.setattr(httpx.AsyncClient, "get", spy_get)
monkeypatch.setattr("dashboards.collectors.fetch_snapshot", spy_fetch)
response = client.get("/api/disks/snapshot")
assert response.status_code == 404
body = response.json()
assert body["configured"] is False
assert "HOST_TARGETS" in body["detail"], "the sentence names the fix"
assert calls == [], "an unconfigured page must not touch the network"
assert client.app.state.ctx.hub.jobs == {}, (
"no targets means no disks job, so nothing can age out or go red")
def test_disks_history_serves_the_dev_series_the_page_charts(disks_client) -> None:
"""One request, every series a host's cards could draw, points from the stored samples."""
rows = disk_rows(disks_client.get("/api/disks/snapshot").json())
metrics = [f"{base}@{row['device']}" for row in rows for _field, base in SMART_SERIES]
body = disks_client.get("/api/disks/history", params={
"hosts": "driveripper", "metrics": ",".join(metrics), "range": "1h"}).json()
assert body["hosts"] == ["driveripper"], "one host, one panel"
assert body["range_seconds"] == 3_600.0
assert [series["metric"] for series in body["series"]] == metrics
assert series_source(body) == {"disks"}, "the disks family, never the 10-second host one"
points = {(series["metric"]): series["points"] for series in body["series"]}
assert points["disk.temp_c@nvme0n1"][0]["value"] == pytest.approx(38.0)
assert points["disk.power_on_hours@nvme0n1"][0]["value"] == pytest.approx(8_760.0)
def test_disks_history_answers_empty_for_a_series_a_drive_does_not_publish(
disks_client) -> None:
"""Unknown names are an empty series, not a 404 — the /system and /llm contract.
The recorded ATA shape reports no `percentage_used`, so that drive's wear plot has
nothing to draw, and a device name the agent never listed behaves the same way. The card
asks for every series it could draw and reads `n/a` where there is nothing, instead of
the request failing.
"""
rows = disk_rows(disks_client.get("/api/disks/snapshot").json())
unworn = next(row["device"] for row in rows
if (row["smart"] or {}).get("percentage_used") is None)
body = disks_client.get("/api/disks/history", params={
"hosts": "driveripper",
"metrics": f"disk.percentage_used@{unworn},disk.temp_c@nosuchdisk,"
"disk.temp_c@nvme0n1",
"range": "1h"}).json()
by_metric = {series["metric"]: series["points"] for series in body["series"]}
assert by_metric[f"disk.percentage_used@{unworn}"] == [], "no measurement, no line"
assert by_metric["disk.temp_c@nosuchdisk"] == []
assert by_metric["disk.temp_c@nvme0n1"], "the series that does exist still answers"
def test_disks_history_charts_every_configured_host_in_one_request(disks_client) -> None:
"""With no `hosts` the answer covers every configured agent, dead ones included (empty)."""
body = disks_client.get("/api/disks/history",
params={"metrics": "disk.temp_c@nvme0n1,disk.temp_c@sda",
"range": "24h"}).json()
assert body["hosts"] == ["driveripper", "deskwork"]
assert body["range_seconds"] == 86_400.0
assert {(series["host"], series["metric"]) for series in body["series"]} == {
("driveripper", "disk.temp_c@nvme0n1"), ("driveripper", "disk.temp_c@sda"),
("deskwork", "disk.temp_c@nvme0n1"), ("deskwork", "disk.temp_c@sda")}
assert [series for series in body["series"] if series["host"] == "deskwork"], (
"a host with no history is an empty series, never a missing one")
def test_the_metrics_endpoint_answers_for_the_disks_family(disks_client) -> None:
""""Why is this chart empty" has an answer per page: /api/metrics?source=disks."""
body = disks_client.get("/api/metrics", params={"source": "disks"}).json()
assert "disk.temp_c@nvme0n1" in body["metrics"]
assert not [name for name in body["metrics"] if not name.startswith("disk.")], (
"the disks family holds only disk series")
assert not [name for name in disks_client.get(
"/api/metrics", params={"source": "host"}).json()["metrics"]
if name.startswith("disk.temp_c")], "SMART never mixes into the system family"
# ---- the `gateway` job: wiring, health, alerts, summary (phase 29 task 02) -- #
#
# The `gateway` source is a job of its own reading the same agent `/snapshot` the `host:` job reads
# (the `disks` precedent), so the pins below are about the wiring and the consequences: one job and
# only for the host `GATEWAY_HOST` names, the one certificate entry shared by all three of that
# host's jobs, an outage that stays one line on the overview, and a summary block that never turns
# an unknown into a zero.
#: The host name every gateway test here uses — `GATEWAY_HOST`'s own default, so the settings line
#: under test is "a target whose name matches" and nothing else.
GATEWAY_TARGET = Target("gateway", "http://127.0.0.1:1")
def hub_of(tmp_path: Path, *, tls: Any | None = None,
**overrides: Any) -> tuple[Any, SampleStore]:
"""A hub built exactly as the app builds it, with no event loop and nothing dialled.
`build_hub` wires jobs and starts no task, so a shared client that is never used is enough —
which is also the point: a job that does not exist cannot open a socket. `tls` is passed
through as the app passes it, because the shared-entry property below is about that argument.
"""
db = str(tmp_path / "gateway-hub.sqlite3")
store = SampleStore(db)
settings = Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2, **overrides)
hub, _monitor = app_module.build_hub(settings, store, httpx.AsyncClient(), tls=tls)
return hub, store
def test_a_configured_gateway_host_gets_exactly_one_job_of_its_own(
tmp_path: Path) -> None:
"""One `gateway` job, on the gateway's clock, labelled with the host it reads.
One and not one-per-host: the page is about the single ingress, and a job against every
machine would put a `gateway` health record on hosts that were never asked to route. Its
interval is `GATEWAY_INTERVAL_SECONDS`, not the 10 s system one — a connection count is the
shape of half a minute (invariant 4's isolation, the `disks` shape).
"""
hub, store = hub_of(tmp_path,
host_targets=(Target("deskwork", DEAD_URL),
Target("router", DEAD_URL),
Target("driveripper", DEAD_URL)),
gateway_host="router", gateway_interval=15.0)
jobs = [job for job in hub.jobs.values() if job.dashboard == "gateway"]
store.close()
assert [job.key for job in jobs] == ["gateway"], "the source is one job, not one per host"
assert jobs[0].label == "router", "the label is the host: an alert and an event print it"
assert jobs[0].interval == 15.0, "its cadence is GATEWAY_INTERVAL_SECONDS, not the host's"
assert jobs[0].enabled is True
assert sorted(hub.jobs) == ["disks:deskwork", "disks:driveripper", "disks:router",
"gateway", "host:deskwork", "host:driveripper", "host:router"], (
"the gateway job is a third reader of one agent, not a replacement for either")
def test_a_gateway_name_that_is_not_a_target_gets_no_job_at_all(tmp_path: Path) -> None:
"""Unconfigured means **no task**, not a task that reports itself red (invariant 3).
`GATEWAY_HOST` defaults to `gateway`, and the trap is a deployment that monitors hosts with
other names: the default must not manufacture a source dialling a machine nobody configured.
"""
hub, store = hub_of(tmp_path, host_targets=(Target("deskwork", DEAD_URL),))
keys = sorted(hub.jobs)
store.close()
assert "gateway" not in keys, keys
assert [key for key in keys if key.startswith("host:")] == ["host:deskwork"]
async def test_the_gateway_job_holds_the_same_credential_as_its_hosts_other_jobs(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""One `AgentTls` per host, handed to all three of its jobs.
The dashboard has **one** identity, and the revocation answer is per name — so if the gateway
job were given a second, separately-built entry it could keep dialling a host the operator just
struck out (`collectors.AgentTls.status` is a live read, and `agent_tls_map` builds one entry).
Asserted on object identity, because a structurally-equal second entry would pass a field test
and still be a second `status` callable to go stale.
"""
seen: dict[str, Any] = {}
entry = app_module.AgentTls(verify="/ca.pem", cert=("/c.pem", "/k.pem"),
status=lambda: certs.ACTIVE)
def record(name: str) -> Any:
async def scrape(client: Any, target: Target, *_args: Any, **kwargs: Any) -> Any:
from dashboards.poller import Result # noqa: PLC0415 - the fake's return shape
seen[f"{name}:{target.name}"] = kwargs.get("tls")
return Result(payload={}, samples=[])
return scrape
monkeypatch.setattr("dashboards.collectors.fetch_host_snapshot", record("system"))
monkeypatch.setattr("dashboards.collectors.fetch_disk_snapshot", record("disks"))
monkeypatch.setattr("dashboards.collectors.fetch_gateway_snapshot", record("gateway"))
hub, store = hub_of(tmp_path, host_targets=(Target("gateway", "https://gw.test"),
Target("deskwork", "http://desk.test")),
tls={"gateway": entry})
try:
for job in hub.jobs.values():
await job.fn()
finally:
store.close()
assert seen == {"system:gateway": entry, "disks:gateway": entry, "gateway:gateway": entry,
"system:deskwork": None, "disks:deskwork": None}, (
"the ingress gets its host's entry and no other host gets one")
def test_the_gateway_job_is_handed_no_rate_estimator(tmp_path: Path) -> None:
"""A count is not a rate, and the wiring is where that could silently break.
`build_hub` spans two estimators for the two cadences that measure rates; the `gateway` job
joins neither, because a connection count pushed through a Δ-window becomes a rate with a
`Δ<window>` label and no measurement behind it (rule 2). Asserted on the closure the factory
built: the only arguments it captured are the client, the target, the timeout and the TLS entry
— no estimator object.
"""
hub, store = hub_of(tmp_path, host_targets=(GATEWAY_TARGET,))
job = hub.jobs["gateway"]
captured = dict(zip(job.fn.__code__.co_freevars,
tuple(c.cell_contents for c in job.fn.__closure__ or ()), strict=True))
store.close()
assert captured, "the job's collector captured nothing: it cannot be dialling its own target"
assert not [name for name, value in captured.items()
if isinstance(value, RateEstimator)], "a count must not be handed a rate estimator"
assert set(captured) == {"client", "target", "settings", "entry"}, (
"the gateway fetch takes a client, a target, its settings and one TLS entry — nothing else")
def make_gateway_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
**overrides: Any) -> Any:
"""An app whose only host is the ingress, on clocks fast enough to poll inside a test.
The intervals are the poller's own floor, not 0.05: `Job` clamps its cadence at 0.5 s,
while the staleness threshold is three times the *configured* interval. At 0.05 s a healthy
job's gap (≥ 0.5 s) always outlived its own threshold (0.15 s), so its `:stale` row
appeared in the alert list 70 % of the time and any assertion on the list's length was
a test of when the event loop happened to tick. At 0.5 s the threshold (1.5 s) is
three times the gap a healthy job can make, and the premise jobs stay quiet whatever
the machine is doing."""
return build_client(tmp_path, monkeypatch, host_targets=(GATEWAY_TARGET,),
host_interval=0.5, disks_interval=0.5, gateway_interval=0.5,
**overrides)
def answer_as(client: TestClient, key: str, payload: dict[str, Any]) -> None:
"""Give one job a collector that hands back `payload`, so its `ok` is truthful.
The `healthy_client` idiom: a health record is overwritten only by a poll, so a fixture that
claims a source is answering swaps the poll's function rather than the record. A failed poll
keeps the last good payload, which is why a test wanting a *failing* source leaves the dead-port
`fn` in place and injects the payload as the last good one.
"""
from dashboards.poller import Result # noqa: PLC0415 - only these helpers need it
async def collect() -> Result:
return Result(payload=payload, samples=[])
client.app.state.ctx.hub.jobs[key].fn = collect # type: ignore[attr-defined]
def gateway_block(client: TestClient) -> dict[str, Any]:
return client.get("/api/summary").json()["gateway"]
#: The whole `gateway` block, pinned as a **set** rather than as a list of present keys: phase 30
#: task 02 deleted `unmatched`, `unmatched_listeners` and `partial` with the socket join they
#: described and task 03 added `warnings` back as a tally, so a block that grew or lost a field
#: would do it quietly and this fails instead. Two consumers read these names — `page-index.js`'s
#: `GATEWAY_FIELDS` / `GATEWAY_WARNING_FIELD` (resolved against a live document below) and any
#: notifier walking `/api/summary`.
GATEWAY_BLOCK_FIELDS = {"configured", "host", "href", "supported", "unreachable",
"listeners", "connections", "live_in", "live_out", "map_targets",
"warnings", "states"}
def test_the_summary_gateway_block_is_the_stub_of_a_source_that_never_answered(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Configured, dialling, nothing in yet: every count is `null` and the row says unreachable.
The `system.hosts` stub rule (invariants 3 and 4): a source in the deployment that has not
answered is present with unknowns, because dropping it would read as "never configured" and
zeroing it would read as an idle gateway. The poll has to have happened for the stub to mean
what it says, so the test waits for the dead port's sentence first.
Note which fields are null **here**: the two row counts included. A host that has never
replied has not told us what its configs declare either, and `0` would be the sentence "this
gateway routes nothing" said about a machine that never said it.
"""
with make_gateway_client(tmp_path, monkeypatch) as client:
wait_for_source_error(client, "gateway", "unreachable")
block = gateway_block(client)
assert block["configured"] is True
assert block["host"] == "gateway"
assert block["unreachable"] is True
assert block["supported"] is None, "nobody has answered, so nobody knows the version"
assert set(block) == GATEWAY_BLOCK_FIELDS
for field in ("listeners", "connections", "live_in", "live_out", "map_targets",
"warnings"):
assert block[field] is None, f"{field} must be unknown, never 0"
assert [row["key"] for row in block["states"]] == ["gateway"]
assert block["states"][0]["dashboard"] == "gateway"
# the link travels even here: the overview renders it, and a block that grew `href` only
# when configured would make the page test for two shapes of the same row
assert block["href"] == GATEWAY_HREF
def endpoint_identity(out: dict[str, Any]) -> tuple[Any, ...] | None:
"""One endpoint, in the currency the kernel writes — derived here, not by the source.
An endpoint written as an address **is** that address; an endpoint written as a name is every
address the agent's resolver published for it (`out.resolved`), and `ipaddress` folds the
`::ffff:`-mapped and zero-padded spellings so one address in two tables is one key. `None`
means the join has nothing to name the far end with, which contributes nothing to a total.
This is deliberately a **third** reading of "what is one endpoint" (the others are
`gateway.endpoint_of` behind `api._endpoint_values`, and the join itself): since phase 32 two
route lines can reach one address while the configs spell it as two different names, and a
total keyed on the config's text would book it once per name.
"""
port = out.get("port")
if not isinstance(port, int):
return None
spellings = ([out["host"]] if out.get("address") is True
else list(out.get("resolved") or []))
keys: list[str] = []
for spelling in spellings:
try:
address = ipaddress.ip_address(str(spelling))
except ValueError:
continue
mapped = getattr(address, "ipv4_mapped", None)
keys.append(str(mapped if mapped is not None else address))
return (min(keys), port) if keys else None
def endpoint_sums(payload: dict[str, Any]) -> tuple[int | None, int | None]:
"""The two live totals, derived here so the block is checked against a **second** reading.
Inbound is the sum over trunks. Outbound is the sum over the **distinct endpoints** the route
lines reach: `sources/gateway.py` repeats an endpoint's whole count on every line that reaches
it (`shared: true`), so summing the lines would book the same sockets twice and print a router
busier than the kernel has connections for. Neither is a sum when nothing answered.
"""
live_in = [row["connections"] for row in payload["listeners"]
if isinstance(row["connections"], int)]
endpoints: dict[tuple[Any, ...], Any] = {}
for row in payload["connections"]:
key = endpoint_identity(row["out"])
if key is not None:
endpoints.setdefault(key, row["connections"])
live_out = [value for value in endpoints.values() if isinstance(value, int)]
return (sum(live_in) if live_in else None,
sum(live_out) if live_out else None)
@pytest.mark.parametrize("mode", ["full", "resolved", "predates", "sockets_silent"])
def test_the_summary_gateway_block_is_the_payloads_sums_not_the_pages_arithmetic(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, gateway_snapshot: Any,
mode: str) -> None:
"""The block re-derives nothing: each figure is the projection's own, in all three arms.
Driven from `gateway.parse` over the **captured** configs and `/proc` tables rather than a
hand-written payload, so a rename in the source's shape fails here instead of printing a
plausible zero (rule 7).
The split that replaced `partial` is the point of the arms. `listeners` / `connections`
are **row counts** — a fact readable from a config file, so they are numbers even when the
socket table never answered — while `live_in` / `live_out` are measurements and go `null` with
it. The old block could only report one number per side and needed a `partial` flag to say it
was a subtotal; there is no residual to qualify now, because a socket outside the two files is
counted nowhere by decision.
The `resolved` arm is phase 32's, and it is here because the **block** must not change when the
join widens: twelve fields, the same names, and `live_out` still the sum over distinct
endpoints — now endpoints reached through a resolved name as well as an address, each booked
once however many lines reach it. A block that summed the lines instead would move from 15 to a
bigger, wronger number on the same capture.
"""
payload = gateway_source.parse(gateway_snapshot(mode), "gateway")
expected_in, expected_out = endpoint_sums(payload)
with make_gateway_client(tmp_path, monkeypatch) as client:
answer_as(client, "gateway", payload)
deadline = time.time() + 10
while time.time() < deadline and not client.app.state.ctx.hub.jobs["gateway"].state.ok:
time.sleep(0.05)
block = gateway_block(client)
assert block["configured"] is True and block["unreachable"] is False
assert block["supported"] is payload["supported"]
assert block["host"] == "gateway"
assert set(block) == GATEWAY_BLOCK_FIELDS, mode
if not payload["supported"]:
# nothing was read, so even "this host declares no listener" would be a claim about a
# file nobody opened: `supported: false` and nulls are the honest pair
assert block["map_targets"] is None
assert block["listeners"] is block["connections"] is None
assert block["live_in"] is block["live_out"] is None
assert block["warnings"] is None, "an unread config has not been found guilty of anything"
return
assert block["map_targets"] == len(payload["nginx"]["map_targets"])
assert block["listeners"] == len(payload["listeners"])
assert block["connections"] == len(payload["connections"])
# a **count**, not the list: `/` needs to know whether there is anything to address, and the
# page that lists them reads `/api/gateway/snapshot`
assert block["warnings"] == len(payload["warnings"])
assert block["live_in"] == expected_in
assert block["live_out"] == expected_out
if payload["sockets"]["established"] is None:
# the configs answered and the kernel did not: the rows are real, the counts are not
assert block["listeners"] > 0 and block["connections"] > 0
assert block["live_in"] is block["live_out"] is None, "unknown is not zero"
else:
assert block["live_in"] is not None
assert block["live_in"] + (block["live_out"] or 0) <= payload["sockets"]["established"], (
"the reported figures may under-count the kernel's, never exceed it")
def test_the_summary_gateway_block_of_the_captured_ingress_reads_the_live_join(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, gateway_snapshot: Any) -> None:
"""The real capture, one figure at a time — the block an operator would actually get.
The parametrised test above proves the rule; this one pins the numbers, so a change to the join
that moves a total is visible somewhere rather than only in a browser. The three config-shaped
figures are literal, because only the operator moves them; the two live totals are derived from
the capture rather than quoted, because a re-capture of a busy router lawfully moves them.
Since phase 32 task 03 the outbound total is no longer pinned to the two `127.0.0.1` endpoints:
the recorded `gateway_resolver.json` places every name this ingress routes to, so the routes
that used to be dashes are counted through the address the gateway dialled. What is still pinned
is the **rule** the number comes from — one count per distinct endpoint, and never more than the
kernel has connections for.
"""
payload = gateway_source.parse(gateway_snapshot("resolved"), "gateway")
with make_gateway_client(tmp_path, monkeypatch) as client:
answer_as(client, "gateway", payload)
deadline = time.time() + 10
while time.time() < deadline and not client.app.state.ctx.hub.jobs["gateway"].state.ok:
time.sleep(0.05)
block = gateway_block(client)
assert block["listeners"] == 10, "6 haproxy binds + 4 nginx listen ports"
assert block["connections"] == 33, "every route the two captured configs declare"
assert block["map_targets"] == 18, "the stream map's routing entries are the fan-out"
# The two live totals are the volatile half: a capture of a running router is a different set of
# connections every second, so they are compared with the payload the block was built from
# rather than with a number typed here (the doctrine this suite already keeps for the socket
# fixtures themselves — assert the shape and the rule, never a particular row). What stays
# literal is what only the two config files decide, above, and what stays asserted as a
# relationship is the direction of the error, below.
assert block["live_in"] == sum(row["connections"] or 0 for row in payload["listeners"])
_, expected_out = endpoint_sums(payload)
assert block["live_out"] == expected_out, "one count per distinct endpoint, never one per line"
assert sum(row["connections"] or 0 for row in payload["connections"]) >= (expected_out or 0), (
"the lines repeat a shared endpoint's count, which is exactly why the block adds the "
"endpoints up instead")
assert block["live_in"] + block["live_out"] <= payload["sockets"]["established"], (
"the reported figures may under-count the kernel's, never exceed it")
assert block["warnings"] == 0, (
"this fleet's two config files connect to themselves, and 0 is the truth rather than a "
"missing measurement — the overview line prints nothing for it either way")
assert "severity" not in block
assert block["href"] == GATEWAY_HREF
def gateway_alerts(client: Any) -> list[dict[str, Any]]:
"""`/api/summary`'s alert rows for an app whose jobs are already in the state we asked for."""
return client.get("/api/summary").json()["alerts"]
def test_one_dangling_use_backend_is_one_warn_row_on_the_overview(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, gateway_snapshot: Any) -> None:
"""Both directions of the row, over the **real** projection of the captured configs.
The `self_test_app` device (swap the collector so a poll cannot overwrite what the test set):
the mutation is a `use_backend` naming a backend nobody declares, applied to the haproxy section
the agent's own parser produced, so the warning, its message and its count are the source's and
not this file's. Then the same document with the mutation removed, because an absence-only
assertion is satisfied by deleting the feature.
"""
clean = gateway_source.parse(gateway_snapshot("full"), "gateway")
broken_snapshot = gateway_snapshot("full")
broken_snapshot["haproxy"]["frontends"][0]["backends"] = ["old-tunnel"]
broken = gateway_source.parse(broken_snapshot, "gateway")
assert [row["kind"] for row in clean["warnings"]] == []
assert [row["kind"] for row in broken["warnings"]] == [gateway_source.WARN_ROUTE_DANGLING]
with make_gateway_client(tmp_path, monkeypatch) as client:
answer_as(client, "host:gateway", {"name": "gateway"})
answer_as(client, "disks:gateway", {"name": "gateway", "disks": [], "summary": {}})
answer_as(client, "gateway", broken)
deadline = time.time() + 15
while time.time() < deadline and not client.app.state.ctx.hub.jobs["gateway"].state.ok:
time.sleep(0.05)
# The two host jobs are this test's premise, not its subject: on a slow machine the wait
# loops above outlive their 1.5 s staleness window, and the alert then gains a `:stale`
# row for the premise and the assertion below reads a three-row list. Stamping their
# `last_ok` pins the premise the docstring states ("the agent answers") and leaves the
# gateway row the only line.
for host_key in ("host:gateway", "disks:gateway"):
client.app.state.ctx.hub.jobs[host_key].state.last_ok = time.time()
rows = gateway_alerts(client)
block = client.get("/api/summary").json()["gateway"]
assert len(rows) == 1, rows
assert rows[0]["id"] == "gateway:config"
assert rows[0]["severity"] == "warn"
assert rows[0]["source"] == "gateway"
assert rows[0]["href"] == "/gateway"
assert rows[0]["message"] == (
'1 proxy config problem to address — frontend haproxy-22 routes to backend '
'"old-tunnel", which no backend block declares')
assert block["warnings"] == 1, "the tally and the row are the same reading of one payload"
# …and removing the mutation removes the row: the alert is the config, not the test rig
answer_as(client, "gateway", clean)
deadline = time.time() + 15
while time.time() < deadline and client.get("/api/summary").json()["gateway"]["warnings"]:
time.sleep(0.05)
for host_key in ("host:gateway", "disks:gateway"):
client.app.state.ctx.hub.jobs[host_key].state.last_ok = time.time()
assert gateway_alerts(client) == [], "a clean ingress is an all clear, not a green line"
def test_a_gateway_job_that_stops_answering_keeps_one_row_not_two(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, gateway_snapshot: Any) -> None:
"""The last good payload still holds its warnings; the overview still says one thing.
Invariant 4's page side: a failed poll keeps the payload, and `_source_alerts` writes the
reachability row for the job. Task 03's rule is that the config row goes quiet then — the
operator's problem right now is that the router is not answering, and a second amber line
quoting a reading they cannot refresh is noise on top of it.
"""
broken = gateway_source.parse(gateway_snapshot("full"), "gateway")
broken["warnings"] = [{"id": "haproxy_backend_orphan:old-tunnel",
"kind": "haproxy_backend_orphan", "subject": "old-tunnel",
"message": 'backend "old-tunnel" has 2 servers and no frontend '
"routes to it"}]
with make_gateway_client(tmp_path, monkeypatch) as client:
# the agent answers; only the proxy reading inside it goes quiet, which is the arm this
# rule is about — a dead agent is `host:gateway`'s sentence and echoes to nothing
answer_as(client, "host:gateway", {"name": "gateway"})
answer_as(client, "disks:gateway", {"name": "gateway", "disks": [], "summary": {}})
answer_as(client, "gateway", broken)
deadline = time.time() + 15
while time.time() < deadline and not client.app.state.ctx.hub.jobs["gateway"].state.ok:
time.sleep(0.05)
assert client.get("/api/summary").json()["gateway"]["warnings"] == 1, (
"the control: answering, the config row IS there")
async def failing() -> Any:
raise UpstreamError("gateway: the proxy sections went quiet")
client.app.state.ctx.hub.jobs["gateway"].fn = failing # type: ignore[attr-defined]
deadline = time.time() + 15
while time.time() < deadline:
job = client.app.state.ctx.hub.jobs["gateway"]
if not job.state.ok and job.state.payload is not None:
break
time.sleep(0.05)
else:
pytest.fail("the gateway job never went from answering to failing")
# The two host jobs are this test's premise, not its subject: on a slow machine the wait
# loops above outlive their 1.5 s staleness window, and the alert then gains a `:stale`
# row for the premise and the assertion below reads a three-row list. Stamping their
# `last_ok` pins the premise the docstring states ("the agent answers") and leaves the
# gateway row the only line.
for host_key in ("host:gateway", "disks:gateway"):
client.app.state.ctx.hub.jobs[host_key].state.last_ok = time.time()
alerts = gateway_alerts(client)
assert [row["id"] for row in alerts] == ["gateway:gateway:down"], (
"the reachability row is the gateway's only line while it is down")
assert client.get("/api/gateway/snapshot").json()["warnings"], (
"the page still shows the warnings beside the last good numbers — the alert went quiet, "
"the fact did not disappear")
#: The route the ingress's own line links to, taken from the one table that writes a dashboard's
#: page (`alerts.py`) rather than from a string this file would have to keep in sync with it.
GATEWAY_HREF = alerts.HREF["gateway"]
def test_the_gateway_block_links_to_the_page_the_alert_row_links_to() -> None:
"""One table says where a dashboard lives, and the summary block reads it out of that table.
`/api/summary`'s `gateway.href` is what turns the overview's line into a link, and an alert
about the same source carries an `href` too (`test_a_gateway_job_that_fails_while_its_host_…`).
Two literals would be two chances to disagree, so `api._gateway_summary` reads
`alerts.HREF[GATEWAY_DASHBOARD]`, and this pins both halves: the value is a path of this app by
`auth.safe_next`'s own rule, and the derivation is still the table rather than a fresh string.
"""
assert GATEWAY_HREF == "/gateway"
assert auth.safe_next(GATEWAY_HREF) == GATEWAY_HREF, "the block's href is not a path of this app"
assert api._gateway_summary(None, Settings())["href"] == GATEWAY_HREF
assert "alert_rules.HREF[GATEWAY_DASHBOARD]" in inspect.getsource(api._gateway_summary), (
"`_gateway_summary` stopped deriving `href` from `alerts.HREF` — the overview's link and "
"an alert about the same source can now spell the page differently")
# ---- alerts: one dead agent is one line, and a count is never a verdict ----- #
def test_a_dead_gateway_host_is_one_overview_line_not_three(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Three jobs read one agent; a dead router is one outage (`AGENTS.md`, one line per fault).
Without the echo rule the operator would get `gateway` three times on `/` — the host, its
drives, and its pipes — for the one fact that the machine is not answering. The `system` row
is the voice that stays, because it names the machine and links to the page that says most.
"""
with make_gateway_client(tmp_path, monkeypatch) as client:
deadline = time.time() + 15
while time.time() < deadline:
rows = {row["key"]: row for row in client.get("/api/health").json()["sources"]}
if rows and all(not row["ok"] for row in rows.values()) and len(rows) == 3:
break
time.sleep(0.1)
else:
pytest.fail("the three jobs of one dead agent never all recorded a failure")
alerts = client.get("/api/summary").json()["alerts"]
assert len(alerts) == 1, alerts
assert alerts[0]["source"] == "gateway"
assert alerts[0]["href"] == "/system", "the outage is the host's, and /system is its page"
def test_a_gateway_job_that_fails_while_its_host_answers_is_the_only_voice_it_has(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The other arm: the agent answers and its proxy/socket half does not.
Nothing else can say this — `/system` renders a healthy host, so a gateway job silenced by the
echo rule whenever its host is down would also be silenced here, and the fault would vanish.
The line links to `/gateway`, the page that explains which section went quiet.
"""
with make_gateway_client(tmp_path, monkeypatch) as client:
answer_as(client, "host:gateway", {"name": "gateway"})
answer_as(client, "disks:gateway", {"name": "gateway", "disks": [], "summary": {}})
deadline = time.time() + 15
while time.time() < deadline:
rows = {row["key"]: row for row in client.get("/api/health").json()["sources"]}
if (rows.get("host:gateway") or {}).get("ok") and not (rows.get("gateway") or {}).get(
"ok", True):
break
time.sleep(0.1)
else:
pytest.fail("the gateway job never failed while its host answered")
alerts = client.get("/api/summary").json()["alerts"]
assert [item["source"] for item in alerts] == ["gateway"]
assert alerts[0]["href"] == "/gateway"
assert "unreachable" in alerts[0]["message"]
#: The four figures the overview's ingress line prints, in the order it prints them. Read back out
#: of the served `GATEWAY_FIELDS` and resolved against the block the route answers with, so a
#: rename on either side fails here instead of printing `n/a` beside a busy gateway. The first two
#: are the config's own declarations and the last two what the kernel's socket table answered — the
#: same split `api._gateway_summary` writes, and the reason one number per side could never say both
#: things. The words are the **field names** (phase 30 task 02): the line, the block and the page
#: then say one thing about a route, and a rename on either side fails in three places at once.
GATEWAY_LINE_FIELDS = ("listeners", "connections", "live_in", "live_out")
GATEWAY_LINE_WORDS = ("listeners", "connections", "live in", "live out")
#: The tally beside them, which is NOT in `GATEWAY_FIELDS` because it prints only when non-zero:
#: the four figures above always have a place on the line, and `N to address` has one only when
#: there is something to address (phase 30 task 03).
GATEWAY_LINE_WARNING = ("warnings", "to address")
def test_the_overview_line_reads_the_gateway_block_it_claims(client) -> None:
"""The sixth dashboard's one line on `/` indexes the summary block and nothing else.
The `SLOT_FIELDS` device on the row phase 29 task 05 added: the served script's field table is
parsed and every name in it is resolved against the live `/api/summary` document, so a key that
quietly changes spelling fails a test rather than rendering `n/a` next to an ingress carrying
traffic. Three more claims ride on the same slice, because each is a rule the row could break
silently: the link is the block's own `href` (**no route literal in the renderer**, the rule the
alert rows keep), the row hides itself when `configured` is false instead of printing a red
nothing (invariant 3), and it carries no `severity` — a connection count is not a graded number
(`net.*` precedent), so a colour here would be a threshold invented in JavaScript.
"""
js = _code(client.get("/static/page-index.js").text)
table = re.search(r"const GATEWAY_FIELDS = \[(.*?)\n\];", js, re.S)
assert table, "page-index.js no longer has a GATEWAY_FIELDS table"
keys = re.findall(r"key: '([\w]+)'", table.group(1))
words = re.findall(r"word: '([^']+)'", table.group(1))
assert keys == list(GATEWAY_LINE_FIELDS), f"the line prints {keys}, not the pinned four"
assert words == list(GATEWAY_LINE_WORDS), f"the line labels them {words}"
body = client.get("/api/summary").json()
assert "gateway" in body, "/api/summary lost the block the line reads"
for key in (*keys, "warnings", "configured", "href", "host", "unreachable"):
assert key in body["gateway"], f"the page names `{key}` and the block does not send it"
assert not ({"partial", "unmatched", "unmatched_listeners"} & set(body["gateway"])), (
"the block grew back a field the deleted join needed")
line = js[js.index("function renderGateway("):]
line = line[:line.index("\n}")]
assert "block.href" in line, "the row stopped linking with the block's own href"
for literal in ("'/gateway'", '"/gateway"'):
assert literal not in line, f"the renderer hard-codes {literal} instead of reading `href`"
assert "if (block.configured !== true) {" in line and "row.hidden = true;" in line, (
"an unconfigured ingress no longer renders no line — the disks block's invariant-3 rule")
assert "row.classList.toggle('stale'" in line, (
"a gateway that stopped answering keeps its last counts, so the row must say they are old")
assert "severity" not in line, "the ingress line grew a verdict the block does not send"
# every figure goes through `num()`, which is what renders a `null` as `n/a` rather than `0`
assert "num(block[key], 0)" in line, (
"a live count the block did not measure must print n/a, not 0 (rule 2)")
assert "escapeHtml" in line, "a config string reached innerHTML unescaped"
# the config-integrity tally: read out of a named table (so a rename fails here, not quietly),
# and printed ONLY when it is non-zero — a clean ingress must not grow a permanent `0 to
# address`, and a `null` (an ingress that never answered) is not a number to print at all.
warning_table = re.search(r"const GATEWAY_WARNING_FIELD = (\{.*?\});", js, re.S)
assert warning_table, "page-index.js no longer names the warnings field in a table"
warn_key = re.search(r"key: '([\w]+)'", warning_table.group(1))
warn_word = re.search(r"word: '([^']+)'", warning_table.group(1))
assert warn_key and warn_word, "the tally table lost its key or its word"
assert (warn_key.group(1), warn_word.group(1)) == GATEWAY_LINE_WARNING, (
f"the line reads {warn_key.group(1)} / {warn_word.group(1)}, not the pinned tally")
assert "block[GATEWAY_WARNING_FIELD.key]" in line, (
"the renderer stopped reading the tally through its table")
assert ("` · ${block[GATEWAY_WARNING_FIELD.key]} ${GATEWAY_WARNING_FIELD.word}`" in line)
assert "> 0" in line, "the tally prints even when it is zero — a clean ingress gets no row"
assert "Number.isInteger" in line, (
"a null tally must not print: `null > 0` is false, and that is the unknown arm")
# …and the element it fills is in the fleet panel and ships hidden, so the row cannot appear
# before the payload says the source exists.
html = client.get("/").text
assert re.search(r'<p[^>]*id="fleet-gate"[^>]*hidden>', html), (
"`/` has no hidden #fleet-gate for renderGateway() to reveal")
assert html.count("<section") == 2, "the ingress line became a third overview panel"
refresh = js[js.index("async function refresh("):]
assert "renderGateway(summary.gateway)" in refresh, "the line is not fed by the page's own fetch"
def test_the_gateway_counts_grow_no_verdict_alert_even_with_verdicts_in_the_payload(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, gateway_snapshot: Any) -> None:
"""The rule the phase wrote down as an assertion: `alerts.py` has no `gateway` threshold.
Deliberately fed a payload with `severity` dicts stamped into every level of it — if a future
rule ever *read* a gateway verdict it would light up the overview over a busy proxy, which is
the job on a gateway and not a fault (`severity.py` rates no connection count, the `net.*`
precedent). An answering fleet with a fully-populated gateway payload stays all clear.
"""
payload = gateway_source.parse(gateway_snapshot("full"), "gateway")
payload["severity"] = {"listeners": "fail", "connections": "fail", "sockets": "fail"}
for row in (*payload["listeners"], *payload["connections"]):
row["severity"] = "fail"
with make_gateway_client(tmp_path, monkeypatch) as client:
answer_as(client, "host:gateway", {"name": "gateway"})
answer_as(client, "disks:gateway", {"name": "gateway", "disks": [], "summary": {}})
answer_as(client, "gateway", payload)
deadline = time.time() + 15
while time.time() < deadline:
rows = {row["key"]: row for row in client.get("/api/health").json()["sources"]}
if rows and all(row["ok"] for row in rows.values()):
break
time.sleep(0.1)
else:
pytest.fail("the three jobs never all reported ok")
body = client.get("/api/summary").json()
assert body["alerts"] == [], "a connection count is not a verdict from any direction"
assert "severity" not in body["gateway"]
def test_a_revoked_gateway_agent_fails_all_three_of_its_jobs_without_a_dial(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""`/certs` says revoke; the pipes, the drives and the host all stop talking to it.
The phase-15 pin extended to the third job. What must not happen is a *socket*: the refusal
lives in `collectors.fetch_snapshot`, in front of the network, and it is reached through the
same `AgentTls` entry the host's other two jobs hold (`agent_tls_map`, built once at startup
with a live revocation read) — which is the only reason one click covers three sources.
"""
db = str(tmp_path / "revoked-gateway.sqlite3")
store = SampleStore(db)
settings = Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_interval=0.05, disks_interval=0.05, gateway_interval=0.05,
host_targets=(Target(name="gateway", url="https://127.0.0.1:1"),))
with TestClient(create_app(settings, store)) as client:
sign_in(client)
# the control: this is what a dial of a dead host says, so the sentence after it means
# something else than "the socket failed"
wait_for_source_error(client, "host:gateway", "unreachable")
state = certs.issue_agent_cert(store, certs.ensure_certs(store), "gateway",
["127.0.0.1"])
certs.revoke(store, state, "gateway")
calls: list[str] = []
async def spy_get(self: Any, url: Any, **kwargs: Any) -> Any:
calls.append(str(url))
raise AssertionError(f"a revoked agent was dialled at {url}")
monkeypatch.setattr(httpx.AsyncClient, "get", spy_get)
for key in ("host:gateway", "disks:gateway", "gateway"):
error = wait_for_source_error(client, key, "agent certificate revoked")
assert "unreachable" not in error
assert calls == [], "the gate is in front of the network, not behind it"
messages = [row["message"] for row in client.get("/api/summary").json()["alerts"]]
assert sum(1 for text in messages if "certificate revoked" in text) == 1, (
"one revoked agent is one line, whatever number of jobs read it")
store.close()
# ---- the `/api/gateway/*` routes (phase 29 task 03) ------------------------ #
#
# The page's two data routes, in the shape the `/disks` pair established: the configured gate runs
# before anything that could reach the network (both arms spy-pinned), an answering payload keeps
# the projection's exact keys, a source that has never answered is the `unreachable` stub of
# `null`s, and the history route lands on the store's one batched door and answers the points a
# direct `query()` read returns. A fifth data page grows no write of its own (invariant 1), which
# is what the verb enumeration below is for.
# Every key `/gateway` indexes, in the arm that measured. Read off a live `parse` rather than
# hand-written, so a rename in `sources/gateway.py` fails the route pins instead of turning a
# card into `n/a` beside a healthy ingress.
def gateway_payload_keys(snapshot: Any) -> set[str]:
return set(gateway_source.parse(snapshot, "gateway"))
def find_keys(node: Any, name: str) -> list[Any]:
"""Every value named `name` anywhere in a payload — the no-verdict audit."""
if isinstance(node, Mapping):
found = [value for key, value in node.items() if key == name]
for value in node.values():
found.extend(find_keys(value, name))
return found
if isinstance(node, list):
return [hit for item in node for hit in find_keys(item, name)]
return []
def _gateway_routes_client(tmp_path: Path, mode: str,
gateway_snapshot: Any) -> Iterator[TestClient]:
"""One app per capture mode, its gauges already stored, its payload already in the job state.
Both halves come from `sources/gateway` over the **captured** tree, so every path a page reads
and every series name a chart asks for is one the source really emits (rule 7). The target
points at a dead port: no poll can overwrite the payload, dial out, or add a second sample
between the store read and the route assertion below.
"""
db = str(tmp_path / f"gateway-routes-{mode}.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=(GATEWAY_TARGET,)), store)
with TestClient(app) as test_client:
sign_in(test_client)
now = time.time()
snapshot = gateway_snapshot(mode)
snapshot["ts"] = now # "the agent answered just now", so charts are in range
result = gateway_source.parse_gateway_snapshot("gateway", snapshot, now=now)
store.add_many(result.samples)
app.state.ctx.hub.jobs["gateway"].state.payload = result.payload
yield test_client
store.close()
@pytest.fixture
def gateway_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
gateway_snapshot: Any) -> Iterator[TestClient]:
"""The ingress answering as agent **1.2.0** (or 1.3.0 with `--no-resolve`) does.
No `resolver` section, so the routes whose targets the two config files write as domains carry
no count, no gauge and no stored series — phase 30 verbatim, which is what the phase-32 pins
below compare against.
"""
yield from _gateway_routes_client(tmp_path, "full", gateway_snapshot)
@pytest.fixture
def resolved_gateway_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
gateway_snapshot: Any) -> Iterator[TestClient]:
"""The same ingress answering as agent **1.3.0**: the captured resolver section attached.
The document is the recorded `gateway_resolver.json`, so every series name this fixture's store
holds is one the deployed ingress will really write after task 05 — including the **name-keyed**
outbound gauges phase 30 stopped writing, which are writable again precisely because the
gateway published the address behind the name.
"""
yield from _gateway_routes_client(tmp_path, "resolved", gateway_snapshot)
def dial_spies(monkeypatch: pytest.MonkeyPatch, calls: list[str]) -> None:
"""Two spies that raise: a route that dials before its gate fails here, not in a timeout.
The `disks` pair's instrument, named once because three routes are now pinned with it (the
snapshot's two unconfigured arms and the history route's) and a copy per test is how one of
them stops raising.
"""
async def spy_get(self: Any, url: Any, **kwargs: Any) -> Any:
calls.append(f"GET {url}")
raise AssertionError(f"the unconfigured gateway route dialled {url}")
def spy_fetch(*args: Any, **kwargs: Any) -> Any:
calls.append("fetch_snapshot")
raise AssertionError("the unconfigured gateway route fetched an agent snapshot")
monkeypatch.setattr(httpx.AsyncClient, "get", spy_get)
monkeypatch.setattr("dashboards.collectors.fetch_snapshot", spy_fetch)
def test_gateway_snapshot_404s_when_no_host_is_named_the_gateway(
client, monkeypatch: pytest.MonkeyPatch) -> None:
"""`GATEWAY_HOST` unset and no targets: `404 {"configured": false}`, nothing dialled.
Invariant 3, and the arm a deployment without an ingress lives in. The spies raise, so a route
that built a client or fetched a snapshot before testing `gateway_enabled` fails here instead
of costing a connect timeout and a red dot, and the empty hub is the structural half: with no
targets there is no `gateway` job that could age out.
"""
calls: list[str] = []
dial_spies(monkeypatch, calls)
response = client.get("/api/gateway/snapshot")
assert response.status_code == 404
body = response.json()
assert body["configured"] is False
assert "GATEWAY_HOST" in body["detail"], "the sentence names the fix"
assert calls == [], "an unconfigured page must not touch the network"
assert client.app.state.ctx.hub.jobs == {}, ( # type: ignore[attr-defined]
"no targets means no gateway job, so nothing can age out or go red")
@pytest.mark.parametrize("gateway_host", [None, "router"])
def test_gateway_snapshot_404s_when_the_name_is_not_a_target(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, gateway_host: str | None) -> None:
"""A name with no agent behind it is unconfigured, whatever the reason for the mismatch.
`None` is the trap this default creates: `GATEWAY_HOST` defaults to `gateway`, so a deployment
that monitors `deskwork` only must not grow a source dialling a machine nobody configured.
`"router"` is the opposite typo — a name set on purpose that the target list does not hold.
Both answer the same refusal with the same no-dial proof.
The two host jobs' first polls are awaited **before** the spies go in: the hub polls once the
moment it starts, and a spy that catches `host:deskwork` dialling its own dead target would be
a failure about a job this route never touched. With both intervals at an hour, the refusal is
measured against a hub that has already said everything it has to say.
"""
overrides: dict[str, Any] = {"host_targets": (Target("deskwork", DEAD_URL),),
"host_interval": 3_600.0, "disks_interval": 3_600.0}
if gateway_host is not None:
overrides["gateway_host"] = gateway_host
with build_client(tmp_path, monkeypatch, **overrides) as client:
wait_for_source_error(client, "host:deskwork", "unreachable")
wait_for_source_error(client, "disks:deskwork", "unreachable")
calls: list[str] = []
dial_spies(monkeypatch, calls)
body = client.get("/api/gateway/snapshot").json()
assert body["configured"] is False
assert "GATEWAY_HOST" in body["detail"], "the sentence names the fix"
assert calls == [], "the gate is in front of the network, not behind it"
jobs = client.app.state.ctx.hub.jobs # type: ignore[attr-defined]
assert "gateway" not in jobs, "an unconfigured name grew a job that could look red"
assert sorted(jobs) == ["disks:deskwork", "host:deskwork"]
def test_the_gateway_gate_keys_on_the_name_and_not_on_a_default(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The control for both refusals above: name the host that **is** a target and it opens.
Same fleet, one changed setting, and the route answers — with the stub, because the target is
still a dead port. Without this leg a broken router would look identical to a correct gate.
"""
with build_client(tmp_path, monkeypatch, host_targets=(Target("deskwork", DEAD_URL),),
gateway_host="deskwork", host_interval=3_600.0) as client:
body = client.get("/api/gateway/snapshot").json()
assert body["configured"] is True and body["unreachable"] is True
assert body["host"] == "deskwork"
assert [row["key"] for row in body["states"]] == ["gateway"]
def test_gateway_snapshot_answers_the_pipes_the_projection_joined(
gateway_client, gateway_snapshot: Any) -> None:
"""The answering arm is `sources/gateway.parse`'s payload, key for key, plus `states`.
Two things are pinned at once. The **shape**: every key the page indexes is there, so a rename
in the source fails this test rather than printing `n/a` beside a live ingress. And the
**arithmetic** phase 30 task 02 replaced the old equality with:
`sum(listeners) + sum(distinct endpoints) <= established`, with the difference reported nowhere
— a socket neither config file describes is not a number on this page, and the phase deleted
the card that used to hold it. `sockets.established` stays the kernel's own row count beside
it, so the reader can see the gap rather than being told the two agree.
"""
body = gateway_client.get("/api/gateway/snapshot").json()
assert set(body) - {"states"} == gateway_payload_keys(gateway_snapshot("full")), (
"the route grew or lost a top-level key of the source's shape")
assert body["configured"] is True and body["supported"] is True
assert body["host"] == "gateway"
assert body["why"] is None, "a page with pipes has nothing to apologise for"
assert body["sections"] == {"haproxy": True, "nginx": True, "sockets": True}
assert not ({"outbound", "unmatched", "partial"} & set(body)), (
"the deleted join is back in the payload")
# the config half, verbatim from the capture the agent read (phase 28's six frontends, the
# stream map's 18 routing entries) — the echo the pipes were built from
assert len(body["haproxy"]["frontends"]) == 6
assert len(body["nginx"]["map_targets"]) == 18
# `/proc/net/tcp` is one row per live connection, so every count below is derived from the same
# capture the payload was built from instead of typed here: the rule is the pin, the number is a
# reading. The config-shaped figures above stay literal, because only the operator moves those.
established = gateway_snapshot("full")["sockets"]["established"]
stream = next(pipe for pipe in body["listeners"] if pipe["port"] == 443)
assert stream["connections"] == sum(1 for row in established
if row["local"]["port"] == 443), (
"the inbound pipe carries the sockets whose LOCAL port it is, which is the whole join")
assert stream["metric"] == "gateway.in_conns@443", (
"the pipe carries the series it is, so the page never rebuilds the name")
assert stream["clients"], "the inbound pipe names the addresses that are using it"
secure = next(pipe for pipe in body["listeners"] if pipe["port"] == 4443)
assert "nextcloud.reeseapps.com" in secure["domains"], "the listen port carries its server_names"
# The graph: every route the two captured configs declare, each line's inbound end a trunk this
# same payload draws, and a count on **only** the lines whose endpoint the config wrote as an
# address — this capture has exactly two (`127.0.0.1:4443` and `127.0.0.1:8081`).
assert len(body["connections"]) == 33
listener_ports = {listener["port"] for listener in body["listeners"]}
assert listener_ports == {22, 2000, 5201, 8080, 25565, 25566, 80, 443, 4443, 8443}
assert all(row["in"]["port"] in listener_ports for row in body["connections"]), (
"a connection on a port no listener carries is an orphan the page cannot draw")
def sockets_to(host: str, port: int) -> int:
return sum(1 for row in established
if (row["remote"]["addr"], row["remote"]["port"]) == (host, port))
counted = {row["id"]: row["connections"] for row in body["connections"]
if row["connections"] is not None}
assert counted == {"nginx:443->127.0.0.1:4443": sockets_to("127.0.0.1", 4443),
"nginx:8443->127.0.0.1:8081": sockets_to("127.0.0.1", 8081)}, (
"a route written as a domain carries no count, and one written as an address carries the "
"endpoint's own — 0 included, because the socket table answered")
assert all(row["out"]["address"] is True for row in body["connections"]
if row["connections"] is not None)
assert all(row["metric"] is None for row in body["connections"]
if row["connections"] is None), "no series for a number nobody can measure"
known_in = sum(pipe["connections"] for pipe in body["listeners"]
if pipe["connections"] is not None)
endpoints = {(row["out"]["host"], row["out"]["port"]) for row in body["connections"]
if row["connections"] is not None}
assert known_in + sum(counted.values()) <= body["sockets"]["established"] == len(established)
assert len(endpoints) == 2
assert [row["key"] for row in body["states"]] == ["gateway"]
def test_the_served_snapshot_counts_a_resolved_name_and_says_which_address_it_used(
resolved_gateway_client) -> None:
"""`/api/gateway/snapshot` on the 1.3.0 document: the dash is gone, and the reason is in the row.
One route, both halves of the claim. `brain.reeseapps.com` leaves this ingress for
`reeseapps-brain-of-reese.reeselink.com:443`; the row now carries `address: false` (the config
wrote a name — verbatim, still), `resolved: ["10.1.2.64"]`, `via: "libc"` and that entry's own
`resolved_ts`, **and** a finite `connections`. A number with no provenance beside it would be
the invention this app refuses to print, which is why the two halves are one test.
"""
body = resolved_gateway_client.get("/api/gateway/snapshot").json()
assert body["resolver"]["available"] is True and body["resolver"]["via"] == "libc"
brain = next(row for row in body["connections"]
if row["out"]["host"] == "reeseapps-brain-of-reese.reeselink.com")
assert brain["out"]["selectors"] == ["brain.reeseapps.com"]
assert brain["out"]["address"] is False
assert brain["out"]["resolved"] == ["10.1.2.64"]
assert brain["out"]["via"] == "libc"
assert isinstance(brain["out"]["resolved_ts"], (int, float))
assert isinstance(brain["connections"], int) and not isinstance(brain["connections"], bool), (
"resolved and idle is a `0`, not the dash: the socket table answered and this endpoint has none")
assert brain["metric"] == "gateway.out_conns@reeseapps-brain-of-reese.reeselink.com:443"
ntfy = next(row for row in body["connections"]
if row["out"]["host"] == "reeseapps-ntfy.reeselink.com")
assert ntfy["connections"] > 0, "the capture has live ntfy sockets behind that name"
counted = [row for row in body["connections"] if row["connections"] is not None]
assert len(counted) == len(body["connections"]), (
"every route this ingress declares is placed by the recorded resolver, so no route on the "
"live page is a dash any more — the operator's complaint, in one assertion")
assert all(row["out"]["address"] is True or row["out"]["resolved"] for row in counted), (
"a count never arrives without the address it was counted through — one the config wrote, "
"or one the gateway published; `resolved` stays null on the address rows, because copying "
"the agent's `literal` echo of a string the config already wrote would claim a lookup that "
"never happened")
def test_an_injected_nxdomain_row_is_a_dash_in_the_served_document(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, gateway_snapshot: Any) -> None:
"""Both arms on one route list: placed is a number, unplaced is `null` / `null`.
The capture resolves everything, so the second arm is injected: one entry of the recorded
document emptied and marked `nxdomain`, exactly what agent 1.3.0 publishes when `getaddrinfo`
says a name has no record. Its line keeps its place in the graph with `resolved: null`,
`via: null`, `connections: null` and `metric: null` — no `0`, no series, no sample — while the
routes around it carry their counts, so a page cannot read one failed name as a dead ingress.
"""
broken = gateway_snapshot("resolved")
entries = [entry for entry in broken["resolver"]["entries"]
if entry["name"] == "reeseapps-immich.reeselink.com"]
assert entries, "the capture resolves immich, so the injection has something to un-resolve"
for entry in entries:
entry["addresses"] = []
entry["status"] = "nxdomain"
payload = gateway_source.parse(broken, "gateway")
with make_gateway_client(tmp_path, monkeypatch) as client:
answer_as(client, "gateway", payload)
deadline = time.time() + 10
while time.time() < deadline and not client.app.state.ctx.hub.jobs["gateway"].state.ok:
time.sleep(0.05)
body = client.get("/api/gateway/snapshot").json()
row = next(item for item in body["connections"]
if item["out"]["host"] == "reeseapps-immich.reeselink.com")
assert row["out"]["resolved"] is None, "never `[]`, so the page's dash arm is one `is null` test"
assert row["out"]["via"] is None and row["out"]["resolved_ts"] is None
assert row["connections"] is None and row["metric"] is None
assert row["in"] == {"port": 443}, "the route is still drawn: a name with no record is not no route"
placed = [item for item in body["connections"] if item["connections"] is not None]
assert len(placed) == len(body["connections"]) - 1, (
"one unresolved name dims one line and nothing else")
documented = {entry["name"]: entry["status"] for entry in body["resolver"]["entries"]}
assert documented["reeseapps-immich.reeselink.com"] == "nxdomain", (
"the entry the index refused to use is still the entry the page can quote")
def test_no_port_the_two_configs_do_not_name_appears_anywhere(gateway_client,
gateway_snapshot: Any) -> None:
"""The live router's own agent port is `9120`, and this payload never mentions it.
Phase 29 shipped a card that reported it. Phase 30 deleted the reporting: `9120` (and `2222`,
and everything else outside the two files) is a fact about the host that `/system` answers, and
filed under no pipe, no total and no warning here. Asserted over the **serialised** document,
because "not in a field I thought to check" is not the claim.
"""
snapshot = gateway_snapshot("full")
live = {row["port"] for row in snapshot["sockets"]["listens"]}
assert 9120 in live, "the capture must still contain the unclaimed port for this to mean anything"
body = gateway_client.get("/api/gateway/snapshot").json()
text = json.dumps(body)
assert ":9120" not in text and "@9120" not in text
assert 9120 not in {row["port"] for row in body["listeners"]}
assert 9120 not in {row["in"]["port"] for row in body["connections"]}
assert 9120 not in {row["out"]["port"] for row in body["connections"]}
def test_the_gateway_snapshot_carries_no_verdict_and_no_window(gateway_client) -> None:
"""A connection count is neither a graded number nor a rate, at any depth of the payload.
`severity.py` rates no `gateway.*` metric and the source is handed no `RateEstimator`, so the
route must not grow either either: a `severity` anywhere would colour a busy proxy (the
phase-06 GPU reasoning, applied to the ingress) and a `window_seconds` would claim a Δ was
measured when nothing was (rule 2).
"""
body = gateway_client.get("/api/gateway/snapshot").json()
assert body["listeners"] and body["connections"], (
"an empty walk would pass this pin for the wrong reason")
assert body["warnings"] == [], (
"this fleet's captured configs connect to themselves — the seven checks are not a lie "
"about the ingress the page draws")
assert find_keys(body, "severity") == [], "no count on this page is graded"
assert find_keys(body, "window_seconds") == [], "nothing here was measured over a window"
# `by_port` belonged to the join that credited a **port** for a target written as a name. The
# graph claims no join on a name at all, so the field has nowhere honest to live.
assert find_keys(body, "by_port") == [], "the port-only arm of the join is gone from the payload"
assert find_keys(body, "unmatched") == [], "no residual is reported, so no residual is labelled"
def test_gateway_snapshot_stubs_the_ingress_that_has_never_answered(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Configured, dialling, nothing in yet: the row stays and every count is unknown.
Invariant 4's page-side half, the `system.hosts` / `/api/disks` stub rule: dropping the host
would read as "never configured" and zeroing it would read as an idle gateway, so the answer
is `unreachable: true` with `null` counts and the `gateway` health record beside them. The
stub also has to carry **every key the answering payload carries** (plus its own
`unreachable`), or the page that renders both arms has to guess which keys exist.
"""
with make_gateway_client(tmp_path, monkeypatch) as client:
wait_for_source_error(client, "gateway", "unreachable")
body = client.get("/api/gateway/snapshot").json()
assert body["configured"] is True, "the source is in the deployment"
assert body["unreachable"] is True
assert body["host"] == "gateway", "the name is a config fact, so it survives the outage"
assert body["supported"] is None, "nobody has read its version"
assert body["listeners"] == [] and body["connections"] == []
assert body["haproxy"]["frontends"] == []
assert body["nginx"] == {"servers": [], "map_targets": []}
assert body["sockets"] == {"listens": None, "established": None}, "unknown, never 0"
assert not ({"outbound", "unmatched", "partial"} & set(body)), (
"the stub grew a key the deleted join needed")
assert body["why"] and "not answered" in body["why"], "a sentence the page can print"
assert body["ts"] is not None, "the age of the attempt is what the page prints"
assert [row["key"] for row in body["states"]] == ["gateway"]
def test_the_gateway_stub_and_the_answer_share_one_key_set(gateway_client) -> None:
"""The two arms differ in exactly two keys, so no page branch can index a missing one.
The stub cannot publish an `agent_version` (nobody answered, so nobody said) and must publish
its own `unreachable`. Everything else — every section, pipe list, tally and socket count the
answering arm carries — is there in both, which is what lets `/gateway` render one card body
for a live ingress and for a host that has never replied.
"""
answered = set(gateway_client.get("/api/gateway/snapshot").json()) - {"states"}
assert set(gateway_source.stub("gateway", ts=1.0)) == (
answered - {"agent_version"} | {"unreachable"})
def test_gateway_history_serves_the_pipe_gauges_the_store_holds(
gateway_client) -> None:
"""The points are the stored rows — read a third way, through `store.query`.
Phase 14 task 02's pin shape: the route goes through `_history_batch` → `store.history_many`
and the test reads `store.query`, and neither is the per-series loop the batch replaced, so an
"optimisation" that moves, drops or reorders a point fails here. Both gauge families are read,
the outbound one **with its port** in the name (two Minecraft backends on one host are two
facts), and the `source` says the `gateway` family — a page must never ask the 10-second `host`
series for a connection count.
The outbound name asked for is an **address** (`127.0.0.1:4443`) because this fixture answers as
a 1.2.0 agent. The rule `samples()` keeps is about the **address**, not the spelling: a route
publishes a gauge when the kernel can name its far end — an address the config wrote, or (since
phase 32 task 03, pinned beside this one on the `resolved` arm) a name the gateway itself
resolved. A name nobody resolved publishes nothing, which is what
`test_gateway_history_answers_empty_for_a_pipe_it_never_measured` still proves.
"""
store: SampleStore = gateway_client.app.state.ctx.store # type: ignore[attr-defined]
body = gateway_client.get("/api/gateway/history", params={
"hosts": "gateway",
"metrics": "gateway.in_conns@443,gateway.in_conns@4443,"
"gateway.out_conns@127.0.0.1:4443",
"range": "1h"}).json()
assert body["hosts"] == ["gateway"]
assert body["range_seconds"] == 3_600.0
assert [series["metric"] for series in body["series"]] == [
"gateway.in_conns@443", "gateway.in_conns@4443",
"gateway.out_conns@127.0.0.1:4443"], "host-major, metric-minor, request order"
assert series_source(body) == {"gateway"}, "its own family, never the system one"
for series in body["series"]:
rows = store.query("gateway", "gateway", series["metric"], res=RES_RAW)
assert rows, f"{series['metric']} has no stored row: the fixture wrote nothing"
assert [(point["ts"], point["value"]) for point in series["points"]] == rows
def test_gateway_history_answers_empty_for_a_pipe_it_never_measured(gateway_client) -> None:
"""Unknown names are an empty series, not a 404 — the contract the other routes have.
A card asks for every pipe it could draw in one request: a target with no traffic in the
window, a port the configs stopped naming, and a **domain** the source stopped storing because
nothing could ever count it all draw an empty plot while the series that do exist still answer.
"""
body = gateway_client.get("/api/gateway/history", params={
"hosts": "gateway",
"metrics": "gateway.in_conns@9999,gateway.out_conns@nowhere.test:1,"
"gateway.in_conns@443",
"range": "24h"}).json()
by_metric = {series["metric"]: series["points"] for series in body["series"]}
assert by_metric["gateway.in_conns@9999"] == []
assert by_metric["gateway.out_conns@nowhere.test:1"] == []
assert by_metric["gateway.in_conns@443"], "the series that does exist still answers"
assert series_source(body) == {"gateway"}
def test_a_resolved_name_publishes_its_gauge_and_history_answers_it(
resolved_gateway_client) -> None:
"""The name-keyed series phase 30 stopped writing, written again on an honest footing.
Phase 30 task 02 retired `gateway.out_conns@<domain>:<port>` because the only figure it could
have carried was the *port's* — the port-only join's count filed under whatever domain happened
to share it. Phase 32 task 03 gives the name a figure of its own: the sockets to the address the
**gateway** resolved that name to. So the name is stored, `/api/metrics` lists it, and
`/api/gateway/history` answers points for it — read through `store.query`, the third path, as
every other history pin is.
"""
store: SampleStore = resolved_gateway_client.app.state.ctx.store # type: ignore[attr-defined]
metric = "gateway.out_conns@reeseapps-ntfy.reeselink.com:443"
body = resolved_gateway_client.get("/api/gateway/history", params={
"hosts": "gateway", "metrics": metric, "range": "1h"}).json()
assert [series["metric"] for series in body["series"]] == [metric]
assert series_source(body) == {"gateway"}
rows = store.query("gateway", "gateway", metric, res=RES_RAW)
assert rows, "the resolved route's gauge was not stored"
assert [(point["ts"], point["value"]) for point in body["series"][0]["points"]] == rows
assert metric in resolved_gateway_client.get("/api/metrics",
params={"source": "gateway"}).json()["metrics"]
def test_one_shared_endpoint_costs_one_insert_per_poll_even_through_a_name(
tmp_path: Path, gateway_snapshot: Any) -> None:
"""Two lines, one gauge name, **one row per poll** — counted in the store, not asserted.
The captured ingress reaches `keycloak.reeselink.com:8080` from `:4443` and from `:8443`, and
since task 03 the endpoint is reachable through a **resolved name** rather than a written
address. `shared: true` says the two lines carry one number; `samples()` de-duplicating by
metric is what stops that number being inserted twice for one reading, and the row count is the
only proof worth having. Two polls, two rows: one per reading, never one per line.
"""
db = str(tmp_path / "gateway-shared.sqlite3")
store = SampleStore(db)
now = time.time()
metric = "gateway.out_conns@keycloak.reeselink.com:8080"
# both readings in the past: `store.query` bounds its window at now, so a simulated second poll
# that lands in the future is a row that never comes back, not a lost insert
for offset in (-60.0, -30.0):
snapshot = gateway_snapshot("resolved")
snapshot["ts"] = now + offset
result = gateway_source.parse_gateway_snapshot("gateway", snapshot, now=now + offset)
shared = [row for row in result.payload["connections"] if row["shared"]]
assert {row["metric"] for row in shared} == {metric}, (
"this capture's shared endpoint is the keycloak pair, and one poll writes it once")
assert len({row["connections"] for row in shared}) == 1, (
"both lines carry the same number, which is what makes one insert the honest count")
out = [row for row in result.samples if row.metric == metric]
assert len(out) == 1, "one sample per poll, however many lines reach the endpoint"
store.add_many(result.samples)
rows = store.query("gateway", "gateway", metric, res=RES_RAW)
assert len(rows) == 2, "two polls, two rows — not four (one per line) and not one (lost poll)"
store.close()
def test_gateway_history_names_the_ingress_and_no_other_host(gateway_client) -> None:
"""With no `hosts=` the answer is the one host this source reads — not the whole fleet.
The gauges are stored under the `GATEWAY_HOST` name, so defaulting to every `HOST_TARGETS`
entry would draw a card of empty plots for machines that were never asked to route, and
defaulting to nothing would leave the page's disclosure with nothing to ask for.
"""
body = gateway_client.get("/api/gateway/history",
params={"metrics": "gateway.in_conns@22"}).json()
assert body["hosts"] == ["gateway"]
assert body["range_seconds"] == 86_400.0, "the page's default window, like /disks"
assert [(series["host"], series["metric"]) for series in body["series"]] == [
("gateway", "gateway.in_conns@22")]
assert body["series"][0]["points"], "the gauge was stored under that very name"
def test_gateway_history_dials_nothing_when_the_gateway_is_unconfigured(
client, monkeypatch: pytest.MonkeyPatch) -> None:
"""History reads only SQLite, so an unconfigured ingress costs no dial and no error.
The `/api/disks/history` rule: the configured gate belongs to the route that could reach the
network, and a page that asks for history anyway gets no hosts and no series rather than a
404 it has to special-case. The spies are the proof, not the empty body — an empty answer that
had dialled would be the invariant-3 failure with a 200 attached.
"""
calls: list[str] = []
dial_spies(monkeypatch, calls)
body = client.get("/api/gateway/history",
params={"metrics": "gateway.in_conns@443"}).json()
assert body["hosts"] == []
assert body["series"] == []
assert calls == []
def test_the_metrics_endpoint_answers_for_the_gateway_family(gateway_client) -> None:
""""Why is this chart empty" has an answer per page: /api/metrics?source=gateway.
The fifth family stays out of the 10-second `host` list, which is the other half of the same
promise — a connection count charted from the system series would be a number from a source
that never measured it.
"""
names = gateway_client.get("/api/metrics", params={"source": "gateway"}).json()["metrics"]
assert "gateway.in_conns@443" in names
assert not [name for name in names if not name.startswith("gateway.")], (
"the gateway family holds only gateway series")
assert not [name for name in gateway_client.get(
"/api/metrics", params={"source": "host"}).json()["metrics"]
if name.startswith("gateway.")], "a pipe's gauge never mixes into the system family"
def test_the_gateway_surface_is_gets_only_and_the_write_stays_the_ups_one(
gateway_client) -> None:
"""A fifth data page grows no write: the app's one upstream POST is still UPS's (invariant 1).
The read-only half of the audit grows with every surface, and this is the half that catches a
POST arriving quietly — the two gateway routes are GETs, they refuse every other verb, and the
app-wide write table is still the UPS command plus the session and certificate-authority rows
that touch only this process.
"""
routes = flat_routes(gateway_client.app)
gate = [route for route in routes
if str(getattr(route, "path", "")).startswith("/api/gateway")]
assert {route.path for route in gate} == {"/api/gateway/snapshot",
"/api/gateway/history"}
verbs = {verb for route in gate for verb in (getattr(route, "methods", None) or set())}
assert verbs == {"GET"}, "a write appeared on the connection-topology surface"
for verb in ("POST", "PUT", "PATCH", "DELETE"):
response = gateway_client.request(verb, "/api/gateway/snapshot", json={})
assert response.status_code == 405, f"{verb} /api/gateway/snapshot was accepted"
writes = sorted(str(route.path) for route in routes
if "POST" in (getattr(route, "methods", None) or set()))
upstream = [path for path in writes if not path.startswith(("/api/auth/", "/api/certs/"))]
assert upstream == ["/api/ups/command"], "one write endpoint, four gates, unchanged"
# ---- the `/gateway` page (phase 29 task 04) -------------------------------- #
#
# The fifth dashboard, asserted the way the other four are: on the served bytes, because a JS
# test runner would be a build step (a locked anchor). What the page *is* decides what is worth
# pinning. It draws **pipes**, and a pipe's only claim is a connection count, so the pins are: the
# geometry animates while that count is at least one and rests at zero (a static drawing of a live
# connection reads as idle, and an animated zero reads as traffic — both are the lie this page can
# tell); every pipe says what it is to assistive tech, because a moving line says nothing to a
# screen reader; nothing is coloured by a verdict, because `severity.py` rates no `gateway.*`
# metric and the API sends none; every label is the config's own text, so a rename in
# `sources/gateway.py` fails here instead of printing `n/a` beside a healthy ingress. Phase 30
# task 02 removed the last one of those — the unreconciled remainder is no longer printed, because
# a socket outside the two config files is not this page's business — so the pins below assert its
# **absence** beside the rules that survived. `data/logs/29-task04-pipes.cjs` is the browser half.
#: Every payload path the page's script indexes or keys a row by, in the arm that measured. Resolved
#: against a live `GET /api/gateway/snapshot` below rather than written from the source, so the two
#: sides of the join are checked against each other (`SLOT_FIELDS`' / `COLUMNS`' device). Phase 30
#: task 04 dropped `sockets.listens` / `sockets.established` (the kernel's table size is on no cell
#: any more — a tally the map cannot reconcile is one an operator learns to skip) and
#: `nginx.map_targets.*` / `haproxy.frontends.*` (neither is a row any more: the fan-out arrives as
#: `connections[]` carrying its own `out.kind`, which
#: `test_the_stream_map_is_rendered_inside_nginx_and_nowhere_else` pins), and added `warnings.*`.
#: Phase 31 task 02 put the four `connections.out.*` paths to a second use: they are exactly what the
#: leaf row's two text helpers index (`inboundText` → `selectors` then `kind`, `counterpartText` →
#: `host` + `port`), so `test_the_leaf_row_leads_with_the_inbound_word` resolves the same tuple the
#: page reads against a live document and a rename fails by name rather than printing `undefined`.
GATEWAY_PAGE_FIELDS = (
"host", "supported", "why", "agent_version",
"listeners.port", "listeners.source", "listeners.domains", "listeners.connections",
"listeners.clients", "listeners.metric",
"connections.id", "connections.source", "connections.in.port",
"connections.out.host", "connections.out.port", "connections.out.name",
"connections.out.kind", "connections.out.selectors", "connections.out.address",
# Phase 32 task 04: the provenance a counted line prints and the dash refuses. `resolved` is what
# the filter matches an IP against, `via` + `resolved_ts` are the tooltip's `address from libc,
# resolved 3m` — so a rename of any of the three fails by name here, not as a permanent silence.
"connections.out.resolved", "connections.out.via", "connections.out.resolved_ts",
"connections.connections", "connections.shared", "connections.metric",
"warnings.id", "warnings.kind", "warnings.subject", "warnings.message",
)
#: The panels, in the order the page ships them: the two config files, keyed on the payload's own
#: `source` words, nginx first (the operator named it first, and it carries this fleet's widest
#: fan-out). `map` and `unmatched` are gone (phase 30 task 02) — a stream `map` is nginx config, so
#: its routes are that panel's `:443` leaves, and there is no residual panel because nothing outside
#: the two files is reported at all.
GATEWAY_CARD_KEYS = ("nginx", "haproxy")
def _payload_field(node: Any, path: str) -> tuple[bool, list[Any]]:
"""`(found, values)` for one dotted path, where a segment naming a list maps over it.
`found` is False only when a **key** is missing, which is the rename this checks for. A null
value is a found key: the arms that did not measure publish nulls on purpose (rule 2), and the
page's job is to render them as `n/a`, which the arms test below pins separately.
"""
segments = path.split(".")
if not segments[0]:
return False, []
head, rest = segments[0], segments[1:]
if isinstance(node, list):
values: list[Any] = []
for item in node:
found, mapped = _payload_field(item, path)
if not found:
return False, []
values.extend(mapped)
return True, values
if isinstance(node, dict):
if head not in node:
return False, []
if not rest:
return True, [node[head]]
return _payload_field(node[head], ".".join(rest))
return False, []
def one_real_warning() -> list[dict[str, str]]:
"""One warning row written by `sources/gateway.warnings()`, not by this file.
The page renders `warnings` verbatim, so a fixture has to carry the words the source actually
produces — hand-written prose here would pass a page test while the real sentence changed shape.
The section is the smallest one that trips `haproxy_route_dangling`: a frontend routing to a
backend no `backend` block declares, which is the kind this fleet's capture provably does **not**
have (pinned in `tests/test_gateway_source.py`).
"""
broken = {"available": True,
"frontends": [{"name": "ssh", "default_backend": "gitea-2222",
"backends": ["gitea-2222"],
"binds": [{"addr": "::", "port": 22}]}],
"backends": []}
found = gateway_source.warnings(broken, None)
assert len(found) == 1 and found[0]["kind"] == "haproxy_route_dangling", found
return found
def _warnings_section(html: str) -> str:
"""The warnings panel's own markup, from its opening tag to its `</section>`."""
start = html.index('id="gate-warnings"')
head = html.rindex("<", 0, start)
return html[head:html.index("</section>", start) + len("</section>")]
def test_the_gateway_page_is_exactly_two_connection_panels_and_a_warnings_list(client) -> None:
"""The page's shape, on the bytes it serves: two panels, one warnings list, four tallies.
Exactly two `[data-gate]` articles — `nginx` then `haproxy`, **ordered**, because the operator
named nginx first and it carries this fleet's widest fan-out — each shipping the `gate-map` its
trunk→leaf groups are injected into, its `empty` sentence and its chart disclosure. No `stream
map` panel and no `unmatched` panel: the `map` is nginx config, so its seventeen entries are that
panel's `:443` leaves, and a socket neither file describes is reported nowhere (phase 30 task
02). Above the panels sits `#gate-warnings`, which ships `hidden` because a clean ingress has
nothing to list and a permanent empty "Warnings to address" is the ink the empty-state rules
exist to avoid.
The four tallies are what the two configs **declare** (`ports`, `connections`), the one number
the kernel adds (`live`, the open sockets on a port a config names) and `warnings`. The socket
table's own size is not a cell any more: it is not reconcilable with the map, and a tally an
operator cannot account for is one they learn to skip. There is also no `#range` control, and
that is a decision, not an omission: the history here is a count trend and one window for every
panel is the honest default for a source that re-reads every 30 seconds.
"""
html = _code(client.get(GATEWAY_PAGE).text)
keys = re.findall(r'data-gate="([^"]+)"', html)
assert keys == list(GATEWAY_CARD_KEYS), (
f"/gateway ships {keys}; the page is exactly the two config panels, nginx first")
assert 'data-gate="map"' not in html, "the stream map grew a panel of its own back"
assert 'data-gate="unmatched"' not in html, "the residual grew a panel back"
for key in GATEWAY_CARD_KEYS:
card = html[html.index(f'data-gate="{key}"'):]
card = card[:card.index("</article>")]
assert 'class="gate-map" data-k="map"' in card, f"the {key} panel has no group list"
assert 'data-k="empty"' in card, f"the {key} panel has no empty sentence"
assert 'data-chart="conns"' in card, f"the {key} panel has no count plot"
assert 'data-legend="conns"' in card, f"the {key} panel's plot has no legend"
panel = _warnings_section(html)
assert "hidden" in panel[:panel.index(">")], (
"the warnings panel ships visible, so a clean ingress renders an empty card")
assert '<ul class="alerts" id="warnings-list">' in panel, (
"the warnings rows do not use the overview's `ul.alerts` vocabulary")
assert 'id="warnings-count"' in panel
for tally in ("t-ports", "t-connections", "t-live", "t-warnings"):
assert f'id="{tally}"' in html, f"/gateway lost the {tally} tally"
for gone in ("t-listeners", "t-established", "t-inbound", "t-outbound", "t-unmatched"):
assert f'id="{gone}"' not in html, f"/gateway grew the {gone} cell back"
assert 'id="range"' not in html, "/gateway grew window chips over a gauge that has no rate"
assert 'src="/static/page-gateway.js"' in client.get(GATEWAY_PAGE).text
def test_the_gateway_page_prints_every_field_the_projection_publishes(
gateway_client) -> None:
"""Every label the page builds is a key the payload really sends, resolved over HTTP.
This is the rename catcher the slot grid and the `/certs` table already have, and the gateway
page needs it more than either: a row is joined out of four structures (the trunk's port, the
endpoint's `host`/`port`, the config's `selectors`, the join's `connections`), so a rename in one
of them would otherwise draw a plausible line with the wrong target on it — worse than `n/a`,
because nothing about the picture would look broken.
The `warnings.*` paths are resolved twice, over a clean document and over one carrying a real
row from `sources/gateway.warnings()`: a path into an **empty** list is trivially present, so a
single pass over this fleet's clean configs could never catch `subject` becoming `name`.
"""
payload = gateway_client.get("/api/gateway/snapshot").json()
assert payload["supported"] is True and payload["host"] == "gateway"
for path in GATEWAY_PAGE_FIELDS:
found, _values = _payload_field(payload, path)
assert found, f"/gateway indexes {path}, which the payload does not publish"
broken = gateway_client.get("/api/gateway/snapshot").json()
broken["warnings"] = one_real_warning()
for path in GATEWAY_PAGE_FIELDS:
found, values = _payload_field(broken, path)
assert found, f"/gateway indexes {path}, which a warning row does not carry"
if path.startswith("warnings."):
assert values and all(value for value in values), (
f"{path} is empty on the row the source writes — the page would print nothing for it")
# The two families the panels are keyed on are the payload's own `source` words, and the panel
# table would silently lose a family if `sources/gateway.py` ever renamed one. Both sides of the
# map use the same words, so a drift between them is caught here too.
sources = {row["source"] for row in payload["listeners"]}
assert {row["source"] for row in payload["connections"]} <= sources, (
"a route is filed under a config family no trunk of this page carries")
assert sources == set(GATEWAY_CARD_KEYS), \
f"the payload's `source` words are {sorted(sources)} and the page ships {GATEWAY_CARD_KEYS}"
def test_every_pipe_is_an_accessible_image_built_from_the_payload(client) -> None:
"""Each connection line is `role="img"` + an `aria-label` naming its port, target and count.
The battery glyph's convention (`DashCharts.battery`), for the same reason: the line carries
information the markup does not say in words, and here it is worse — a line is a *line*, so
without a name a screen reader gets a panel that says `nginx` and nothing about the seventeen
connections on it. Built with `createElementNS` + `setAttribute`, never `innerHTML`, because the
domains, client addresses and `server_name`s in that sentence are upstream strings.
The trunk's facts (live count, client addresses, domain count) are text rather than an image,
so they are pinned as the renderer that assigns them: the same four payload fields have to reach
the page whichever component draws them.
"""
js = _code(client.get("/static/page-gateway.js").text)
assert "createElementNS(SVG_NS" in js, "the line is no longer hand-rolled SVG"
assert "http://www.w3.org/2000/svg" in js
pipe = js[js.index("function buildPipe("):js.index("function leafRow(")]
assert "createElementNS" in pipe and "'path'" in pipe
rendered = js[js.index("function renderLeaf("):js.index("function groupElement(")]
assert "svg.setAttribute('role', 'img')" in rendered
assert "aria-label" in rendered and "leafAria(" in rendered
aria = js[js.index("function leafAria("):js.index("function buildPipe(")]
for fact in ("conn.in", "endpointText(out)", "out.selectors", "conn.connections"):
assert fact in aria, f"the line's accessible name stopped saying {fact}"
assert "conn.shared === true" in aria, (
"a line sharing its endpoint with another route must say so out loud, or the two numbers "
"read as twice the traffic")
assert "connections not attributed" in aria, \
"a line whose join could not count its target must not be announced as a zero"
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
for fact in ("listener.connections", "listener.clients", "listener.domains"):
assert fact in trunk, f"the trunk stopped saying {fact}"
# Phase 31 task 03: the trunk now draws a line as well as printing words, so it is an image with
# a name of its own. **Two** pipe builders' outputs must carry one shape, because the moment one
# of the two lines loses its `role` or its sentence it is an unnamed stroke beside a named one.
assert js.count("createElementNS(SVG_NS, 'svg')") == 1, \
"a second place now constructs an `<svg>`, so the leaf's run and the trunk's stub can "\
"drift apart in geometry, class shape or accessibility"
assert "buildPipe(true)" in js and "li.appendChild(buildPipe())" in js, \
"one of the two lines is no longer built by the one builder"
stub = js[js.index("function buildPipe("):js.index("function routeInConfig(")]
assert "'pipe pipe-trunk' : 'pipe'" in stub, \
"the trunk's stub no longer shares `.pipe` with the leaf's run, so its states are its own"
assert "trunkAria(" in trunk, "the trunk's line has no accessible sentence of its own"
assert "svg.setAttribute('role', 'img')" in trunk, \
"the trunk's line is an unnamed shape: a screen reader gets the panel and none of its ports"
# Phase 31 task 05 measured this in a browser: `title` is a reflected IDL attribute of
# `HTMLElement` and **not** of `SVGElement`, so `svg.title = …` invents a JavaScript property no
# tooltip ever reads. The trunk's sentence has to go on as an attribute or the port's number is
# ink with no explanation on hover — which is what the served code had been doing.
assert "svg.setAttribute('title', aria)" in trunk, \
"the trunk's tooltip is no longer a real attribute: on an `<svg>` an assignment to `.title` " \
"is a property no hover reads"
assert "svg.title =" not in trunk, \
"`svg.title = …` looks like a tooltip and is not one on an SVGElement"
# upstream text reaches the page through textContent and title, and nothing else …
assert "innerHTML" not in rendered
assert "textContent" in js and "escapeHtml" not in js, \
"the page hand-escapes strings it only ever assigns through textContent"
# … and the markup the page does template with `innerHTML` is static: no payload string, ever
for template in re.findall(r"innerHTML = `([^`]*)`", js):
assert "${" not in template, f"a template interpolates upstream text: {template!r}"
def test_a_pipe_moves_only_while_it_carries_a_connection(client) -> None:
"""Motion is the connection; colour is the config. Two class pairs, and neither borrows the other.
Phase 30 task 05 split what had been one `live`/`still` claim into two, because the operator's
green means *the route is in the config* and a resting route is still in the config:
* `live` / `still` — "a socket is open on this pipe this instant" — decides **motion only**. The
`animation` lives on the live rule and nowhere else, the static rule declares none, and the
live rule declares no `stroke`: a busy line and a quiet one are the same colour, or motion has
become a verdict.
* `on` / `off` — "a proxy config declares this route" — decides **stroke only**
(`test_a_connection_is_green_because_it_is_in_the_config`).
Phase 31 task 03 added a **second** pipe — the trunk's short stub — and with it a second source of
truth, which is the part worth pinning. A leaf reads `connections[].connections` (the sockets at
that route's endpoint); a trunk reads `listeners[].connections`, which `/proc` states as the
ESTABLISHED sockets whose **local** end is that listen port. The trunk's number is therefore a
claim about a **port** and **never about a leaf**: `:4443` moving does not mean `nextcloud` has
traffic, and that is why the trunk's sentence names the port and no endpoint
(`test_the_trunk_pipe_is_an_accessible_image_of_its_port`). Both pipes use the same two class pairs
and neither may borrow the other's colour, motion or accessible name.
The keyframe's only property stays `stroke-dashoffset`: the dash travelling is the whole
animation, and animating a colour would put a number's judgement inside a line. And
`vector-effect="non-scaling-stroke"` stays in the script: the pipe is stretched to the card's
width, so without it a wide card would draw a thick pipe.
"""
section = _code(_css_section(_css(client), "gateway pipes"))
live = _declarations(section, ".pipe.live .pipe-path")
assert live.get("animation", "").startswith("pipe-flow"), \
"the animated state no longer runs the flow animation"
assert live.get("stroke") is None, (
"the live rule paints again, so a resting route no longer looks like the configured route "
"it is — colour belongs to `on`/`off` and motion belongs to `live`/`still`")
assert _declarations(section, ".pipe-path").get("animation") is None, \
"motion moved onto the base rule: every pipe animates, resting ones included"
still = _declarations(section, ".pipe.still .pipe-path")
assert still.get("animation") is None, "the static state animates — zero is not traffic"
assert still.get("stroke-dasharray"), "the static pipe lost its dotted rest state"
assert still.get("stroke") is None, \
"`still` colours itself again: a resting route must keep the config's green"
keyframes = re.search(r"@keyframes\s+pipe-flow\s*\{(.+?)\n\}", section, re.S)
assert keyframes, "the flow animation has no keyframes to run"
body = keyframes.group(1)
assert body.count("stroke-dashoffset") == 2 and "stroke:" not in body, \
"the keyframe must travel the dash and nothing else — animating colour is a verdict"
js = _code(client.get("/static/page-gateway.js").text)
assert "svg.classList.toggle('live', live)" in js
assert "svg.classList.toggle('still', !live)" in js
assert "conn.connections > 0" in js, \
"the live test is no longer the connection count itself"
assert 'vector-effect' in js
# …and the trunk's stub, whose `live` comes from a **different payload path** and may only ever
# come from that one. A leaf answers "is a socket open on this route?"; a trunk answers "is a
# socket arriving on this port?" — the port's number belongs to the trunk as a whole, so crediting
# it to one of the leaves under it would be a claim `listeners[]` cannot make.
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
assert "const count = listener.connections" in trunk, \
"the trunk no longer reads its own `listeners[].connections` field"
assert "const measured = typeof count === 'number' && isFinite(count)" in trunk, \
"the trunk no longer separates `0` (measured, idle) from `null` (not measured)"
assert "svg.classList.toggle('live', measured && count > 0)" in trunk, (
"the trunk's motion is no longer the port's own count — either it animates on an unknown "
"(a `null` read as traffic) or it stopped animating on a busy port at all")
assert "svg.classList.toggle('still', !measured || count === 0)" in trunk, \
"the trunk has no static state, so an unknown reads as motion"
for leaf_field in ("conn.connections", "endpointText", "out.selectors", "connections.shared"):
assert leaf_field not in trunk, (
f"the trunk reads {leaf_field} — a trunk's line is a fact about a **port** and may never "
"be derived from a leaf's endpoint, or one stub would move for one route out of sixteen")
assert "const live = typeof conn.connections === 'number'" in js, \
"the leaf's motion no longer keys on its own endpoint count"
def test_a_connection_is_green_because_it_is_in_the_config(client) -> None:
"""`--gate-on` is the whole colour story, and it answers *does the config say so*.
The operator's words were "green if the connection is active", with active defined in the phase
as *written in the nginx / haproxy config*. That is an **existence** state, so it gets its own
pair of tokens (`--gate-on` / `--gate-off`, declared once in `:root` as a substitution of the
verdict colours so both themes follow) and it never borrows `--ok` — a route existing is not
good news, and a colour the operator reads as a judgement on a number is exactly what
`severity.py` refuses to publish for a `gateway.*` metric.
Four things make the pair mean something rather than merely exist, and each is one of the ways
this could silently rot: the base `.pipe-path` rule declares **no** `stroke`, so colour can only
arrive through a state class; `.pipe.on` and `.pipe.off` are the two classes that carry it; the
script puts `on` on every connection it draws (a row exists because a config declared it, so
`off` is a styled seam for the write phase, not a second state in use); and the `off` half is
declared, which is the only reason the pair is a pair.
"""
css = _css(client)
code = _code(css)
light_at = code.index('[data-theme="light"]')
root = code[code.index(":root {"):light_at]
assert re.search(r"--gate-on:\s*var\(--ok\)", root), (
"`--gate-on` stopped being the one indirection over the theme colours — a literal here "
"would not follow `[data-theme=\"light\"]` and the page would need a second declaration")
assert re.search(r"--gate-off:\s*var\(--fail\)", root), "`--gate-off` is missing from `:root`"
assert code.count("--gate-on:") == 1 and code.count("--gate-off:") == 1, \
"the existence pair is declared more than once, which is how a pair drifts from its theme"
section = _code(_css_section(css, "gateway pipes"))
assert _declarations(section, ".pipe-path").get("stroke") is None, \
"the base rule paints, so a pipe has a colour the state classes did not grant it"
assert _declarations(section, ".pipe.on .pipe-path").get("stroke") == "var(--gate-on)", \
"the in-the-config state stopped taking its green from the existence pair"
assert _declarations(section, ".pipe.off .pipe-path").get("stroke") == "var(--gate-off)", \
"the out-of-the-config state has no colour, so the pair is half a vocabulary"
js = _code(client.get("/static/page-gateway.js").text)
assert "svg.classList.toggle('on', on)" in js and "svg.classList.toggle('off', !on)" in js, \
"the existence classes are not both driven from one value, so a row can contradict itself"
exists = js[js.index("function routeInConfig("):js.index("function buildSwitch(")]
assert "return true" in exists, (
"a drawn connection is no longer unconditionally in the config — if the payload grew a way "
"to say otherwise, this test and `GATEWAY_PAGE_FIELDS` both have to learn about it")
assert "const on = routeInConfig(conn)" in js
def test_the_whole_route_takes_one_existence_class(client) -> None:
"""One class on the `<li>` paints the spine, the tick, the run **and** the switch.
The operator's complaint was about ink they could see: *"Why are the dotted lines on the left
grey? Shouldn't those be green too? They're active."* Phase 30 applied the existence pair to
`.pipe-path` alone, so every route drew its last third in the config's green and the rest of its
length in structural grey — a picture that contradicted the page's own claim on all 33 rows.
Phase 31 task 01 moves the state onto the **row**: `renderLeaf` toggles `on`/`off` on the `<li>`
off the same value it colours the `<svg>` with, and four rules reach the two halves of the spine
and the tick out to the switch from that one class.
Two rules keep that from being a colour change with no contract behind it:
* The base connector rules declare `transparent`. An SVG path with no `stroke` is invisible for
free; a border's default colour is `currentColor`, so "a row built and never rendered draws
nothing" has to be **written down** for a border the way the missing `stroke` does for a path.
* The switch keeps taking its colour from `[aria-checked]` (phase 30's pin, asserted again here).
The row is now painted by two selectors — `.gate-link.on` and `[aria-checked]` — both driven
from one value, and a pin on only one of them is how the two halves of a row end up disagreeing.
"""
section = _code(_css_section(_css(client), "gateway pipes"))
rules = [(" ".join(sel.split()), body)
for sel, body in re.findall(r"([^{}]+)\{([^}]*)\}", section)]
def painted(selector: str) -> list[str]:
"""Bodies of the rules whose selector **list** names `selector` exactly."""
return [body for sel, body in rules
if selector in [part.strip() for part in sel.split(",")]]
for segment in (".gate-link.on::before", ".gate-link.on::after", ".gate-link.on .gate-elbow"):
bodies = painted(segment)
assert bodies, f"nothing paints {segment} from the row's `on` class — that segment of the " \
"route is still structural ink, which is the defect this task was written to close"
assert "var(--gate-on)" in " ".join(bodies), \
f"{segment} takes its colour from somewhere other than the existence pair"
for segment in (".gate-link.off::before", ".gate-link.off::after", ".gate-link.off .gate-elbow"):
bodies = painted(segment)
assert bodies, f"nothing paints {segment} from the row's `off` class, so the pair is half a " \
"vocabulary for the connectors and a route could never be drawn out of the config"
assert "var(--gate-off)" in " ".join(bodies), \
f"{segment} takes its colour from somewhere other than the existence pair"
# Geometry, no colour: a row the payload pass never reached must draw no dashed ink at all.
for selector, prop in ((".gate-link::before", "border-left"),
(".gate-link::after", "border-left"),
(".gate-elbow", "border-top")):
value = _declarations(section, selector).get(prop, "")
assert value.endswith("transparent"), (
f"{selector} declares {value!r} instead of a dashed border whose colour is "
"`transparent` — an uncoloured border is `currentColor`, so an unrendered row would draw "
"a line in whatever colour its text happens to be")
assert "--border-strong" not in section, (
"a gateway connector still reads the structure token: the row's green has to run from its "
"trunk to its endpoint, and one grey segment of it says the route is not in the config")
js = _code(client.get("/static/page-gateway.js").text)
leaf = js[js.index("function renderLeaf("):js.index("function groupElement(")]
assert "const on = routeInConfig(conn)" in leaf, \
"the row's state no longer comes from the one reading of the payload"
for target in ("li", "svg"):
assert f"{target}.classList.toggle('on', on)" in leaf, \
f"`on` is no longer written on the {target}, so part of the route colours itself "\
"without the payload saying the route exists"
assert f"{target}.classList.toggle('off', !on)" in leaf, \
f"`off` is not written on the {target} beside `on` — one row could show both states"
assert leaf.index("const on = routeInConfig(conn)") < leaf.index(
f"{target}.classList.toggle('on', on)"), (
f"the {target}'s state is set before the payload is read")
row = js[js.index("function leafRow("):js.index("function renderLeaf(")]
assert "li.className = 'gate-link'" in row
assert "classList" not in row, \
"a row now ships a state class before any payload reached it, so an unrendered leaf draws a "\
"confident route"
# …and the two halves of the row that already had the pair still take it the same way, so the
# connectors joining them cannot arrive on a third vocabulary.
assert _declarations(section, ".pipe.on .pipe-path").get("stroke") == "var(--gate-on)", \
"the run no longer colours from the pair the connectors do"
assert "var(--gate-on)" in _declarations(section, ".gate-switch").get("border", ""), \
"the switch's on-state border is no longer the existence pair"
assert "var(--gate-off)" in _declarations(
section, '.gate-switch[aria-checked="false"]').get("border-color", ""), \
"the switch's off state has no colour, so `[aria-checked]` is decoration again"
def rendered_trunk(row: dict[str, Any]) -> dict[str, Any]:
"""What one trunk prints, claims and animates, recomputed from the payload as a **second** reading.
The `rendered_leaf` idiom, for the same reason: there is no JS runner (a locked anchor), so the
sentence and the two classes are re-derived here from the literals the served-byte pins below
assert verbatim. `renderTrunk` prints `N live` / `n/a` in `.pipe-count` and `trunkAria` says the
port's sentence; a rename or a paraphrase on either side therefore fails a pin **and** moves what
this mirror prints. Nothing here reads a leaf: the trunk's whole claim is about its port.
"""
port = f":{row['port']}"
count = row["connections"]
measured = isinstance(count, (int, float)) and not isinstance(count, bool)
if not measured:
aria = f"{port} \u2014 connections on this port are unknown: the socket table did not answer"
else:
how_many = "no" if count == 0 else str(count)
verb = "connection is" if count == 1 else "connections are"
aria = f"{port} \u2014 {how_many} {verb} open on this port right now"
return {"count": "n/a" if not measured else f"{count} live",
"aria": aria,
"live": bool(measured and count > 0),
"still": not measured or count == 0,
# a trunk row exists because a config names the port, which is the existence fact
"on": True}
def test_the_trunk_pipe_is_an_accessible_image_of_its_port(client, gateway_client,
gateway_snapshot) -> None:
"""Every trunk draws a short line that moves on **its port's** count and says so in one sentence.
The operator's complaint (2026-10-23): *"The only animated line is `:443 -> 127.0.0.1:4443`."*
On the live ingress almost every leaf endpoint is a **name** the page cannot resolve, so its count
is a dash and its line is still — while the kernel states, for the port itself, how many
ESTABLISHED sockets have it as their local end. That figure is a real measurement, and it is the
only live number this page can print until name resolution lands, so the trunk draws it as a line.
The claim has to stay a **port** claim. `:4443` moving does not mean `nextcloud`'s route carries
those sockets, so the sentence names the port and no endpoint, in all three arms:
:4443 \u2014 9 connections are open on this port right now
:4443 \u2014 no connections are open on this port right now
:4443 \u2014 connections on this port are unknown: the socket table did not answer
and the unknown arm never prints `0` — neither in the sentence nor in `.pipe-count` (rule 2). The
three blocks below are the three ways that goes wrong: the pipe stops being a named image; the
sentence paraphrases "on this port" away or names an endpoint; or the unknown arm is treated as
zero and a silent router starts looking like an idle one.
"""
js = _code(client.get("/static/page-gateway.js").text)
# (1) the stub is built by the one pipe builder and lands inside the trunk, after the count.
group = js[js.index("function groupElement("):js.index("function trunkAria(")]
assert "wrap.querySelector('.gate-trunk').appendChild(buildPipe(true))" in group, (
"the trunk no longer draws a line, or draws it somewhere other than the trunk itself \u2014 "
"an `<svg>` cannot come from the `innerHTML` template, which is why it is appended")
assert group.index("pipe-count") < group.index("buildPipe(true)"), \
"the stub is no longer the count's neighbour at the trunk's right edge"
# (2) the sentence: three arms, every word pinned, and nothing but `listeners[].connections`.
aria = js[js.index("function trunkAria("):js.index("function renderTrunk(")]
assert "`${port} \u2014 connections on this port are unknown: the socket table did not answer`" in aria, \
"the unknown arm no longer says `unknown` \u2014 it must not read as an idle port (rule 2)"
assert "open on this port right now" in aria, \
"the measured arms stopped naming the port as the subject of the sentence"
assert "'no'" in aria and "'connection is'" in aria and "'connections are'" in aria, \
"the measured arms stopped saying 0 as `no` and 1 as `connection is`"
assert "(group.listener || {}).connections" in aria, \
"the sentence no longer reads the port's own figure"
for leaf_field in ("conn.", "out.", "endpointText", "selectors", "host", "clients"):
assert leaf_field not in aria, (
f"the trunk's sentence reaches for {leaf_field} \u2014 the sockets a port counts belong to "
"the trunk as a whole, and naming a route would credit them to one leaf")
# (3) the classes, and the `null` arm that must not move and must not print `0`.
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
assert "svg.hidden = group.listener == null" in trunk, \
"a group with no listener row now draws a stub for a port nobody read"
assert "svg.classList.toggle('on', group.listener != null)" in trunk, \
"the stub stopped taking its colour from the existence fact the trunk row itself is"
assert "svg.classList.toggle('off', false)" in trunk, \
"`off` is no longer cleared on the trunk, so a re-used group could keep a stale state"
# (4) the fixture render, both directions of the rule, over the captured ingress and over the
# socket table going quiet (`sockets_silent`, which is what the agent's unreadable
# /proc/net/tcp{,6} actually produces).
answered = gateway_client.get("/api/gateway/snapshot").json()
listeners = answered["listeners"]
assert listeners, "the capture names no port, so this pin would prove nothing"
endpoints = {f"{c['out']['host']}:{c['out']['port']}" for c in answered["connections"]}
busy = [row for row in listeners if row["connections"]]
idle = [row for row in listeners if row["connections"] == 0]
assert busy and idle, (
f"the capture reports {sorted(r['port'] for r in busy)} busy and "
f"{sorted(r['port'] for r in idle)} idle \u2014 both arms need a port in them to be pinned")
for row in listeners:
printed = rendered_trunk(row)
assert printed["live"] == bool(row["connections"]), \
f":{row['port']} animates on something other than its own non-zero count"
assert printed["live"] != printed["still"], \
f":{row['port']} is both moving and still \u2014 the two arms are one class each"
assert printed["aria"].startswith(f":{row['port']} \u2014"), \
"the sentence no longer leads with the port it is about"
for endpoint in endpoints:
assert endpoint not in printed["aria"], (
f"the trunk of :{row['port']} names the endpoint {endpoint} \u2014 a port's count is "
"not that route's traffic")
# both directions of "never `0` for an unknown"
assert (printed["count"] == "n/a") == (row["connections"] is None), \
f":{row['port']} prints {printed['count']!r} for a count of {row['connections']!r}"
# …and the other half of the same honesty: a port the table *did* answer with nothing on it
# says so. `0` is a measurement here, not an absence, and it is the reason the trunk prints
# `n/a` for the unknown instead of one word for both facts.
if row["connections"] is not None:
assert printed["count"] == f"{row['connections']} live", \
f":{row['port']} hid a count the socket table actually reported"
assert rendered_trunk(busy[0])["live"], "a port the kernel reports traffic on sits still"
assert not rendered_trunk(idle[0])["live"], "an idle port animates \u2014 motion became a guess"
silent = gateway_source.parse(gateway_snapshot("sockets_silent"), "gateway")
quiet = silent["listeners"]
assert quiet and all(row["connections"] is None for row in quiet), \
("`sockets_silent` no longer yields null listener counts, so this no longer exercises the "
"arm the page has to render as `n/a`")
for row in quiet:
printed = rendered_trunk(row)
assert printed["on"] and not printed["live"] and printed["still"], (
f":{row['port']} is not green-and-motionless with the socket table quiet \u2014 the "
"configs are still drawn and nothing is claimed about traffic")
assert printed["count"] == "n/a", \
f":{row['port']} prints {printed['count']!r} instead of `n/a` when nobody measured"
assert printed["count"] != "0 live", "an unanswered socket table became a claimed zero"
assert "unknown" in printed["aria"] and "did not answer" in printed["aria"], \
f":{row['port']}'s sentence hides that the reading is missing"
assert not any(rendered_trunk(row)["count"] == "0 live" for row in quiet), \
"a page with no socket table printed `0 live` somewhere"
def _leaf_haystack(row: dict[str, Any]) -> str:
"""The one string the filter can see about one route, as `page-gateway.js` composes it.
Phase 32 task 04 adds the **sixth** field: `out.resolved`, the addresses the gateway published for
a name target. Typing an IP is how an operator asks "which route owns the socket I am looking at",
and a haystack of names alone cannot answer it.
"""
out = row.get("out") or {}
port = (row.get("in") or {}).get("port")
parts = ["" if port is None else str(port), out.get("host"), out.get("name"), out.get("kind"),
*(out.get("selectors") or []), *(out.get("resolved") or [])]
return " ".join(p for p in parts if isinstance(p, str) and p).lower()
def _trunk_haystack(port: int, listener: dict[str, Any]) -> str:
"""The same for a trunk: the port as the page prints it, then its domains and clients."""
parts = [f":{port}", *(listener.get("domains") or []), *(listener.get("clients") or [])]
return " ".join(p for p in parts if isinstance(p, str) and p).lower()
def filter_view(snapshot: dict[str, Any], query: str) -> dict[str, Any]:
"""What the page leaves standing for one query, derived as a **second** reading.
There is no JS runner (a locked anchor), so this is `rendered_leaf`'s idiom: the rules are
re-applied here from the payload, and the served-byte pins in the test below are what keep the
mirror honest — `haystack` indexes `in.port`, `out.host`, `out.name`, `out.kind`,
`out.selectors`, `out.resolved`;
`trunkHaystack` indexes `:${port}`, `domains`, `clients`; a trunk that matches keeps **all** its
leaves; a group stands when it or one of its routes answers. A rename on either side therefore
fails a pin *and* changes what this mirror prints.
Returns the panels' worth of groups plus the two numbers the count chip prints, both over leaf
routes: `standing` and `total`.
"""
needle = query.strip().lower()
groups: list[dict[str, Any]] = []
standing = 0
for key in ("nginx", "haproxy"): # the order `CARDS` declares
by_port: dict[int, dict[str, Any]] = {}
for row in snapshot["listeners"]:
if row["source"] != key:
continue
by_port.setdefault(row["port"], {"panel": key, "port": row["port"],
"listener": None,
"connections": []})["listener"] = row
for row in snapshot["connections"]:
if row["source"] != key:
continue
by_port.setdefault(row["in"]["port"],
{"panel": key, "port": row["in"]["port"], "listener": None,
"connections": []})["connections"].append(row)
for port in sorted(by_port):
group = by_port[port]
trunk_hit = needle in _trunk_haystack(port, group["listener"] or {})
leaves = []
for row in group["connections"]:
shown = trunk_hit or needle in _leaf_haystack(row)
leaves.append({"id": row["id"], "hidden": not shown})
if shown:
standing += 1
groups.append({"panel": key, "port": port, "trunk_hit": trunk_hit,
"hidden": bool(needle) and not trunk_hit
and not any(not leaf["hidden"] for leaf in leaves),
"leaves": leaves})
return {"groups": groups, "standing": standing, "total": len(snapshot["connections"])}
def test_the_filter_narrows_the_map_without_asking_anything(client, gateway_client,
gateway_snapshot: Any) -> None:
"""One field, four rules, and not one extra request.
The operator's complaint is a reading-speed one: *"I want to find a domain."* This ingress routes
33 of them off two config files, and a page that draws all 33 every 30 s makes that a scan. The
field is an **eye, not a query**, and the four halves of that sentence are the four ways the
feature could go wrong:
* **It matches payload text and nothing else.** `in.port`, `out.host`, `out.name`, `out.kind`,
`out.selectors` and `out.resolved` for a route, `:${port}` + `domains` + `clients` for a trunk.
The sixth field is phase 32 task 04's: an address the **gateway** published is text the payload
already carries, so typing the far end of a socket finds the route that owns it. No DNS (the
page still resolves no name — it matches a string, it never looks one up), no re-ranking, no
rule about `.reeselink.com` — a query can never invent a route, because there is no other text
to match.
* **It costs nothing.** The repaint runs on `state.snapshot`; a keystroke that re-fetched would be
a poll per character and a lie about what changed.
* **It hides rows, it does not remove them.** `li.hidden` in the patch pass, so a route the query
hides keeps its identity, its state classes and its place for the next poll.
* **It moves nothing else.** The four header tallies are the payload's figures, the warnings panel
is a different claim, and the charts follow the panel: beside a filtered map the header still
says what the whole ingress declares and carries.
"""
js = _code(client.get("/static/page-gateway.js").text)
# (1) the matcher's whole vocabulary: six payload fields for a route, three for a trunk.
hay = js[js.index("function haystack("):js.index("function trunkHaystack(")]
assert "String((conn.in || {}).port ?? '')" in hay, \
"the inbound port is no longer searchable — `4443` is the second thing an operator types"
for field in ("out.host", "out.name", "out.kind", "out.selectors", "out.resolved"):
assert field in hay, f"the filter no longer sees {field}"
# Phase 32 task 04: an address the **gateway** resolved is payload text like any other, so typing
# the far end of a socket finds the route that owns it. Names alone could not answer that question.
assert "...(out.resolved || [])" in hay, \
"a name-written route stopped being findable by the address it resolves to"
assert "toLowerCase()" in hay, "the match became case-sensitive on a page of lowercase domains"
for absent in ("conn.connections", "severity", "metric", "getaddrinfo", "split(",
"endsWith", "startsWith", "slice("):
assert absent not in hay, (
f"the matcher reached for {absent} — it may read what the payload calls a route, never "
"what it reports about it, and never resolve a name")
# …and the three things that would stop it being an eye over the document in hand: a dial, a read
# of what is painted rather than of what the payload says, and a literal `null` a query could
# match against a row that published nothing.
for not_here in ("fetchJSON", "textContent", "innerText", '"null"', "'null'"):
assert not_here not in hay, f"the haystack reached for {not_here}"
trunk = js[js.index("function trunkHaystack("):js.index("function filtering(")]
for field in ("group.port", "listener.domains", "listener.clients"):
assert field in trunk, f"the trunk no longer answers for {field}"
# (2) the repaint path: the payload in hand, no network, and no re-entry into the poll.
panels = js[js.index("function renderPanels("):js.index("function pruneCards(")]
assert "fetchJSON" not in panels and "refresh(" not in panels, (
"the filter path dials something — it is an eye over `state.snapshot`, and a keystroke must "
"cost no request (nor may it re-enter `refresh()`, which fetches a history pair per panel)")
wiring = js[js.index("function initFilter("):js.index("function pruneCards(")]
assert "state.filter = event.target.value.trim()" in wiring, \
"the filter stopped taking its needle verbatim-and-trimmed from the field"
assert "event.key !== 'Escape'" in wiring, \
"Escape no longer clears the field, so getting out of a filter means backspacing it"
assert "fetchJSON" not in wiring and "refresh(" not in wiring
count = js[js.index("function renderFilterCount("):js.index("function initFilter(")]
assert "`${standing} of ${total} routes`" in count, \
"the chip stopped counting leaf routes the way the panels draw them"
assert "chip.hidden = true" in count, \
"the count stays on screen beside an empty field — ink about nothing"
# (3) the tallies and the warnings list never move, because neither is a route.
for name, end in (("renderHeader", "async function refresh("),
("renderWarnings", "function dotFor(")):
body = js[js.index(f"function {name}("):js.index(end)]
for leak in ("state.filter", "filtering(", "haystack(", "leafShown(", "trunkShown("):
assert leak not in body, (
f"{name} now reads {leak} — the header's four figures and the warnings count are the "
"payload's own and must keep saying what the whole ingress declares")
# (4) rows are hidden, not removed, inside the patch pass that already holds them.
leaves = js[js.index("function renderLeaves("):js.index("function renderGroups(")]
assert "li.hidden = shown !== null && !shown[index]" in leaves, (
"a leaf is no longer hidden by index into the group's own connection list — the one form "
"that cannot credit a count to the wrong row")
refusal = leaves[leaves.index("const emptyId"):]
assert ".hidden" not in refusal, (
"the trunk's refusal sentence is being hidden by a route's rules — it belongs to the trunk, "
"so this fleet's `:80` is found by typing `80` and by nothing a route says")
assert ".remove()" not in leaves, \
"the filter deleted a row; removal belongs to a route leaving the config"
groups = js[js.index("function renderGroups("):js.index("function familyWord(")]
assert "el.hidden = !visible" in groups, "a trunk is no longer hidden as a whole"
assert "const shown = trunk ? null : group.connections.map(leafShown)" in groups, (
"a trunk that answered the query no longer keeps its whole fan-out — typing `4443` asks for "
"the port, and every route on it is part of the answer")
assert "const visible = !filtering() || trunk || shown.some(Boolean)" in groups, (
"a group is no longer standing on \"the trunk or one of its routes\", which is the one rule "
"that decides whether a port disappears from the map")
assert "none.hidden = !stranded" in groups and \
"const stranded = filtering() && groups.length > 0 && standing === 0" in groups, (
"the panel's no-match sentence is no longer keyed on all three of: the filter is up, there "
"were groups, and the query hid every one of them")
assert '[data-k="no-match"]' in groups and 'no route matches' in groups, \
"a panel whose every group is hidden now says nothing at all"
# (5) the capture, through the mirror: the four answers the field has to give.
payload = gateway_client.get("/api/gateway/snapshot").json()
total = len(payload["connections"])
assert total > 10, "the capture routes too little for a filter to prove anything"
everything = filter_view(payload, "")
assert everything["standing"] == total and not any(
leaf["hidden"] for group in everything["groups"] for leaf in group["leaves"]), \
"an empty query hides something — it must be the same page as no query at all"
assert not any(group["hidden"] for group in everything["groups"]), \
"an empty query hid a trunk"
brain = filter_view(payload, "brain")
shown = [leaf["id"] for group in brain["groups"] for leaf in group["leaves"]
if not leaf["hidden"]]
assert brain["standing"] == len(shown) > 1, \
"`brain` matched nothing but the brain routes"
assert all("brain" in _leaf_haystack(next(c for c in payload["connections"]
if c["id"] == leaf_id))
for leaf_id in shown), "`brain` showed a route it does not appear in"
assert all(c["id"] in shown for c in payload["connections"]
if "brain" in _leaf_haystack(c)), "`brain` lost a route it appears in"
assert any(group["port"] == 443 and not group["hidden"] for group in brain["groups"]), \
"the trunk the brain routes hang off was hidden with the rest of its fan-out"
assert brain["standing"] < total, "nothing was filtered out at all"
port4443 = filter_view(payload, "4443")
for group in port4443["groups"]:
if group["port"] == 4443:
assert not group["hidden"] and all(
not leaf["hidden"] for leaf in group["leaves"]), \
":4443 did not keep its whole fan-out — typing a port asks for the port"
else:
assert group["hidden"], f":{group['port']} survived a query for :4443"
nothing = filter_view(payload, "no-such-domain-anywhere")
assert nothing["standing"] == 0 and all(group["hidden"] for group in nothing["groups"]), \
"a query that matches nothing left a row or a trunk on the page"
# (6) a trunk answers for **itself**: a client address is the listener's own payload, and typing
# it is a question about the port, so every route under it stays as part of the answer.
client_row = next((row for row in payload["listeners"] if row["clients"]), None)
assert client_row, "the captured socket table names no client, so this arm cannot be exercised"
by_port = filter_view(payload, client_row["clients"][0])
home = next(group for group in by_port["groups"] if group["port"] == client_row["port"])
assert home["trunk_hit"] and not home["hidden"], \
f"typing {client_row['clients'][0]!r} did not match :{client_row['port']}'s own row"
assert all(not leaf["hidden"] for leaf in home["leaves"]), \
"a trunk that matched lost leaves of its fan-out"
# (7) phase 32 task 04: an address the **gateway** resolved is searchable too, which is the field
# an operator reaches for when they are holding a socket rather than a domain. Over the 1.2.0
# document there is nothing to match (no resolver section, and the page resolves nothing), so
# this leg runs on the recorded 1.3.0 one.
placed = gateway_source.parse(gateway_snapshot("resolved"), "gateway")
assert not filter_view(payload, "10.1.2.64")["standing"], \
"the page found an address it was never given — it resolves nothing, and must keep not doing it"
brain_ip = filter_view(placed, "10.1.2.64")
found = [leaf["id"] for group in brain_ip["groups"] for leaf in group["leaves"]
if not leaf["hidden"]]
assert found and all("reeseapps-brain-of-reese" in route_id for route_id in found), \
f"typing the brain's address left {found}"
assert all(group["hidden"] or group["port"] == 443 for group in brain_ip["groups"]), \
"an address match let a trunk of another port stand"
shared_ip = filter_view(placed, "10.1.1.78")
expected = sum(1 for row in placed["connections"] if "10.1.1.78" in (row["out"]["resolved"] or []))
assert expected > 1 and shared_ip["standing"] == expected, (
f"one address reached by {expected} routes was found by {shared_ip['standing']} of them")
def test_the_filter_field_is_markup_the_sheet_and_nobody_else_uses(client) -> None:
"""The field is pinned markup, on one page, wearing the sheet's one field skin.
Three absences carry as much as the two presences. **No other page ships `type="search"`**: the
shared rule is what makes a field match the theme, and a page growing its own styled box is how a
control stops following a theme change. The base rule is `max-width: 18rem; min-width: 0` and the
≤640 px block makes it `width: 100%` — a 33-name ingress is exactly the case for typing into the
field on a phone, and a field that keeps 18 rem of cap there is a field with two characters of
visible text. And `.gate-filter` carries no `:hover`, no `cursor` and no `transition`: the
connection map owns one interactive-looking thing (the unwired switch's knob) and this is not it.
"""
css = _css(client)
shared = _declarations(css, 'input[type="text"], input[type="password"], input[type="search"]')
for prop in ("background", "color", "border", "border-radius", "padding", "font"):
assert shared.get(prop), f"the shared field rule no longer declares {prop}"
assert shared.get("border") == "1px solid var(--border)", \
"the field skin drifted from the sheet's own border token"
section = _code(_css_section(css, "gateway pipes"))
base = _declarations(section, ".gate-filter")
assert base.get("max-width") == "18rem", \
f"the field is capped at {base.get('max-width')!r} rather than 18rem"
assert base.get("min-width") == "0", \
"without `min-width: 0` the field cannot shrink, so a long placeholder grows the page head"
# The phone override lives in the gateway section's own ≤640 px block and says only that one thing.
mobile = section[section.index("@media (max-width: 640px)"):]
reach = re.search(r"\.gate-filter\s*\{([^}]*)\}", mobile)
assert reach, "the field is not full-width at the phone break — 33 domains want a real field"
wide = _declarations(mobile, ".gate-filter")
assert wide.get("width") == "100%", f"the phone rule reads {wide} rather than `width: 100%`"
assert wide.get("max-width") == "none", (
f"{wide} lifts the width but not the 18rem cap, so the field is still 288 px on a 320 px page")
for selector, body in re.findall(r"([^{}]*\.gate-filter[^{}]*)\{([^}]*)\}", _code(css)):
assert ":hover" not in selector, f"the filter grew a hover affordance: {selector.strip()}"
assert "cursor" not in body and "transition" not in body, (
f"the field advertises itself as a pointer control: {body.strip()}")
pages = {path: client.get(path).text for path in (*MARKUP_PAGES, "/login")}
html = pages[GATEWAY_PAGE]
head = html[html.index("Connection map"):]
field = next(tag for tag in re.findall(r"<input[^>]*>", head) if 'id="gate-filter"' in tag)
chip = next(tag for tag in re.findall(r"<span[^>]*>", head) if 'id="gate-filter-count"' in tag)
for attr in ('type="search"', 'class="gate-filter"', 'autocomplete="off"', 'spellcheck="false"'):
assert attr in field, f"the filter field lost {attr}"
assert 'aria-label="Filter routes by domain, endpoint or port"' in field, \
"the field has no accessible name — a placeholder is a hint, not a label"
assert "placeholder=" in field, "the field no longer says what it matches on"
assert 'class="chip faint"' in chip and "hidden" in chip \
and 'aria-live="polite"' in chip, f"the count chip is {chip}"
for other, page in pages.items():
if other != GATEWAY_PAGE:
assert 'type="search"' not in page, f"{other} grew a second search field"
# one no-match sentence per panel, and it ships hidden like every conditional sentence here
for key in ("nginx", "haproxy"):
card = html[html.index(f'data-gate="{key}"'):]
card = card[:card.index("</article>")]
assert card.count('data-k="no-match"') == 1, \
f"the {key} panel has no single no-match sentence"
assert 'data-k="no-match" hidden' in card, f"the {key} no-match sentence ships visible"
assert 'data-k="empty"' in card, \
f"the {key} panel lost the sentence for a config that names no port — the filter's " \
"sentence is a different fact"
# the footer says what the field does and does not touch, once
foot = " ".join(html[html.index("<footer class="):].split())
assert "hides the routes whose own labels do not contain what you typed" in foot, \
"the page stopped saying out loud that the filter asks for nothing"
assert "the tallies, the warnings and the charts above it are still the whole ingress" in foot, \
"the sentence no longer names the three things a filter must not quietly narrow"
for junk in ("no page narrates", "every value is scraped", "filters live, nothing is stored"):
assert junk not in foot, f"the footer grew flavour text: {junk}"
def test_haproxy_draws_straight_across_and_nginx_branches(client) -> None:
"""The geometry the operator asked for, in four rules and zero JavaScript.
"The dotted line connections need to connect to their counterparts. This is easy for haproxy —
just draw the line straight across. For nginx you'll need to branch the lines from `:443` to the
services they connect to." The answer is a spine plus a tick per leaf, and the spine is built out
of the rows themselves: each `.gate-link` carries its own upper half (`::before`) and lower half
(`::after`) at the gutter's left edge, and the first and last rows drop the half that would stick
out of the fan-out. That is why the spine starts at the first branch and stops at the last with
no mask, no measured height and no background-coloured eraser — and why it is still right on the
poll that adds a leaf, which any `ResizeObserver` version would have to be re-run to discover.
`.gate-group.solo`, set by the script from the group's own leaf count, takes the two halves away:
one route off a port is not a branch, and haproxy's six one-`server` frontends must read as one
straight line. `column-gap: 0` on the group is what makes the trunk's right edge and the spine
the same x, so the tick starts *at* the trunk rather than a gap short of it.
"""
section = _code(_css_section(_css(client), "gateway pipes"))
before = _declarations(section, ".gate-link::before")
after = _declarations(section, ".gate-link::after")
for name, rule in (("::before", before), ("::after", after)):
# `transparent`, not a colour: this rule is the branch's *geometry*, and the ink arrives from
# the row's state class (`test_the_whole_route_takes_one_existence_class`). A colour here would
# be a row drawing a confident line before any payload said the route exists.
assert rule.get("border-left") == "2px dashed transparent", (
f"the spine's {name} half no longer declares a dashed, colourless border — geometry "
"belongs on the row and colour belongs to `.gate-link.on`/`.off`")
assert rule.get("position") == "absolute" and rule.get("left") == "0", \
f"the spine's {name} half no longer sits in the row's gutter"
assert before.get("top") == "calc(var(--gate-leaf-gap) / -2)", \
"the upper half no longer starts half a leaf gap above its row"
assert after.get("top") == "50%", "the lower half no longer starts at the row's centre"
half = "calc(50% + var(--gate-leaf-gap) / 2)"
assert before.get("height") == half and after.get("height") == half, (
"a half no longer reaches half a gap past its own row, so consecutive rows stop meeting and "
"the spine becomes a stack of brackets; and if it reached further it would run past the ends "
"of the list, which the rule below is what prevents")
assert _declarations(section, ".gate-leaves").get("gap") == "var(--gate-leaf-gap)", \
"the leaf list's gap is a literal again, so the spine's reach and the gap can drift apart"
assert re.search(r"--gate-leaf-gap:\s*\.3rem", _code(_css(client))), \
"`--gate-leaf-gap` is no longer declared in `:root`"
assert _declarations(section, ".gate-link:first-child::before, .gate-link:last-child::after").get(
"display") == "none", "the fan-out grew a spine past its first and last branch"
elbow = _declarations(section, ".gate-elbow").get("border-top", "")
assert "2px dashed" in elbow and elbow.endswith("transparent"), (
"the tick from the spine to the switch is gone, or coloured itself, so a leaf no longer "
"reaches its trunk in the same ink as its run")
solo = _declarations(
section, ".gate-group.solo .gate-link::before, .gate-group.solo .gate-link::after")
assert solo.get("display") == "none", (
"a one-leaf group draws the vertical again — haproxy's six frontends would each grow a "
"branch stub where the line is supposed to run straight across")
assert _declarations(section, ".gate-group").get("column-gap") == "0", \
"the trunk and the spine are no longer the same x, so the tick starts in mid-air"
columns = _declarations(section, ".gate-link").get("grid-template-columns", "")
assert columns.startswith("26px "), \
f"the row's gutter is not a fixed 26px any more ({columns}), so the spine drifts"
# The phone stacks the trunk *above* its leaves, so the first row's upper half comes back and
# reaches into the group's row gap. Pinned inside the media query, because outside it this rule
# would be the desktop defect the paragraph above forbids.
mobile = section[section.index("@media (max-width: 640px)"):]
reach = re.search(r"\.gate-link:first-child::before\s*\{([^}]*)\}", mobile)
assert reach, "the stacked phone layout stopped reconnecting the trunk to its spine"
rules = _declarations(mobile, ".gate-link:first-child::before")
assert rules.get("display") == "block", "the reaching half is still switched off on a phone"
assert rules.get("top") == "calc(var(--gate-row-gap) * -1)", (
f"the reach is {rules.get('top')!r} rather than the group's own row gap negated — a literal "
"would leave the spine short of the trunk the day the rhythm moved")
assert "var(--gate-row-gap)" in rules.get("height", ""), \
"the reaching half grew by a different number than it moved"
js = _code(client.get("/static/page-gateway.js").text)
assert "el.classList.toggle('solo', group.connections.length === 1)" in js, (
"`.solo` is no longer the group's own leaf count, so the straight-across rule is keyed on "
"something other than the thing it means")
assert "function gateLink" not in js, \
"a JS-measured connector came back; the branch is CSS and stays right without measuring"
def test_the_pipe_animation_stills_in_both_states_under_reduced_motion(client) -> None:
"""The one reduced-motion block names `.pipe.live`, `.pipe.still` **and** `.gate-switch-thumb`.
The menu's pair above (phase 20 task 03) is the precedent and the reason: the animated rule
out-specifies the base one, so stilling only the static state would leave every busy pipe
flowing for someone who asked for no motion. Same single block, because two blocks drift.
Phase 30 task 05 added the third name, and it is not decoration: the switch's knob slides on a
`transition` of its own, and a block that stills the pipes while the thumb keeps sliding is the
phase-18 lesson half-applied — the operator asked for no motion, not for less of it.
Phase 31 task 03 added a **second pipe** (the trunk's stub) and deliberately added no fourth name.
That works only because of how the two names are written, so it is pinned rather than assumed: the
selectors are **descendant** (`.pipe.live .pipe-path`), and the stub is built by the same
`buildPipe(trunk)` with the same `pipe pipe-trunk` class list and the same `.pipe-path` child. A
rule that reaches `.pipe.live .pipe-path` therefore reaches a trunk's path too. The failure this
prevents is the quiet one: someone gives `.pipe-trunk .pipe-path` its own `animation` to make the
stub "nicer", and the block below stops covering it.
"""
code = _code(_css(client))
block = re.search(r"@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{(.*?)\n\}",
code, re.S)
assert block, "the stylesheet lost its reduced-motion block"
selector = re.search(r"(\.pipe\.live[^{]*)\{", block.group(1))
assert selector, "the reduced-motion block stopped naming the pipes at all"
named = {" ".join(part.split()) for part in selector.group(1).split(",")}
assert named == {".pipe.live .pipe-path", ".pipe.still .pipe-path"}, (
"the block must name both pipe states — naming only the static one leaves a live pipe "
f"animating for someone who asked for no motion; got {sorted(named)}")
assert _declarations(block.group(1), ".pipe.still .pipe-path").get("animation") == "none", \
"the named rules stopped stilling the animation"
assert _declarations(block.group(1), ".gate-switch-thumb").get("transition") == "none", \
"the switch's knob still slides when the operator asked for no motion"
# The two names are descendant selectors, which is the whole reason a second pipe needs no third:
# `.pipe.live` is on the `<svg>` (leaf or trunk) and `.pipe-path` is its child in both cases.
assert all(name.endswith(" .pipe-path") for name in named), (
f"the stilling rules are {sorted(named)} — a rule aimed at `.pipe.live` alone would not "
"reach the `<path>` that carries the `animation`, and would not reach a trunk's either")
# …and every animated thing on the page is in this block, counted rather than trusted: a third
# `transition` added to the map later has to be named here, not assumed to be covered.
animated = {".pipe.live .pipe-path", ".pipe.still .pipe-path", ".gate-switch-thumb"}
section = _code(_css_section(_css(client), "gateway pipes"))
transitioned = {" ".join(sel.split()) for sel, body in
re.findall(r"([^{}]+)\{([^}]*)\}", section)
if "transition:" in body or "animation:" in body}
assert transitioned <= animated, (
f"{sorted(transitioned - animated)} animates in the gateway map without being stilled")
# The trunk's stub must earn that coverage rather than assume it: it exists because `buildPipe`
# makes it (one class shape for both lines) and it owns no motion declaration of its own.
js = _code(client.get("/static/page-gateway.js").text)
assert "function buildPipe(trunk = false)" in js and "'pipe pipe-trunk'" in js, \
"`.pipe-trunk` is no longer produced by the one pipe builder, so it is a second component "\
"with a class shape this block does not reach"
stub_rules = [body.strip() for sel, body in re.findall(r"([^{}]+)\{([^}]*)\}", section)
if ".pipe-trunk" in sel]
assert stub_rules, "the stylesheet lost the trunk stub's rule"
for body in stub_rules:
assert "animation" not in body and "transition" not in body, (
f"the trunk stub styles motion itself ({body!r}), which the stilling block above does "
"not name — motion belongs on `.pipe.live .pipe-path` and nowhere else")
def test_no_gateway_count_is_coloured_by_a_verdict(client) -> None:
"""The pipes and switches colour from **existence**, and the verdict vocabulary stays out.
Phase 30 task 05 gave `/gateway` its green, and the whole point of that green is that it is not
the severity language: `--gate-on` means *this route is written in a proxy config*, and
`--gate-off` means the opposite. It is an existence state, so it lives in its own pair of tokens
(`test_a_connection_is_green_because_it_is_in_the_config` pins the pair, where it is declared,
and that the base `.pipe-path` carries no colour of its own) and nowhere near `--ok`.
So the absence this test exists for is unchanged, and it is now the thing that keeps task 05's
green honest: a busy ingress is the job on this fleet, `severity.py` has no `gateway.*` row, and
`/api/gateway/snapshot` publishes no `severity` at any depth (task 03 pins that over HTTP), so no
line, count, tally or switch may be graded. The whole vocabulary is pinned absent — the tokens,
the class names, and the two JS helpers that would have applied a verdict — across the stylesheet
section, the script and the markup, comments stripped because prose is allowed to *name* a colour
the code must not paint.
Two things read `ok|warn|fail` and neither is a verdict on a number. The reachability **dot** is
the job's health, the same dot every other page's card header draws from the same `states` row.
And the **warnings list** borrows the overview's `li.warn` row shape verbatim, because "a route
that does not connect to itself" is the same kind of sentence `/` prints and it is `alerts.py`'s
verdict, not a number's — so the pin is that exactly one `className` assignment in the whole
script says `warn`, and it is inside `renderWarnings`, while every row, line, switch and tally
renderer stays free of it.
"""
section = _code(_css_section(_css(client), "gateway pipes"))
for token in ("--ok", "--warn", "--fail", ".ok", ".warn", ".fail", "severity"):
assert token not in section, f"the gateway stylesheet section paints with {token}"
# Phase 31 task 01 extended that single vocabulary to the **whole** row, and the two halves of
# this assertion are the same claim read two ways: every connector takes the existence pair, and
# nothing in this section any longer has a reason to name the structure token. `--border-strong`
# was where the spine and the tick lived while only the run went green, so its absence here *is*
# the statement "a route is green from its trunk to its endpoint, or grey for all of it".
assert "--border-strong" not in section, (
"a gateway connector reads structure ink again — the spine halves, the tick, the run and the "
"switch are one existence claim and must take `--gate-on`/`--gate-off` together")
for connector in (".gate-link.on::before", ".gate-link.on::after", ".gate-link.on .gate-elbow",
".gate-link.off::before", ".gate-link.off::after",
".gate-link.off .gate-elbow"):
assert connector in section, f"the row's state class no longer paints {connector}"
js = _code(client.get("/static/page-gateway.js").text)
for helper in ("severityFrom", "severityFor", ".severity"):
assert helper not in js, f"/gateway grades a number with {helper}"
assert "setDot(" in js, "and the reachability dot is still drawn (that one is health)"
assigned = re.findall(r"className = '([^']*)'", js)
assert assigned.count("warn") == 1, (
f"`warn` is painted in {assigned.count('warn')} places; only the warnings list may say it")
warn_body = js[js.index("function renderWarnings("):js.index("function dotFor(")]
assert "li.className = 'warn'" in warn_body, (
"the one `warn` in the script is no longer the warnings row — re-check what is coloured")
for cls in assigned:
assert cls == "warn" or not any(v in cls for v in ("ok", "fail")), (
f"a verdict class ({cls}) entered the map itself")
html = _code(client.get(GATEWAY_PAGE).text)
for token in ('class="ok', 'class="warn', 'class="fail', "severity"):
assert token not in html, f"/gateway markup carries {token}"
def test_gateway_charts_are_collapsed_lazy_and_cost_one_history_request_per_card(
client) -> None:
"""`/system`'s chart discipline, per card: shut, built on open, one request per refresh.
Three separate things, pinned because `/system` and `/disks` each lost one of them in review.
The disclosure ships without `open`; `drawCardCharts` returns while it is shut, so no
`LineChart` exists and no SVG is painted for a plot nobody is looking at (`LineChart` measures
its container, and a hidden one is 0 px wide); and `renderCardCharts` fires once per visible
card per refresh naming every gauge that card's pipes publish — the endpoint answers an
unknown name as an empty series on purpose, so asking for the whole card costs nothing.
"""
html = client.get(GATEWAY_PAGE).text
assert '<details class="charts-box"' in html and '<details open' not in html
js = _code(client.get("/static/page-gateway.js").text)
assert 'addEventListener("toggle"' in js or "addEventListener('toggle'" in js
draw = js[js.index("function drawCardCharts("):js.index("async function renderCardCharts(")]
assert draw.index("if (!chartsOpen(key)") < draw.index("cardChart(key)"), \
"the paint pass builds charts before it asks whether the stack is open"
fetch = js[js.index("async function renderCardCharts("):js.index("function sumLive(")]
assert "/api/gateway/history" in fetch and "&range=" in fetch
assert fetch.count("fetchJSON") == 1, "a card now costs more than one history request"
assert "snapshot.host" in fetch, \
"the request must name the ingress and no other host — that is what the gauges are stored under"
refresh = js[js.index("async function refresh("):js.index("function pruneCards(")]
assert "renderCardCharts(key, snapshot)" in refresh and "visible.map" in refresh, \
"the history pass is no longer one request per visible card per refresh"
assert js.count("ariaLabel:") == 1, "the per-card plot lost or duplicated its accessible name"
assert "chart.series.filter((s) => s.points && s.points.length)" in js, \
"the legend is naming pipes the plot never drew — a permanent `n/a` row per map target"
def test_the_page_reports_no_card_and_no_cell_for_sockets_the_configs_do_not_name(
client) -> None:
"""The deletion, pinned as an absence so it cannot come back quietly.
Phase 29 shipped an `unmatched` card (three cells: ports no config names, connections inbound,
connections outbound) and a `by port` chip, both of which existed to report the residual of a
join that could only ever credit a **port**. On the live router that residual was mostly this
app's own agent (`:9120`) and SSH — noise dressed as a signal, and a card of it teaches the
operator to ignore the panel. So this pins that neither the markup, nor the script, nor the
stylesheet carries the vocabulary any more, in the page's own words and in the payload's.
What replaced it stays pinned too: the header's live tally still refuses to total a side that
did not measure (`0` there would be a claim about traffic nobody counted), and still dims when a
port that should have had a number did not.
"""
html = _code(client.get(GATEWAY_PAGE).text)
js = _code(client.get("/static/page-gateway.js").text)
css = _code(_css(client))
for name, text in (("markup", html), ("script", js), ("stylesheet", css)):
for token in ("unmatched", "um-other", "um-in", "um-out", "gate-groups", "by_port",
"by port", "pipe-columns", "pipe-node", "pipe-col", "solo-out", "solo-in"):
assert token not in text, f"/gateway's {name} still carries {token!r}"
assert 'data-gate="map"' not in html, "the stream map grew its panel back"
# The rules that survived the deletion, in the function that computes the header's live total
# and the one that paints it.
sums = js[js.index("function sumLive("):js.index("function setTally(")]
assert "unknown += 1" in sums and "attributed" in sums
assert "unknown === counted.length ? null" in sums, \
"a side where nothing measured started reporting a zero total"
assert "filter((row) => row.metric)" in sums, (
"a row that publishes no gauge is not an unmeasured port — it is absent, and counting it "
"would hold the total hostage")
tally = js[js.index("function setTally("):js.index("function renderHeader(")]
assert "classList.toggle('stale'" in tally, \
"a total that left a port out stopped saying it was partial"
assert "sum == null ? null : sum.attributed" in tally, \
"an unmeasured side must print n/a, not 0"
def test_the_page_says_where_a_line_comes_from_and_where_its_count_cannot(client) -> None:
"""Four footnotes, one per claim the page is allowed to make, and not one more.
Why the page draws a line at all (the config declares the route) and what its motion means (a
socket is open this instant, so a resting line is not a dead one); why a route carries no number
(phase 32 task 04: **the gateway could not resolve that name in this reading** — the gateway does
the resolving, so the sentence blames the reading and points at the hover that shows the address a
counted line was matched to, instead of blaming the config for having written a name or the page
for having no resolver); what the switch does **not** do (toggling a route needs a config write
path this app does not have); and — since phase 31 task 04 — what the filter box does to the map
and what it leaves alone (the tallies, the warnings and the charts keep describing the whole
ingress, because the field asks for nothing).
Each lives in the footer exactly once — the page does not repeat a why-line per panel, and it does
not need to: an uncounted line prints `—` and the footnote is the sentence that explains it
(rule 2).
"""
html = client.get(GATEWAY_PAGE).text
footer = html[html.index("<footer"):]
assert footer.count("a dash means") == 1, (
"the page's single footnote about a missing count, printed once, in the phase-32 reason")
assert "could not resolve that name" in footer, (
"a dash means the gateway's reading came back with nothing, not that the config wrote a name")
# The two sentences phase 32 made stale may not come back from either file: the blame on the page
# (`the page knows no`) and its no-DNS limit as a per-row excuse, which task 04 replaced with the
# thing to do about a dash rather than a reason to accept it.
assert "the page knows no" not in footer, (
"the dash stopped blaming the gateway's reading and went back to blaming the page")
# The served source wraps this paragraph, so the two halves are matched separately: what is
# pinned is that the sentence names a cause **and** the place to look.
flat = " ".join(footer.split())
assert "hover a counted line for the address it matched" in flat, (
"the footnote names a cause and no longer says where the address and its age are")
script = client.get("/static/page-gateway.js").text
assert "the gateway could not resolve this name in this reading" in script, (
"the per-row tooltip must say the same thing the footnote says")
assert "nothing here resolves names" not in script, (
"the old tooltip sentence blames the name, which is no longer why a line has no count")
assert "animates" in footer, "the animation rule lost its footnote"
band = footer[:footer.index("</footer>")]
# Four paragraphs and nothing else: one sentence per claim, in one ink band under the panels.
assert band.count("<p") == 4, f"the footer is {band.count('<p')} paragraphs, not four"
assert "<span" not in band, "the footer grew a second element inside a sentence"
assert "the config says the route exists" in band, "the page stopped saying what a line means"
assert "not wired up" in band and "write" in band, \
"the switch stopped saying it does nothing yet"
assert "coloured by a verdict" in band and "address" in band, \
"a claim the page still has to make left with the rewrite"
js = _code(client.get("/static/page-gateway.js").text)
counts = js[js.index("function countText("):js.index("function groupsFor(")]
assert "return null" in counts or ": null" in counts, \
"an unmeasured count must stay null for setTxt to print n/a"
def test_the_gateway_page_renders_each_degradation_arm_in_the_payloads_own_words(
client, gateway_client) -> None:
"""Four arms, four sentences, none of them invented in the browser.
* **unconfigured** — `404 {configured: false}`. The route's own refusal names `GATEWAY_HOST`,
and the page's setup block is the other half of the same sentence; the panels stay hidden
because there is nothing to draw and nothing to age (invariant 3).
* **a host that has never answered** and **a pre-1.2.0 / `--no-proxy` agent** — both dim
(`unreachable`, or `supported !== true`) rather than redden, and both arrive with
`payload.why` already written by `sources/gateway.py` (with the agent's own error line when it
had one), so the page prints `#gate-banner` from the payload and never re-derives a sentence of
its own about someone else's config.
* **a socket table that did not answer** — the configs still draw and only the counts go `n/a`.
* every case that did not measure keeps its numbers as `n/a`, which `setTxt` does with a `null`
and which `countText` is the only gate for — there is no `|| 0` anywhere on a count, because a
zero is a claim about traffic and `n/a` is not.
"""
html = _code(client.get(GATEWAY_PAGE).text)
empty = html[html.index('id="gate-empty"'):html.index('</div>', html.index('id="gate-empty"'))]
assert "GATEWAY_HOST" in empty and "1.2.0" in empty
assert 'id="gate-banner"' in html
js = _code(client.get("/static/page-gateway.js").text)
assert "$('#gate-banner')" in js and "snapshot.why" in js
assert "err.status !== 404" in js, (
"only the unconfigured refusal may raise the setup block; a failed read keeps the drawing")
assert "markStale(" in js and "function dimmed(" in js, \
"an arm that could not measure must dim, not redden"
dimmed = js[js.index("function dimmed("):js.index("function cardElements(")]
assert "unreachable" in dimmed and "supported !== true" in dimmed, \
("the dim is keyed on something other than the two arms that did not measure — a host that "
"has never answered and an agent that predates the sections are the `.stale` cases")
counts = js[js.index("function countText("):js.index("function groupsFor(")]
assert "return null" in counts or ": null" in counts, \
"countText no longer turns the unknown into the `n/a` setTxt prints"
assert not re.search(r"connections\s*\|\|\s*0", js), "a missing count became a claimed zero"
# …and the tallies obey the same rule over a real payload: an arm that measured nothing prints
# `n/a` in every cell rather than four zeros about a config nobody read.
silent = gateway_client.get("/api/gateway/snapshot").json()
header = js[js.index("function renderHeader("):js.index("async function refresh(")]
assert "measured ?" in header and "!dimmed(snapshot)" in header, (
"the tallies are no longer keyed on the two arms that did not measure")
assert silent["sockets"]["established"] >= 0, "the control: this payload did measure"
stub = dict(silent)
stub["supported"] = None
stub["unreachable"] = True
stub["listeners"] = []
stub["connections"] = []
assert sum(1 for field in ("listeners", "connections") if stub[field]) == 0, (
"the stub arm's rows are not empty, so the header's `n/a` would not be about the same thing")
# (5) a **resolver** that did not answer (agent 1.2.0, `--no-resolve`, or a pass that could not
# run) is the fifth arm and the quietest: the section is not one of the two config files, so
# it raises no banner and no colour, and every name-written line simply carries the dash with
# the resolution sentence. `full` is that document — no `resolver` key at all.
assert silent["resolver"]["available"] is None and silent["resolver"]["entries"] == [], (
"the `full` capture is supposed to be the no-resolver arm this case measures")
assert silent["why"] is None, "an absent resolver section became a page-wide complaint"
assert "severity" not in json.dumps(silent), "the fifth arm arrived with a verdict on it"
dashes = [row for row in silent["connections"] if row["connections"] is None]
assert dashes, "a document with no resolver counted a name-written route"
assert all(count_surfaces(row)["why"] == COUNT_WHY_UNRESOLVED for row in dashes), \
"a line nobody resolved is being explained by something other than resolution"
assert not any(count_surfaces(row)["glyph"] == "0" for row in dashes), \
"the uncounted became a claimed zero"
def test_the_gateway_page_keeps_its_last_drawing_when_a_poll_fails(client) -> None:
"""Invariant 4 on the fifth page: a failed read leaves the last picture on screen.
The cards are hidden **by the payload**, never by the failure path — a `fetchJSON` rejection
(a proxy hiccup, a server mid-restart) updates the status line and returns, so the operator
keeps the topology they were reading instead of an empty grid whose emptiness would read as
"this gateway routes nothing".
"""
js = _code(client.get("/static/page-gateway.js").text)
refresh = js[js.index("async function refresh("):js.index("function pruneCards(")]
guard = refresh[:refresh.index("$('#gate-empty').hidden = true")]
assert "catch" in guard and "$('#gate-cards')" not in guard, \
"the failure path blanks the card grid — the last good drawing must survive it"
assert "$('#sub').textContent = err.message" in guard, \
"and the reason has to be printed where the operator is already looking"
COUNT_WHY_UNRESOLVED = "the gateway could not resolve this name in this reading"
COUNT_WHY_NO_SOCKETS = "the socket table did not answer this reading"
def count_surfaces(row: dict[str, Any]) -> dict[str, Any]:
"""The count column's two strings for one route, as `countWhy` / `countTitle` build them.
A **second** reading of the same decision, with the served-byte pins below as its other half:
* `glyph` — the em dash for a null, the number otherwise (`0` included, because a resolved
endpoint with no socket is a measurement and not a gap);
* `why` — for a null, which of the two unknowns the payload describes: an address is **known**
when the config wrote one (`out.address is True`) or the gateway published one
(`out.resolved` non-empty), and a row with neither is a name nothing could resolve;
* `title` — for a counted row, the number plus the provenance the tooltip owes it. The **age**
fragment is left out here and pinned as a prefix instead, because it is a clock: the test
asserts what comes before it and what the remainder looks like.
"""
out = row["out"]
shown = row["connections"]
if shown is None:
known = out["address"] is True or bool(out["resolved"])
why = COUNT_WHY_NO_SOCKETS if known else COUNT_WHY_UNRESOLVED
return {"glyph": "\u2014", "why": why, "title": f"no number: {why}",
"aria": f"connections not attributed: {why}", "age": False}
title = f"{shown} open connection(s) on this endpoint"
via = out["via"] if isinstance(out["via"], str) and out["via"] else None
if out["resolved"] is None or via is None:
return {"glyph": str(shown), "why": None, "title": title, "age": False,
"aria": f"{shown} live connections"}
title += f" \u00b7 address from {via}"
# `Dash.duration(Dash.ageSeconds(ts))`, whose text is a clock and therefore not this mirror's to
# reproduce — only the *decision* to print it is.
return {"glyph": str(shown), "why": None, "title": title,
"age": out["resolved_ts"] is not None, "aria": f"{shown} live connections"}
def rendered_leaf(row: dict[str, Any]) -> dict[str, str]:
"""The two lines and the two long strings one leaf row prints, derived as a **second** reading.
There is no JS runner (a locked anchor), so this is `endpoint_sums`' idiom: the figure is
recomputed here from the payload, and the served-byte pins are what keep the mirror honest —
`inboundText` joins `out.selectors` and falls back to `out.kind`, `endpointText` is `out.host`
+ `:` + `out.port`, `counterpartText` is `ARROW` + endpoint + ` · out.kind`, and `leafTitle` /
`leafAria` compose those same two from the literals asserted verbatim below. A rename on either
side therefore fails a pin **and** changes what this mirror prints, which is the pair of
guarantees a page with no test runner can actually make.
The row's `aria-label` ends with the count's claim, and phase 32 task 04 made that claim carry its
cause: `count_surfaces()` is the one place that sentence is assembled, so the row and the count
element cannot disagree about which unknown a dash is.
"""
out = row["out"]
host = out["host"] or "?"
port = out["port"]
endpoint = f"{host}{'' if port is None else f':{port}'}"
kind = out["kind"]
inbound = " ".join(out["selectors"]) or kind
pair = (f"{inbound} " if inbound else "") + f"→ {endpoint}" + (f" · {kind}" if kind else "")
shared = " — this endpoint is shared with another route" if row["shared"] is True else ""
bits = [f"connection from :{row['in']['port']} to {endpoint}"]
if out["selectors"]:
bits.append("reached by " + ", ".join(out["selectors"]))
if out["name"]:
bits.append(f"server {out['name']}")
if row["shared"] is True:
bits.append("this endpoint is shared with another route")
bits.append("in the config")
bits.append(count_surfaces(row)["aria"])
return {"line1": inbound,
"line2": f"→ {endpoint}" + (f" · {kind}" if kind else ""),
"title": pair + shared,
"aria": ", ".join(bits)}
def test_a_clipped_pipe_label_keeps_its_full_text_on_the_element(client, gateway_client) -> None:
"""The ellipsis is only honest while **both ends** are on the element, and pinned so.
Phase 29 task 06 measured 28 of 42 labels ellipsised at 1440 px and found the old fix (letting the
label wrap) worse than the clip — the flex line squeezed the name to seven characters of a
40-character target, and wrapping the text inside that box tore one hostname over eleven lines.
Phase 30 task 04's trunk→leaf row re-measured it (`data/logs/30-task04-panels.out`): **0 of 33**
endpoint labels clip at 1440 px (a 391 px target column) and **27 of 33** do at 360 px (177 px), so
what remains is a phone column-width fact (AGENTS.md gap 13) and not a lost one.
Phase 31 task 02 made that sentence twice as important, because the row now prints two strings and
both are the answer: `brain.reeseapps.com` on line 1 and
`→ reeseapps-brain-of-reese.reeselink.com:443` on line 2. So the claim is strengthened from "the
target survives the clip" to **every inbound word and the whole `host:port` appear in full in the
row's `title` *and* in the pipe's `aria-label`**, for every route the capture declares. Three
things make that true rather than aspirational: neither text helper cuts a string (no `slice`, no
`substring`, no ellipsis character — an ellipsis is CSS's job and CSS does not touch `title`),
`leafTitle` composes `inboundText` with `endpointText`, and `leafAria` names the endpoint and every
selector. The payload half then runs that over the real rows, longest name included.
"""
css = _code(_css(client))
assert _declarations(css, ".pipe-name").get("text-overflow") == "ellipsis", \
".pipe-name no longer truncates, so re-measure the wrap decision this pin documents"
assert _declarations(css, ".gate-selector").get("text-overflow") == "ellipsis", \
"the trunk's name line no longer truncates — its `title` is what makes that honest"
# …and the leaf's counterpart line, which is now the longer of the row's two strings and so the
# one that actually clips. Same trio, same reason: a truncation is only honest while `title` and
# the `aria-label` below carry the whole thing.
end = _declarations(css, ".pipe-end")
assert end.get("text-overflow") == "ellipsis", \
".pipe-end no longer truncates — re-measure which of the row's two lines clips (gap 13)"
assert end.get("min-width") == "0" and end.get("white-space") == "nowrap", \
".pipe-end lost part of the truncation trio, so a long counterpart grows the card"
# …and the two declarations that decide *how much* it truncates. Task 06 measured the old page
# showing seven characters of a 40-character target, because a `nowrap` flex row shrinks the name
# to what the chip, the footnote and the `flex: none` count leave. `max-content` as its base size
# means a name longer than the column is alone on its line and gets the whole column, with the
# count, clients and domain list wrapping beneath; `min-width: 0` on the target column is what
# lets a long hostname ellipsise instead of widening the card (phase 27's grid rule).
assert "wrap" in _declarations(css, ".gate-trunk").get("flex-wrap", ""), \
"a long trunk label is squeezed beside its count again"
assert "max-content" in _declarations(css, ".pipe-name").get("flex", ""), \
"the name no longer claims the line, so it shrinks to the leftover width"
assert _declarations(css, ".gate-target").get("min-width") == "0", \
"a long target widens its card instead of ellipsising inside it"
assert "minmax(0, 1fr)" in _declarations(css, ".gate-link").get("grid-template-columns", ""), \
"the target track no longer permits 0, so a hostname grows the card"
js = _code(client.get("/static/page-gateway.js").text)
row = js[js.index("function renderLeaf("):js.index("function groupElement(")]
assert "const inbound = inboundText(conn.out)" in row, \
"the row no longer builds both of its strings from one pair of helpers"
assert "li.title = title" in row, "a clipped connection line lost the full text on the element"
title = js[js.index("function leafTitle("):js.index("function buildPipe(")]
assert "inboundText(out)" in title and "endpointText(out)" in title, \
"the row's tooltip stopped carrying one of the row's two ends — half a route is no answer"
aria = js[js.index("function leafAria("):js.index("function buildPipe(")]
assert "endpointText(out)" in aria and "selectors.join" in aria, \
"the accessible sentence stopped carrying the whole endpoint or every inbound word"
# Nothing in the row's text helpers may cut a string. The CSS ellipsis is honest only because the
# data behind it is whole, and a `slice(0, 30)` in `counterpartText` would eat the tooltip too.
helpers = js[js.index("function endpointText("):js.index("function buildPipe(")]
for cutter in ("slice(", "substring(", "substr(", "trim(", "\u2026"):
assert cutter not in helpers, (
f"a leaf's text helpers cut strings with {cutter!r} — truncation belongs to CSS so the "
"`title` and the `aria-label` can carry the whole route (gap 13)")
# …and the claim over every route the captured ingress actually declares, longest name first:
# `reeseapps-brain-of-code.reeselink.com:443`, 41 characters, is this fleet's longest endpoint and
# the one the 17-row `:443` fan-out clips at a phone width.
rows = gateway_client.get("/api/gateway/snapshot").json()["connections"]
assert rows, "the captured ingress declares no routes, so this pin would prove nothing"
longest = max(rows, key=lambda r: len(f"{r['out']['host']}:{r['out']['port']}"))
longest_text = f"{longest['out']['host']}:{longest['out']['port']}"
# 42 characters, which is the figure AGENTS.md gap 13 measures the phone clip against. The name
# itself is pinned as a family rather than a string because this fleet has two 41/42-character
# neighbours (`…-brain-of-reese` and `…-brain-of-code`) and which one is longest is a fact about
# `stream.conf`, not a contract; the **length** is the thing the clip depends on.
assert len(longest_text) == 42 and longest["out"]["host"].startswith("reeseapps-brain-of-"), (
f"the fleet's longest endpoint is now {longest_text!r} ({len(longest_text)} chars) — gap 13's "
"clip measurement and gap 13's prose both have to be re-read")
for conn in rows:
printed = rendered_leaf(conn)
whole = f"{conn['out']['host']}:{conn['out']['port']}"
for surface in ("title", "aria"):
assert whole in printed[surface], (
f"{whole} is not whole in the row's {surface} — gap 13: whatever CSS clips, the "
"element has to carry the full counterpart")
for selector in conn["out"]["selectors"]:
assert selector in printed["title"] and selector in printed["aria"], (
f"the inbound word {selector!r} is missing from a surface that is supposed to survive "
"the clip — the row's answer is *which domain goes where*, so both ends are needed")
def test_the_leaf_row_leads_with_the_inbound_word(client, gateway_client) -> None:
"""Line 1 is the domain the operator types; line 2 is the counterpart it flows to.
The operator's complaint (2026-10-23): *"I can't tell which `reeseapps.com` or
`stackexpected.com` addresses flow to their reeselink counterparts (very important)."* Phase 30
drew the **target** on the primary line and demoted the inbound `server_name` / `map` key to an
11px faint qualifier, which is exactly backwards from the question the page exists to answer. So
the row re-ranked: `.pipe-name` is the config's inbound word and `.pipe-end` is
`→ host:port · kind` underneath it.
Three pins keep that from being a cosmetic swap that quietly invents something:
* `inboundText` reads `out.selectors` and falls back to `out.kind` — **never** `out.host`, because
picking the target as the "best name" is a fact the two config files never stated (rule 2);
* all the selectors are joined, so a merged connection (two vhosts claiming one
`(in_port, endpoint)`) shows both rather than the page choosing one;
* every field the two helpers index is in `GATEWAY_PAGE_FIELDS` **and** resolves on a live
`GET /api/gateway/snapshot` row, in both directions, so a rename in `sources/gateway.py` fails
this test by name instead of printing `undefined` beside a healthy ingress.
The last block is the complaint itself, asserted on the capture: the `brain.reeseapps.com` leaf of
nginx's `:443` reads that domain on line 1 and
`reeseapps-brain-of-reese.reeselink.com:443` on line 2, every haproxy leaf leads with its `backend`
name, and no row's primary line is empty, `null` or `undefined`.
"""
js = _code(client.get("/static/page-gateway.js").text)
# (1) the two text helpers, and the one field each may never read.
head = js[:js.index("function countText(")]
assert "const ARROW = '\u2192'" in head, "the destination marker is no longer a page constant"
inbound = js[js.index("function inboundText("):js.index("function counterpartText(")]
assert "(out || {}).selectors" in inbound and ".join(' ')" in inbound, \
"line 1 stopped printing every selector the config wrote"
assert "(out || {}).kind" in inbound, \
"a row with no selector has no fallback, so its primary line would be empty"
assert "host" not in inbound and "port" not in inbound, \
"line 1 reaches for the target again — the counterpart is line 2's job and nothing else"
counterpart = js[js.index("function counterpartText("):js.index("function leafAria(")]
assert "${ARROW} ${endpointText(out)}" in counterpart, \
"line 2 stopped marking the endpoint as the destination"
assert "\u00b7 ${kind}" in counterpart, \
"line 2 lost the config's own word for the route kind, so a `map` leaf and a vhost leaf "\
"reaching one address became indistinguishable"
# (2) the call sites: line 1 is the inbound word, line 2 the counterpart, and the primary line
# is never handed the endpoint as its *first* choice.
row = js[js.index("function renderLeaf("):js.index("function groupElement(")]
assert "const inbound = inboundText(conn.out)" in row
assert "const primary = inbound == null ? endpointText(conn.out) : inbound" in row, \
"a row with no selector would print an empty primary line instead of its endpoint"
assert "name.textContent = primary" in row
assert not re.search(r"name\.textContent\s*=\s*endpointText", row), \
"the endpoint is back on the primary line — that is the reading order this task replaced"
assert "setTxt(counterpart, counterpartText(conn.out))" in row, \
"line 2 is no longer the counterpart"
assert "counterpart.hidden = inbound == null" in row, \
"the hidden rule no longer keys on the fallback, so a row could print its target twice"
markup = js[js.index("function leafRow("):js.index("function renderLeaf(")]
assert "counterpart.className = 'pipe-end mono'" in markup, \
"line 2 is no longer mono, so the two hostname columns stopped aligning down a fan-out"
assert "target.appendChild(name)" in markup and "target.appendChild(counterpart)" in markup, \
"the two lines are no longer both children of the target column"
# (3) the field table resolves in both directions for what the two helpers index.
needed = ("connections.out.selectors", "connections.out.kind",
"connections.out.host", "connections.out.port")
for path in needed:
assert path in GATEWAY_PAGE_FIELDS, (
f"the leaf row indexes {path.removeprefix('connections.')} but `GATEWAY_PAGE_FIELDS` "
"does not name it, so a rename in `sources/gateway.py` would print `undefined` here")
payload = gateway_client.get("/api/gateway/snapshot").json()
rows = payload["connections"]
assert rows, "the capture routes nothing, so the field table would resolve over an empty list"
for path in needed:
found, _values = _payload_field(payload, path)
assert found, f"/gateway's row reads {path}, which the payload does not publish"
# (4) the complaint, on the capture: `brain.reeseapps.com` → its reeselink counterpart.
brain = [conn for conn in rows if conn["out"]["selectors"] == ["brain.reeseapps.com"]]
assert brain, "the captured stream `map` no longer routes brain.reeseapps.com"
printed = rendered_leaf(brain[0])
assert printed["line1"] == "brain.reeseapps.com", \
f"line 1 reads {printed['line1']!r}, not the inbound domain the operator reads for"
assert "reeseapps-brain-of-reese.reeselink.com:443" in printed["line2"], \
f"line 2 reads {printed['line2']!r} — the counterpart is what the row exists to show"
assert brain[0]["in"]["port"] == 443 and brain[0]["source"] == "nginx", \
"that leaf no longer hangs off the stream `map`'s port in the nginx panel"
haproxy = [conn for conn in rows if conn["source"] == "haproxy"]
assert haproxy, "the capture's haproxy routes are gone, so this half proves nothing"
for conn in haproxy:
assert rendered_leaf(conn)["line1"] == conn["out"]["selectors"][0], \
"a haproxy leaf no longer leads with its `backend` name"
assert any(rendered_leaf(c)["line1"] == "default" for c in rows), \
"the map's own word for unmapped SNI was paraphrased off the primary line"
for conn in rows:
line1 = rendered_leaf(conn)["line1"]
assert line1 and line1 not in ("null", "undefined", "None"), (
f"a row's primary line prints {line1!r} — every route has an inbound word, and an empty "
"primary line is the bug the fallback exists to prevent")
# (5) the trunk is untouched by the re-rank: it still leads with its port and still refuses in
# the script's own sentence when its config names no target (this fleet's `:80` `return 301`).
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
assert "name.textContent !== port" in trunk, "the trunk no longer prints its port as line 1"
assert "no target named in the config" in js, "the `:80` refusal sentence is gone"
def test_the_switch_is_built_and_explicitly_unwired(client) -> None:
"""The switch from the sketch, and the pin that keeps it from becoming a lie.
The operator asked for the control to exist before it works: "build the switches in now but don't
wire them up." That is a real `<button role="switch">` with a real accessible name — a
toggle-shaped `<span>` would hide the reading "this route is in the config" from the people a
colour cannot tell it to — and it is **unwired**, because invariant 1 means no browser may disable
a route: `haproxy.cfg` and the nginx `server` block are the only switches on this fleet.
Four things keep the shape honest, and every one of them is the way a dead control starts looking
live:
* `aria-checked`, written by `renderLeaf` from the same value that colours the line, so a row
cannot show a green pipe beside an off switch;
* `aria-disabled="true"` and **never** the `disabled` attribute — `disabled` would take the pill
out of the a11y tree, which deletes the reading for exactly the people it is for;
* a `title` that names the missing write path rather than promising a click;
* no listener bound to it, no `:hover` rule anywhere in the map, and no `cursor` but
`not-allowed`. The one `transition` is the knob's, and `test_the_pipe_animation_stills_in_both_
states_under_reduced_motion` pins that it is stilled on request.
"""
raw = client.get("/static/page-gateway.js").text
js = _code(raw)
# The completion criterion, asserted rather than grepped: in the code (comments are prose about
# the difference, and they are allowed to say the word), every `disabled` carries `aria-` and
# none of them is the attribute that would hide the reading.
bare = re.findall(r"(?<!aria-)\bdisabled\b", js)
assert not bare, (
f"the code writes `disabled` {len(bare)} time(s) without the `aria-` prefix — that attribute "
"takes the switch out of the a11y tree, which deletes the reading for exactly the people "
"a colour cannot tell it to")
assert "setAttribute('aria-disabled', 'true')" in js
switch = js[js.index("function buildSwitch("):js.index("function switchAria(")]
assert "document.createElement('button')" in switch, \
"the switch is not a button, so it has no accessible role to carry the reading"
assert "button.setAttribute('role', 'switch')" in switch
assert "button.setAttribute('aria-disabled', 'true')" in switch
assert not re.search(r"\bbutton\.disabled\b|setAttribute\('disabled'", switch), (
"the switch grew the `disabled` attribute, which drops it out of the a11y tree — the reading "
"would disappear for a screen reader while looking identical")
title = re.search(r"button\.title = ([^;]+);", switch)
assert title, "the switch lost its `title` outright"
assert "Toggling a route is not wired up yet" in title.group(1), \
"the tooltip stopped naming the missing write path"
assert "config write path" in title.group(1), \
"the tooltip promises something and does not say what is missing"
assert "gate-switch-thumb" in switch, "the pill lost its knob, so it is no longer the sketch"
assert "addEventListener" not in switch
# `aria-checked` is patched from the payload pass, beside the colour that means the same thing.
leaf = js[js.index("function renderLeaf("):js.index("function groupElement(")]
assert "sw.setAttribute('aria-checked', checked)" in leaf
assert "const on = routeInConfig(conn)" in leaf, \
"the switch's state is no longer the value the line is coloured from"
assert "sw.setAttribute('aria-label', swAria)" in leaf, \
"the switch has no accessible name, so it is a pill with a claim it never states"
label = js[js.index("function switchAria("):js.index("function leafRow(")]
for fact in ("endpointText(conn.out)", "selectors", "config", "on ? 'on' : 'off'"):
assert fact in label, f"the switch's name stopped saying {fact}"
# The events this page listens for, as an allow-list rather than a count: `toggle` is the charts
# disclosure, `DOMContentLoaded` is the boot, and the two phase-31 names belong to the filter
# field and to nothing else. **`click` is not on the list** and that is the switch's pin — the day
# a route can be toggled, some code has to dial a config write, and invariant 1 says this app has
# no such thing.
bound = set(re.findall(r"addEventListener\('(\w+)'", js))
assert bound <= {"toggle", "DOMContentLoaded", "input", "keydown"}, (
f"the page now binds {sorted(bound)} — a switch that could be clicked would need a config "
"write path this app does not have (invariant 1)")
assert "click" not in bound, \
"a click handler appeared on /gateway: nothing on this page may write to the fleet"
filter_body = js[js.index("function initFilter("):js.index("function pruneCards(")]
for event in ("input", "keydown"):
assert f"addEventListener('{event}'" in filter_body, (
f"the filter no longer binds {event} in its own function, so the allow-list above is "
"hiding a listener somewhere it cannot be read")
assert js.count(f"addEventListener('{event}'") == 1, \
f"{event} is bound in more than one place on this page"
assert not re.search(r"gate-switch[^\n]*\)\s*\.?\s*addEventListener", js), \
"a listener was bound to the switch itself"
css = _code(_css(client))
rules = re.findall(r"([^{}]*\.gate-switch[^{}]*)\{([^}]*)\}", css)
assert rules, "the switch has no rule at all, so its ink is the browser default"
for selector, body in rules:
assert ":hover" not in selector, f"the switch grew a hover affordance: {selector.strip()}"
assert ":enabled" not in selector and ":disabled" not in selector, (
f"{selector.strip()} keys the switch on its enabled state — a switch this app cannot "
"wire would then paint every one of them off")
cursor = [d for d in body.split(";") if "cursor" in d]
assert all("not-allowed" in d for d in cursor), \
f"the switch advertises a pointer: {cursor}"
for token in re.findall(r"var\((--[a-z0-9-]+)\)", body):
assert re.search(rf"^\s*{token}:", css, re.MULTILINE), \
f"the switch reads an undeclared colour token: {token}"
# The pill and its knob take the existence pair, so the gateway owns one colour vocabulary and
# the soft fill derives from it rather than borrowing a verdict tint.
assert "var(--gate-on)" in _declarations(css, ".gate-switch").get("border", ""), \
"the switch's on-state border is not the existence pair"
assert "var(--gate-off)" in _declarations(
css, '.gate-switch[aria-checked="false"]').get("border-color", ""), \
"the off state has no colour, so `[aria-checked]` is decoration"
section = _code(_css_section(_css(client), "gateway pipes"))
assert "--ok-soft" not in section and "--fail-soft" not in section, \
"a gateway rule reached for a verdict tint instead of deriving its fill from the pair"
interactive = [sel.strip() for sel, body in re.findall(r"([^{}]+)\{([^}]*)\}", css)
if ("gate-" in sel or ".pipe" in sel)
and (":hover" in sel or "cursor: pointer" in body
or ("transition" in body and "gate-switch-thumb" not in sel))]
assert not interactive, (
f"the connection map grew an interactive-looking rule: {interactive} — nothing on this page "
"writes to the fleet, and the one moving part is the knob, which is stilled on request")
# …and the page says so, once, in the footer rather than in a tooltip.
html = _code(client.get(GATEWAY_PAGE).text)
foot = " ".join(html[html.index("<footer class="):].split())
assert "toggling a route needs a config write path and is not wired up" in foot, \
"the switch stopped saying out loud that it does nothing"
def test_the_stream_map_is_rendered_inside_nginx_and_nowhere_else(
client, gateway_client) -> None:
"""The `map` is nginx config, so its fan-out is the nginx panel's `:443` leaves and nothing else.
Phase 29 drew the stream `map` as a card of its own, which split one file's routes across two
headings and left the operator to work out that the sixteen domains on one card left through the
port shown on the other. The payload already says it (`out.kind == "map"`, `source == "nginx"`,
`in.port == 443`), so the page has no `map` key, no `map_targets` reader, and no third panel.
Pinned from both ends: the script ships exactly the two panel definitions and never indexes
`nginx.map_targets`, and the live document's map connections are all nginx's, all on one port,
and exactly as many as the map has routing entries (18 on this capture — 17 `*.reeselink.com`
plus the `default` the file writes for SNI nobody mapped).
"""
html = _code(client.get(GATEWAY_PAGE).text)
js = _code(client.get("/static/page-gateway.js").text)
definitions = js[js.index("const CARDS = ["):js.index("];", js.index("const CARDS = ["))]
keys = re.findall(r"key: '(\w+)'", definitions)
assert keys == list(GATEWAY_CARD_KEYS), (
f"the script ships {keys}; the stream map must not be a panel key")
assert "map_targets" not in js, (
"the page reads the map's target list again — its fan-out arrives as connections[] now")
assert 'data-gate="map"' not in html
payload = gateway_client.get("/api/gateway/snapshot").json()
mapped = [row for row in payload["connections"] if row["out"]["kind"] == "map"]
assert mapped, "the captured stream config routes nothing, so this pin proves nothing"
assert {row["source"] for row in mapped} == {"nginx"}, (
"a `map` route is filed under a config family other than nginx")
assert {row["in"]["port"] for row in mapped} == {443}, (
"the map's leaves hang off a port that is not the stream server's `listen`")
assert len(mapped) == len(payload["nginx"]["map_targets"]), (
"the leaves the nginx panel draws are not the map's routing entries — one of the two lists "
"is dropping an entry")
selectors = {sel for row in mapped for sel in row["out"]["selectors"]}
assert "default" in selectors, (
"the map's own word for unmapped SNI was paraphrased away instead of printed verbatim")
def test_a_listener_with_no_declared_target_says_so_instead_of_drawing_nothing(
client, gateway_client) -> None:
"""A trunk that routes nowhere is a fact, not a gap in the drawing.
This fleet's nginx `:80` vhost is a `return 301`: it listens, it names `_`, and it writes no
`proxy_pass`, so the graph has a listener for it and no connection. Dropping the group would turn
"this port serves no target" into "this port does not exist" — the same refusal `sources/gateway.py`
keeps for the warning list — so the leaf list renders one faint sentence instead of nothing, and
the sentence comes from the script (the payload publishes no row to render).
"""
payload = gateway_client.get("/api/gateway/snapshot").json()
untargeted = [row["port"] for row in payload["listeners"]
if not any(conn["in"]["port"] == row["port"] and conn["source"] == row["source"]
for conn in payload["connections"])]
assert 80 in untargeted, (
f"the capture's :80 vhost is no longer the live case this pin is written against: "
f"{untargeted}")
js = _code(client.get("/static/page-gateway.js").text)
groups = js[js.index("function groupsFor("):js.index("function endpointText(")]
assert "groupFor(row.port).listener = row" in groups, (
"a listener no connection names no longer produces a group")
assert not re.search(r"\.filter\([^)]*connections\.length", groups), (
"groups with no leaves are being dropped, which is the silence this test exists to stop")
leaves = js[js.index("function renderLeaves("):js.index("function renderGroups(")]
assert "no target named in the config" in js, "the refusal sentence is gone"
assert "group.connections.length === 0" in leaves, (
"the empty leaf list no longer renders the sentence instead of nothing")
def test_a_fan_out_is_patched_and_reordered_never_rebuilt(client) -> None:
"""Seventeen rows off one trunk are built once and then patched, in payload order.
The slot-grid discipline (`/llm`): rebuilding a row on every 30 s poll re-announces it to
assistive tech for a difference of one connection, and a 17-row fan-out makes that audible. So
groups are keyed by port and rows by the connection's own `id` in per-panel `Map`s, an existing
node is moved with `appendChild` rather than re-created, and pruning happens once per panel by
id — not by emptying the container, which is the rebuild in slower motion.
"""
js = _code(client.get("/static/page-gateway.js").text)
groups = js[js.index("function renderGroups("):js.index("function familyWord(")]
leaves = js[js.index("function renderLeaves("):js.index("function renderGroups(")]
assert "state.groups" in groups and "state.rows" in groups
for name, body in (("group", groups), ("leaf", leaves)):
assert "appendChild" in body, (
f"the {name} renderer can no longer reorder the rows it already built")
assert "built.get(id)" in groups and "let el = built.get(id)" in groups, (
"a group is no longer keyed on the element the last poll built")
assert "let li = rows.get(id)" in leaves, (
"a leaf row is no longer keyed on the element the last poll built")
assert ".remove()" in groups, "the panel never prunes the rows and groups that left the payload"
assert "box.innerHTML = '';" not in groups, (
"a panel's groups are wiped every poll — that is a rebuild, and it re-announces every row")
# …and the wipe that IS allowed: a panel whose whole family left the payload.
prune = js[js.index("function pruneCards("):js.index("document.addEventListener(")]
assert "box.innerHTML = ''" in prune and "state.groups.delete" in prune
def test_warnings_render_in_the_overviews_alert_vocabulary_and_hide_when_clean(
client, gateway_client) -> None:
"""The config-integrity list, both directions, on the shape the overview already uses.
`/gateway`'s warnings are `sources/gateway.warnings()`' rows, rendered as `li.warn` with a
`.src` (the config family) and a `.msg` (the row's own sentence) — the markup `page-index.js`
uses for an alert, because "a backend no frontend routes to" is the same kind of sentence and
should not grow a second look. The page reads `kind` and `message` and invents nothing.
Both directions are pinned because the absence is half the claim: the captured ingress is clean
(`warnings == []`) and the panel must stay `hidden`, and one real row from the source must make
it appear with that row's words, four fields and all.
"""
html = _code(client.get(GATEWAY_PAGE).text)
panel = _warnings_section(html)
assert "hidden" in panel[:panel.index(">")]
assert '<ul class="alerts" id="warnings-list">' in panel
js = _code(client.get("/static/page-gateway.js").text)
body = js[js.index("function renderWarnings("):js.index("function dotFor(")]
assert "li.className = 'warn'" in body, "the row is not the overview's `.warn` row"
assert "src" in body and "msg" in body, "the row lost the overview's two spans"
assert "snapshot.warnings" in body, "the page derives a warning instead of rendering one"
assert "warning.message" in body and "familyWord(warning.kind)" in body
assert "hidden = rows.length === 0" in body, "an empty list no longer hides the panel"
assert "innerHTML" not in body and "escapeHtml" not in body, \
"the warnings rows interpolate upstream text into markup"
clean = gateway_client.get("/api/gateway/snapshot").json()
assert clean["warnings"] == [], "the captured configs are no longer clean — this arm changed"
row = one_real_warning()[0]
job = gateway_client.app.state.ctx.hub.jobs["gateway"]
assert job.state.payload is not None and job.state.payload["warnings"] == []
job.state.payload["warnings"] = [row]
served = gateway_client.get("/api/gateway/snapshot").json()["warnings"]
assert served == [row], "the route does not serve the warning the page would render"
assert {"id", "kind", "subject", "message"} <= set(served[0])
assert served[0]["kind"].startswith("haproxy"), (
"the row the page files under a config family is not filed by one")
job.state.payload["warnings"] = []
def test_an_unattributed_endpoint_prints_a_dash_and_never_a_zero(client, gateway_client) -> None:
"""A leaf the join cannot count prints `—`, and the tooltip says **which** unknown it is.
`/proc` reports addresses, so a route whose far end has no address cannot be counted, and `0` would
be a claim about traffic (rule 2) while a column of `n/a` would be twelve identical words
competing with the page's one footnote — so the leaf prints an em dash whose `title` names the
reason, while the trunk keeps `n/a`, which is a different fact: the socket table itself went quiet.
Phase 32 task 04 sharpened the mechanism and not only the wording: the dash now has **two** causes
that `countCause` separates from `out.address` / `out.resolved` — a name the gateway could not
resolve, and an address that is known beside a table that answered nothing — and the sentence is
chosen from data, never guessed. A `0` is neither of them: it is a measurement.
"""
js = _code(client.get("/static/page-gateway.js").text)
head = js[:js.index("function countText(")]
assert "const NO_COUNT = '—'" in head, "the unattributed leaf count is no longer an em dash"
assert "the gateway could not resolve this name in this reading" in head, \
"the dash stopped explaining itself"
assert "function countCause(" in head and "out.address === true" in head \
and "out.resolved" in head, \
"the dash's cause is no longer read from the two payload fields that decide it"
assert "nothing here resolves names" not in head, (
"the old sentence blamed the name, which phase 32 task 03 made a false reason")
leaf = js[js.index("function renderLeaf("):js.index("function groupElement(")]
assert "shown == null ? NO_COUNT : shown" in leaf, "a null count is not the dash any more"
assert "count.title = shown == null ? countWhy(conn) : countTitle(conn, shown)" in leaf, \
"the dash lost its sentence, or a counted line lost the provenance beside its number"
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
assert "live" in trunk and "n/a" not in trunk, (
"the trunk now prints the leaf's dash too — a quiet socket table is a different fact, and "
"setTxt turns a null into `n/a` on its own")
assert not re.search(r"connections\s*\|\|\s*0", js), "an uncounted endpoint became a claimed 0"
payload = gateway_client.get("/api/gateway/snapshot").json()
unattributed = [row for row in payload["connections"] if row["connections"] is None]
assert unattributed, "the captured join counted everything, so this arm has nothing to prove"
assert all(row["out"]["address"] is False for row in unattributed), (
"an endpoint the config wrote as an address came back with no count — that is the socket "
"table's arm, not the name arm, and it prints a different sentence")
def test_an_unresolved_name_and_an_idle_route_say_two_different_sentences(
client, gateway_snapshot: Any) -> None:
"""The em dash and the measured `0` are two facts, and every surface of the row says so.
Phase 32 task 03 made a name-written route countable, which left the page's one dash sentence
explaining two different things: a name the gateway could not resolve, and a route that **was**
resolved and simply has nothing open. Task 04 splits them, and the split has to be on every
surface a reader can meet — the glyph, the `title`, the `aria-label` — because a `0` read as a
missing measurement (or a dash read as an idle route) sends the operator to the wrong box.
Three arms, all from the recorded capture and one injection, all rendered through
`count_surfaces` (the mirror) with the served-byte pins below as its other half:
* **resolved and idle** — `brain.reeseapps.com` reaches
`reeseapps-brain-of-reese.reeselink.com:443`, the gateway resolved it to `10.1.2.64`, and the
kernel reports no socket: the glyph is `0`, and a `0` is a measurement.
* **resolved and busy** — the ntfy route, whose glyph is its own number.
* **not resolved** — one entry of the recorded document emptied and marked `nxdomain`, exactly
what agent 1.3.0 publishes for a name with no record: the glyph is the em dash and the sentence
is the resolution one, never the socket one.
And a fourth, because the third arm's sentence must not become a catch-all: with the kernel's
table unreadable, a line whose address the **config wrote** is uncounted too, and blaming
resolution there would be false — so it gets the socket-table sentence.
"""
js = _code(client.get("/static/page-gateway.js").text)
head = js[: js.index("function countText(")]
# (1) the two sentences exist, differ, name no `n/a`, and neither is the other's wording.
assert "const WHY_UNRESOLVED = 'the gateway could not resolve this name in this reading'" in head
assert "const WHY_NO_SOCKETS = 'the socket table did not answer this reading'" in head
assert COUNT_WHY_UNRESOLVED != COUNT_WHY_NO_SOCKETS
for sentence in (COUNT_WHY_UNRESOLVED, COUNT_WHY_NO_SOCKETS):
assert "n/a" not in sentence, "an unknown that has a cause stopped naming its cause"
# The choice is data: the same two fields `sources/gateway.py` joins on, and nothing else.
chooser = js[js.index("function countCause("):js.index("function countWhy(")]
assert "out.address === true" in chooser and "out.resolved" in chooser, \
"the dash's cause is no longer chosen from the address the config wrote or the gateway found"
assert "countCause(conn)" in js[js.index("function leafAria("):js.index("function leafTitle(")], \
"the `aria-label` no longer carries the cause the tooltip carries"
# (2) the three arms, rendered.
resolved = gateway_source.parse(gateway_snapshot("resolved"), "gateway")
def route(host: str, port: int) -> dict[str, Any]:
return next(row for row in resolved["connections"]
if row["out"]["host"] == host and row["in"]["port"] == port)
idle = count_surfaces(route("reeseapps-brain-of-reese.reeselink.com", 443))
busy = count_surfaces(route("reeseapps-ntfy.reeselink.com", 443))
assert idle["glyph"] == "0", "a resolved endpoint with nothing open is a measurement, not a gap"
assert busy["glyph"] == "3" and busy["why"] is None
assert idle["title"].startswith("0 open connection(s) on this endpoint") \
and busy["title"].startswith("3 open connection(s) on this endpoint")
broken = gateway_snapshot("resolved")
for entry in broken["resolver"]["entries"]:
if entry["name"] == "reeseapps-immich.reeselink.com":
entry["addresses"] = []
entry["status"] = "nxdomain"
unplaced = gateway_source.parse(broken, "gateway")
dash = count_surfaces(next(row for row in unplaced["connections"]
if row["out"]["host"] == "reeseapps-immich.reeselink.com"))
assert dash["glyph"] == "\u2014", "a name with no record grew someone else's number"
assert dash["why"] == COUNT_WHY_UNRESOLVED and dash["title"] == f"no number: {dash['why']}"
assert dash["aria"] == f"connections not attributed: {COUNT_WHY_UNRESOLVED}"
# (3) the fourth arm: an address the config wrote, beside a kernel that said nothing.
quiet = gateway_source.parse(gateway_snapshot("sockets_silent"), "gateway")
unwritten = count_surfaces(next(row for row in quiet["connections"]
if row["out"]["address"] is True))
assert unwritten["glyph"] == "\u2014" and unwritten["why"] == COUNT_WHY_NO_SOCKETS, (
"a line whose address the config wrote was told to go resolve something — the socket table "
"is what went quiet, and that is the sentence the trunk already prints as `n/a`")
# (4) five lines read aloud: distinct claims, no invented number, no `null` leaking as text.
spoken = [count_surfaces(row) for row in resolved["connections"][:5]]
claims = {entry["title"] for entry in spoken} | {entry["aria"] for entry in spoken}
assert len(claims) > 1, "five lines said one thing"
for entry in [*spoken, idle, busy, dash, unwritten]:
for text in (entry["glyph"], entry["title"] or "", entry["aria"]):
assert "undefined" not in text and "NaN" not in text and "null" not in text, \
f"a row printed {text!r}"
def test_a_counted_line_names_where_its_address_came_from(client, gateway_snapshot: Any,
resolved_gateway_client) -> None:
"""Every counted line says, on hover, which address it was counted to and when that was learned.
A number that arrived through the gateway's own `libc` resolution is a different claim from one
the config wrote as an address, and the tooltip is the one place both can say it without growing
a column of ink per row. Three arms, one rule: the provenance is the payload's (`out.via`,
`out.resolved_ts`) copied, never paraphrased, and a field that is not there prints **nothing**
rather than `n/a` or `NaN`.
* a **resolved name** — `… · address from libc`, and `, resolved <age>` while the entry has a
timestamp (the age is `Dash.duration(Dash.ageSeconds(…))`, so its text is a clock and only its
presence is pinned here);
* the same row with `resolved_ts` removed — the age fragment goes, the rest of the sentence
stays, and nothing prints the shared `n/a` where an age belongs;
* an **address the config wrote** — the bare sentence. It was never resolved, and stamping the
agent's `literal` echo of it as a lookup would be rule 2 with a tooltip's clothes on.
"""
js = _code(client.get("/static/page-gateway.js").text)
builder = js[js.index("function countTitle("):js.index("const NO_TARGET")]
for field in ("out.via", "out.resolved", "out.resolved_ts"):
assert field in builder, f"the counted line's tooltip stopped reading {field}"
for helper in ("duration(", "ageSeconds("):
assert helper in builder, f"the age stopped going through the shared helper {helper}"
assert "out.resolved_ts == null" in builder, \
"a missing timestamp now prints whatever duration() makes of Infinity"
assert "address from" in builder and "resolved " in builder
assert "n/a" not in builder and "\u2014" not in builder, \
"the counted tooltip grew a fallback glyph; a missing fragment is silence, not a mark"
resolved = gateway_source.parse(gateway_snapshot("resolved"), "gateway")
brain = next(row for row in resolved["connections"]
if row["out"]["host"] == "reeseapps-brain-of-reese.reeselink.com")
printed = count_surfaces(brain)
assert printed["title"] == "0 open connection(s) on this endpoint \u00b7 address from libc"
assert printed["age"] is True
served = client.get("/static/page-gateway.js").text
assert "\u00b7 address from ${via}" in served and ", resolved ${age}`" in served, \
"the two provenance fragments are no longer composed from the payload's own words"
aged = dict(brain, out=dict(brain["out"], resolved_ts=None))
lost = count_surfaces(aged)
assert lost["title"] == printed["title"], "losing the timestamp lost the address it came from"
assert lost["age"] is False, "an age was printed for a row that publishes no timestamp"
assert "NaN" not in lost["title"] and "n/a" not in lost["title"]
written = next(row for row in resolved["connections"] if row["out"]["address"] is True)
plain = count_surfaces(written)
assert plain["title"] == f"{written['connections']} open connection(s) on this endpoint", \
"an address the config wrote was given a resolution provenance it never had"
assert "address from" not in plain["title"]
# …and the field table, in **both** directions (the `SLOT_FIELDS` device): the three names the
# tooltip and the filter index are named there, and the 1.3.0 document answers each with real
# data, so a rename in `sources/gateway.py` fails by name here instead of printing a tooltip that
# quietly stopped carrying its provenance.
provenance = ("connections.out.resolved", "connections.out.via",
"connections.out.resolved_ts")
live = resolved_gateway_client.get("/api/gateway/snapshot").json()
for path in provenance:
assert path in GATEWAY_PAGE_FIELDS, (
f"the counted line reads {path.removeprefix('connections.')}, which the page's field "
"table does not name")
found, values = _payload_field(live, path)
assert found and any(value is not None for value in values), (
f"{path} is named but the resolved document never answers it, so the pin above would "
"pass over a payload that carries nothing")
assert any(value is None for path in provenance
for value in _payload_field(live, path)[1]), (
"every row published provenance, including the addresses the config wrote — the arm that "
"must print nothing is no longer distinguishable")
def test_the_docs_name_the_page_its_knob_and_its_gauges() -> None:
"""The phase's own completion criterion, pinned so it cannot rot quietly.
A page an operator cannot find in the two documents they read is a page that does not exist, and
a knob named only in `config.py` is edited in the wrong place. Three names have to be readable
in prose: the route (both page tables), `GATEWAY_HOST` **as a name and not a URL** with its
default, and the two gauge families the history API is keyed by. The agent-version requirement
belongs in the page row too — `/gateway` is unconfigured-looking rather than red when the host
runs a pre-1.2.0 agent, and that sentence is what saves the support call.
"""
root = Path(__file__).resolve().parents[1]
agents = (root / "AGENTS.md").read_text()
readme = (root / "README.md").read_text()
for name, text in (("AGENTS.md", agents), ("README.md", readme)):
row = next((line for line in text.splitlines() if line.startswith("| `/gateway`")), "")
assert row, f"{name}'s page table has no `/gateway` row"
assert "GATEWAY_HOST" in row, f"{name}'s page row never names the knob the page is gated on"
assert "1.2.0" in row, f"{name}'s page row never names the agent version the page needs"
assert "GATEWAY_HOST" in agents and "default `gateway`" in agents, \
"the config.py bullet stopped saying what the knob defaults to"
# The metric-naming paragraph: these are the names `/api/gateway/history` and `sample.source`
# speak, and the outbound one carries its port because two services on one host are two facts.
assert "gateway.in_conns@<listen-port>" in agents, "the inbound gauge left the naming paragraph"
assert "gateway.out_conns@<host>:<port>" in agents, "the outbound gauge left the naming paragraph"
assert "/api/gateway/*" in readme, \
"README stopped documenting the page's own pair of routes"
# ---- the `/disks` page (phase 09 task 04) ---------------------------------- #
#
# The fourth dashboard, asserted the way the other three are: on the served bytes, because a
# JS test runner would be a build step (a locked anchor). What is pinned is not that the page
# exists but that it keeps the four promises the phase wrote down — one card per **physical**
# drive and never one per partition, alarm chips that exist only while a counter is non-zero,
# a host without `smartctl` dimmed rather than reddened, and charts that belong to one host,
# stay collapsed until opened, and cost one history request per host per refresh.
def _disks_js(client: TestClient) -> str:
return client.get("/static/page-disks.js").text
#: The tab nav's whole content, in order: the six dashboards and nothing else. Phase 29 task 05
#: appended `/gateway` rather than slotting it in, so the five pre-existing tabs keep their order
#: and the new one is last — the assertion below compares this list to the nav's hrefs, so the
#: shape of the nav is one document rather than five separate "is X present" greps.
TAB_ORDER = list(PAGES)
def _nav_tabs(html: str) -> str:
"""The served page's `<nav class="tabs">` block, masthead slice included.
A grep for `href="/gateway"` anywhere in the document would be satisfied by a footer link; the
tab table is about the nav, so this is the window every nav pin reads.
"""
header = _masthead(html)
start = header.index('id="nav-tabs"')
return header[start:header.index("</nav>", start)]
def test_the_disks_tab_is_in_the_masthead_of_every_page(client) -> None:
"""Five tabs became six, on every page, with exactly one marking itself current.
The masthead is the app's only "which dashboard do I need" surface since phase 02 deleted
the overview's table, so a tab missing from one page is a page you cannot get to, and a
tab marked current on the wrong page is a lie about where you are.
Phase 29 task 05 made this the whole tab audit, in both directions. The loop is over
`MARKUP_PAGES` (all eight signed-in pages), the nav's hrefs must equal `TAB_ORDER` exactly —
a tab that leaves the nav is unreachable and a tab that joins it outside the table is a link
the phase pins know nothing about — and the `aria-current` count is the page's own status:
one, naming itself, on the six dashboards; zero on `/settings` and `/certs`, which are pages
and not dashboards. So a page that grew a current tab is caught here, and so is `/gateway`
shipping the tab everywhere except on itself.
"""
for path in MARKUP_PAGES:
html = client.get(path).text
nav = _nav_tabs(html)
assert [m.group(1) for m in re.finditer(r'<a href="([^"]+)"', nav)] == TAB_ORDER, (
f"{path}'s tab nav is not the six dashboards in order: {nav}")
current = re.findall(r'<a href="([^"]+)"[^>]*aria-current="page"', nav)
expected = [path] if path in PAGES else []
assert current == expected, (
f"{path} marks {current} current; a dashboard marks itself and an admin page "
f"marks nothing (it is not one of the tabs)")
assert (nav.index('href="/system"') < nav.index('href="/disks"')
< nav.index('href="/llm"') < nav.index('href="/ups"')
< nav.index('href="/gateway"')), (
"tab order is machine, drive, engine, power, ingress")
# ---- the collapsing masthead nav (phase 18) -------------------------------- #
#
# At ≤640px the five tab links leave the bar and come back as a dropdown under it, opened by
# one `☰` button. There is no JS test runner (a locked anchor), so the phase is pinned the way
# every page surface here is: on the served bytes. Three questions, in the order the pieces
# were built — is the button on every tabbed page and on no others, does the stylesheet still
# hide it above the breakpoint, and do the stylesheet and the binding still agree on the
# breakpoint itself? That last one is the only failure mode no browser check catches: a number
# that drifts in one file opens a menu into a hidden nav on the operator's phone.
#: The attributes that make `#nav-toggle` a button and not a mystery glyph: it is not a form
#: control (`type`), it says what it toggles (`aria-controls`), it says what state it is in
#: (`aria-expanded`, which `app.js` keeps honest), and its only content is a `☰`.
NAV_TOGGLE_ATTRS = ('class="ghost nav-toggle"', 'type="button"', 'aria-expanded="false"',
'aria-controls="nav-tabs"', 'aria-label="Toggle navigation"')
def _masthead(html: str) -> str:
"""The served page's own masthead — the block the toggle and the nav have to live in.
Slicing rather than searching the whole document is the point: a button in the footer would
satisfy `id="nav-toggle" in html` and anchor to nothing, because the dropdown's
`top: 100%` resolves against `.masthead`.
"""
start = html.index('<header class="masthead"')
return html[start:html.index("</header>", start)]
def test_the_nav_toggle_is_in_the_masthead_of_every_tabbed_page(client) -> None:
"""Eight pages, one identical button, and the nav it controls still carrying six tabs.
The dropdown is the only way to change dashboard on a phone, so a page missing the button is
a page you can enter and leave the tab bar on, and a page whose nav lost its `id` is a button
that toggles nothing. Pinned on all eight (`MARKUP_PAGES`) rather than on `PAGES` because the
two admin pages have the same masthead and therefore the same contract.
"""
for path in MARKUP_PAGES:
header = _masthead(client.get(path).text)
button = re.search(r'<button[^>]*id="nav-toggle"[^>]*>', header, re.S)
assert button, f"{path}'s masthead has no #nav-toggle button"
for attr in NAV_TOGGLE_ATTRS:
assert attr in button.group(0), f"{path}'s nav toggle is missing {attr}"
assert header.count('id="nav-toggle"') == 1, f"{path} has more than one nav toggle"
# The nav is the menu: the id the button names, and all six dashboards inside it.
assert '<nav class="tabs" id="nav-tabs" aria-label="Dashboards">' in header, \
f"{path}'s tab nav has no #nav-tabs for the toggle to control"
nav = header[header.index('id="nav-tabs"'):header.index("</nav>",
header.index('id="nav-tabs"'))]
for tab in PAGES:
assert f'href="{tab}"' in nav, f"{path} collapsed a nav that lost the {tab} tab"
# Order inside the masthead: brand, toggle, nav — and the cluster after them, because
# the ≤640px block right-aligns the button and wraps the cluster onto its own row.
assert (header.index('id="nav-toggle"') < header.index('id="nav-tabs"')
< header.index('class="masthead-right"')), \
f"{path} put the toggle or the nav outside the brand → tabs → cluster row"
def test_the_login_page_carries_no_nav_and_no_toggle(tmp_path: Path) -> None:
"""`/login` is brand link + theme toggle, and the shared binding must be inert there.
No `client` fixture, and that is the whole shape of the test: the signed-in one cannot see
this page at all (a signed-in `GET /login` is a `303` to the dashboard, and following it
audits `/`'s markup under the name `/login`), which is the same redirect trap
`tests/test_auth_api.py::test_the_login_page_carries_no_id_nothing_reads_and_no_dashboard_nav`
documents for the orphan-`id` audit.
Every tab on this page bounces straight back to it, so the page renders no tabs at all; a
stray hamburger would be a control that opens a menu of redirects, and the `640px` block
would reveal it. The last assertion is the other half of the contract: `app.js` still loads
here, which is only safe because `if (navToggle && appNav)` no-ops without the elements.
"""
db = str(tmp_path / "login-nav.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
try:
with TestClient(app) as anon:
response = anon.get("/login")
assert response.status_code == 200, \
"a signed-out /login must be the form, not a bounce"
html = response.text
finally:
store.close()
for absent in ('id="nav-toggle"', 'id="nav-tabs"', 'class="tabs"',
'aria-controls="nav-tabs"', 'aria-label="Dashboards"'):
assert absent not in html, f"/login renders the collapsed nav's {absent}"
assert '<a class="brand" href="/">' in html and 'id="theme-toggle"' in html, \
"/login lost the two pieces of chrome that do belong on it"
assert 'src="/static/app.js"' in html, (
"/login stopped loading the shared chrome whose guard is its no-toggle contract")
# ---- the fleet badges on every masthead (phase 25 task 01) ------------------ #
#
# `TODO.md:3-4` — "hosts disks engines UPS disappear when in the certs/account page" and the
# operator's correction: "I did want hosts disks engines UPS badges". `Dash.initChrome()`
# builds the five badges whenever `#nav-status` exists, so the **container** is the whole
# difference between a page that has them and a page that does not — the two admin pages shipped
# without one, which is why they read as the badges vanishing rather than as two odd pages. The
# fix is the container and nothing else, so the pin is the container: on every page that renders
# the masthead, in the position the six dashboards already used (first child of
# `.masthead-right`, which is what puts it on its own row — task 02 re-justifies that row and this
# pin deliberately says nothing about which way the badges face). `tests/test_auth_api.py` keeps
# its own assertion that `/login` has none: a stranger has no fleet to be told about.
#: The container exactly as all eight signed-in pages write it (`index.html` is the reference).
NAV_STATUS_ELEMENT = '<div id="nav-status" class="gap" aria-live="polite"></div>'
#: The badges `initChrome()` renders, in its own order. `gateway` is the fifth and the phase 29
#: one: the ingress has a job of its own, so it gets a dot of its own, and `refreshStatus` leaves
#: it grey when `GATEWAY_HOST` names no target (an unconfigured source is not a fault — invariant
#: 3 — so a red dot here would train the operator to ignore red).
NAV_STATUS_BADGES = ("hosts", "disks", "engines", "UPS", "gateway")
def test_every_signed_in_page_carries_the_fleet_badges(client) -> None:
"""`#nav-status` is the first child of `.masthead-right` on all eight signed-in pages.
One loop over `MARKUP_PAGES`, because the control is the same on all of them: the six
dashboards had it, `/certs` and `/settings` grew it. The `app.js` half of the assertion is
what makes the markup mean something — the badges are built from the container's existence,
so a script that stopped reading `#nav-status` would turn eight `<div>`s into dead markup and
the orphan-`id` audit alone would not say so (it also matches `#nav-status` inside the CSS).
"""
js = _code(client.get("/static/app.js").text)
assert "const nav = $('#nav-status');" in js, \
"initChrome() no longer builds the badges from #nav-status, so the container is inert"
assert "if (nav) {" in js, \
"the container guard is gone — a page without #nav-status (only /login) would throw"
for label in NAV_STATUS_BADGES:
assert f"label: '{label}'" in js, f"the badge table lost {label}"
assert "setInterval(refreshStatus, 15000)" in js, \
"the badges' refresh cadence moved — this phase changes placement, not polling"
for path in MARKUP_PAGES:
header = _masthead(client.get(path).text)
assert NAV_STATUS_ELEMENT in header, \
f"{path} has no fleet badges in its masthead (TODO.md:3)"
after_cluster = header[header.index('<div class="masthead-right">')
+ len('<div class="masthead-right">'):]
assert after_cluster.lstrip().startswith(NAV_STATUS_ELEMENT), \
f"{path} does not render #nav-status as the first child of .masthead-right "\
"(the six dashboards' position is what puts the badges on their own row)"
assert header.count('id="nav-status"') == 1, f"{path} renders the badge container twice"
def test_the_nav_toggle_css_and_binding_carry_the_same_breakpoint(client) -> None:
"""Hidden by default, revealed at one breakpoint, in two files that must agree on its value.
The base rule is *not* a literal `.nav-toggle { display: none }` and pinning that literal
would be pinning a bug: the element also carries the shared `ghost` class, whose
`button.ghost { display: inline-flex }` out-ranks a single-class rule and ships a visible
button into the desktop masthead. The shipped shape is an id rule reading a token
(`--nav-toggle-display`, `none` in `:root`), which the ≤640px block flips by re-declaring the
token on the element — so that is what is asserted here, plus the dropdown and the four close
paths the binding owns.
"""
css = _code(_css(client))
assert re.search(r"--nav-toggle-display:\s*none", css), \
"the nav toggle is no longer hidden by default — the desktop masthead grew a ☰"
assert re.search(r"#nav-toggle\s*\{\s*display:\s*var\(--nav-toggle-display\)", css), \
"#nav-toggle stopped reading the token that keeps its reveal order-independent"
nav_css = _code(_css_section(_css(client), "mobile nav"))
media = re.search(r"@media\s*\(max-width:\s*(\d+)px\)", nav_css)
assert media, "the mobile nav block lost its media query"
for rule in ("--nav-toggle-display: inline-flex", # the reveal, through task 01's token
"nav.tabs.is-open", # the links leave the row as the menu
"position: absolute",
"top: 100%", # pinned under the whole masthead
"z-index: 41", # one above the masthead's 40
"background: var(--bg-raised)",
"flex-wrap: wrap", # the controls wrap, none is hidden
# the safe-area inset keeps the first-row #nav-toggle out of the system
# top-edge zone a real phone's status bar owns (taps there never reach
# the page; headless emulation has no such zone, so only this pin says
# the bar still pads for it):
"padding-top: calc(.5rem + env(safe-area-inset-top, 0px))"):
assert rule in nav_css, f"the ≤640px nav block lost `{rule}`"
# Nothing on the phone masthead may be hidden to make the width work: the cluster wraps.
assert ".masthead-right" in nav_css and "#nav-status" in nav_css, \
"the mobile masthead hid the status cluster instead of wrapping it"
js = _code(client.get("/static/app.js").text)
binding = js[js.index("const navToggle ="):js.index("function rangeControl(")]
assert "if (navToggle && appNav)" in binding, \
"the binding lost the guard that makes it inert on /login"
assert binding.count("classList.toggle('is-open'") == 1, \
"a state change now bypasses setNavMenu(), so is-open and aria-expanded can diverge"
for path in ("navToggle.setAttribute('aria-expanded'",
"navToggle.addEventListener('click'", # close (and open) on the button
"event.target.closest('a')", # close on a tab click, delegated
"event.key === 'Escape'", # close on Esc…
"navToggle.focus()", # …with focus back on the opener
"if (!mq.matches) setNavMenu(false)"): # close on resize back to desktop
assert path in binding, f"the nav binding lost a close path: {path}"
assert "mq.addEventListener" in binding, "the resize-back close was never registered"
js_bp = re.search(r"matchMedia\('\(max-width: (\d+)px\)'\)", js)
assert js_bp, "app.js no longer reads the nav breakpoint through matchMedia"
assert js_bp.group(1) == media.group(1), (
f"the nav breakpoint drifted: CSS hides the tabs below {media.group(1)}px, "
f"app.js closes the menu below {js_bp.group(1)}px")
assert media.group(1) == "640", "the nav breakpoint moved off the 640px the phase shipped"
# ---- the three-row masthead (phase 20 task 01) ------------------------------ #
#
# `TODO.md:5-6` asked for the per-source indicators and the `⚷ Certs ⚙ Account ☾ Dark` cluster
# each on their own line (phase 25 task 02 moved both of those lines flush right; the *rows* and
# the mechanism below are phase 20's). The markup could not change — `#nav-status` is a child
# of `.masthead-right`, and the tab structure is pinned by
# `test_the_disks_tab_is_in_the_masthead_of_every_page` — so the rows are made of line breaks:
# in a `flex-wrap: wrap` container an item whose *base* size fills the line is alone on it (the
# break happens before shrink), so `flex-basis: 100%` on `.masthead-right` starts row 2 of the
# bar and the same declaration on the dots starts row 2 *inside* it, pushing the controls to
# row 3. The alternative the task allowed — `flex-direction: column` — would have put the brand
# and the tabs on separate lines, which is why the fallback shipped. A browser proves the
# geometry at 1280 px and 360 px (`data/logs/20-task01-rows.cjs`, `masthead-rows*.png`); these
# pins keep a later re-flow from putting the buttons back on the dots' line in silence.
def _rule_body(css: str, selector: str) -> str:
"""One rule's declaration block, with the comments stripped.
Anchored on the selector itself rather than a section slice: `.masthead-right {` must not
satisfy a lookup for `.masthead {`, and the ≤640px block restates both selectors.
"""
match = re.search(rf"{re.escape(selector)}\s*\{{([^}}]*)\}}", _code(css))
assert match, f"dash.css no longer has a `{selector} {{ … }}` rule"
return match.group(1)
def _declarations(css: str, selector: str) -> dict[str, str]:
"""That block as `{property: value}`, so a pin can ask whether a property exists at all."""
found: dict[str, str] = {}
for declaration in _rule_body(css, selector).split(";"):
prop, sep, value = declaration.partition(":")
if sep:
found[prop.strip()] = value.strip()
return found
def _cascade(css: str, *selectors: str) -> dict[str, str]:
"""The declarations of several rules merged in cascade order, later winning.
Needed for the ≤640px menu: since phase 20 task 03 the dropdown's *geometry* lives on the
closed `nav.tabs` rule and `.is-open` carries only the visible counterpart, because a box
that does not exist cannot animate. A pin that asks "does the open menu sit at `top: 100%`
and paint above the bar?" is asking about the element, so it has to read both rules the way
the browser does — one duplicated `position` in two rules is the drift this avoids.
"""
merged: dict[str, str] = {}
for selector in selectors:
merged.update(_declarations(css, selector))
return merged
def test_the_masthead_is_three_rows_right_justified_at_every_width(client) -> None:
"""Brand + tabs, then the indicators, then the controls — three rows, two of them flush right.
Every assertion here is one declaration the row layout depends on, read out of the
**served** stylesheet: `flex-wrap` plus the two `flex-basis: 100%` rules are the rows, the
two `justify-content` values are the right justification (phase 25 task 02, after the
operator overruled phase 20's `flex-start`), and the absence of `margin-inline-start` on the
cluster keeps the *mechanism* the alignment rather than the old single-row right-pinning.
"""
css = _css(client)
bar = _declarations(css, ".masthead")
assert bar.get("flex-wrap") == "wrap" or bar.get("flex-direction") == "column", \
"the masthead can no longer break into rows: indicators and controls share a line again"
assert bar.get("flex-direction", "row") == "row", \
"flex-direction: column puts the brand and the tabs on separate rows"
assert "height" not in bar, \
".masthead has a fixed height again — three rows do not fit in --header-h and clip"
assert bar.get("min-height") == "var(--header-h)", \
".masthead lost the min-height that keeps a dots-less page from collapsing to one line"
assert bar.get("gap", "").startswith(".35rem"), \
"the row gap the three rows need is gone from the masthead's shorthand gap"
cluster = _declarations(css, ".masthead-right")
assert cluster.get("flex-basis") == "100%", \
"the control cluster no longer starts its own row under the indicators"
assert cluster.get("justify-content") == "flex-end", \
"the controls are no longer right-justified in their own row"
assert "margin-inline-start" not in cluster, \
"margin-inline-start: auto pushes the cluster back to the right edge of its own row"
dots = _declarations(css, "#nav-status")
assert dots.get("flex-basis") == "100%", \
"the status indicators no longer get a row to themselves"
assert dots.get("justify-content") == "flex-end", \
"the indicators are no longer right-justified"
def test_the_mobile_masthead_block_declares_no_second_bar_height(client) -> None:
"""One `min-height` in the file, and the safe-area padding still on the phone bar.
The ≤640px block used to own `height: auto` + `min-height` because the base rule was a
fixed `--header-h`; task 01 moved the growth to the base rule, and a mobile restatement is
exactly the pair that drifts (one says `auto`, the other a number, and the bar clips on a
phone only). The safe-area line and the wrapping cluster are the phase-18 contract and stay.
"""
css = _css(client)
nav_css = _code(_css_section(css, "mobile nav"))
block = re.search(r"\.masthead\s*\{([^}]*)\}", nav_css)
assert block, "the ≤640px block no longer styles the masthead at all"
for prop in ("height", "min-height", "flex-wrap"):
assert not re.search(rf"(^|;)\s*{prop}\s*:", block.group(1)), \
f"the ≤640px masthead re-declares `{prop}` — two rules now decide the bar's height"
assert "row-gap: .4rem" in block.group(1), "the phone lost its tighter row gap"
assert "padding-top: calc(.5rem + env(safe-area-inset-top, 0px))" in block.group(1), \
"the phone bar stopped padding for the system top-edge zone the toggle sits in"
# The cluster still wraps rather than hiding a control, and so do the dots (phase 29 task 05:
# five of them are 352px of chips against this bar's 320px content box, and `nowrap` on an item
# that may not shrink widens the *document* — the sideways scroll phase 18 forbids. `data/logs/
# 29-task05-tabs.cjs` measures the document at 360px on all eight pages, both widths).
assert re.search(r"#nav-status\s*\{[^}]*flex-wrap: wrap", nav_css), (
"the badge row is nowrap again: with five badges it pushes the document past the phone's "
"viewport instead of taking a second line")
assert re.search(r"\.masthead-right\s*\{[^}]*flex-wrap: wrap", nav_css)
# ---- the masthead that scrolls away (phase 20 task 02) ---------------------- #
#
# `TODO.md:10` — "The navbar is only sticky part way down certain pages on mobile? It shouldn't
# be sticky at all. We want to maximize the amount of screen space for data viewing." The quirk
# and the footprint had the same cause, so both went with the stick. What has to stay is the
# bar's second job: it is the containing block the ≤640px dropdown's `top: 100%` resolves
# against, which is why the declaration is `relative` and not the default `static`. These pins
# read the **served** stylesheet, so a re-stick or a `static` regression fails here rather than
# in a phone's hands. `data/logs/20-task02-unsticky.cjs` is the browser half: scrolled bars at
# 1280 px and 360 px, and the menu still landing under the bar.
def test_the_masthead_scrolls_away_and_stays_the_dropdown_anchor(client) -> None:
"""Not `sticky`, not `static` either: a relative bar scrolls and still anchors the menu.
Every assertion is one half of that trade. `sticky` (with its `top: 0`) is the pinning the
task removes and a `top` on a relative bar would shift the whole masthead down instead;
`position: relative` is the positioned ancestor the dropdown needs; the two `z-index`es are
the stacking sentence "the menu paints one above the bar", which survives the un-stick.
"""
css = _css(client)
bar = _declarations(css, ".masthead")
assert bar.get("position") == "relative", (
"the masthead stopped being the positioned ancestor the ≤640px dropdown resolves "
"against — `sticky` pins the bar to the viewport, `static` moves the menu to the top "
"of the document")
assert "sticky" not in bar.values(), \
".masthead is sticky again: the bar must scroll away for data viewing (TODO.md:10)"
assert "top" not in bar, \
"`top` belongs to a sticky bar; on a relative one it offsets the whole masthead"
assert bar.get("z-index") == "40", \
".masthead lost its z-index, so the dropdown's 41 no longer means one above the bar"
# The mobile half of the contract: the menu still opens directly under the bar and still
# paints above the content it overlaps — with the bar itself now free to scroll. Read as a
# cascade: task 03 moved the box onto the closed rule so the slide has something to animate,
# and `.is-open` is the visibility flip on top of it.
nav_css = _code(_css_section(css, "mobile nav"))
dropdown = _cascade(nav_css, "nav.tabs", "nav.tabs.is-open")
assert dropdown.get("position") == "absolute" and dropdown.get("top") == "100%", \
"the ≤640px menu no longer opens directly under the masthead"
assert int(dropdown["z-index"]) > int(bar["z-index"]), \
"the dropdown no longer stacks above the masthead it hangs from"
# ---- the menu that slides (phase 20 task 03) -------------------------------- #
#
# `TODO.md:11` — "I want a nice animation for the hamburger menu - a slide out and slide back in
# would be perfect. See ~/Project/Personal/brain-of-reese for an example." (The project is
# `brain_of_reese`; the pattern is its `.app-nav` closed/open transition pair plus the phase-46
# reduced-motion block.) It is pure CSS over the class flip `setNavMenu()` already performs,
# which is why `app.js` and its four close paths are untouched. What that pattern *does* force
# is a re-cut of the two rules: a `display: none` box has no intermediate state, so the closed
# menu is `visibility: hidden` now and carries the dropdown geometry the transition animates,
# while `.is-open` carries only the visible counterpart. Three questions are pinned here, in the
# order that makes the pair readable: does the closed state name the invisible, non-interactive
# start of the slide; does the open state name its visible end with the `visibility` delay
# dropped; and does `prefers-reduced-motion` name **both** states, because an open rule left out
# of that block keeps animating. `data/logs/20-task03-slide.cjs` is the browser half: the
# computed closed state, a mid-transition frame, the four close paths, the no-phantom-scroll
# check, and the snap under `prefers-reduced-motion: reduce`.
#: The one duration the slide uses, quoted into both states so a pin can demand they agree.
NAV_SLIDE_MS = "180ms ease"
def test_the_closed_phone_menu_is_the_start_of_the_slide(client) -> None:
"""Invisible, 8 px high, unhittable — and a box that exists, so it can animate.
Every declaration is one half of the closed state the reference pattern needs. `display:
none` is the one thing the block must **not** contain: it removes the box, and a transition
on a box that is not rendered never runs — which is exactly why the menu used to snap, and
why the last assertion is a regression guard and not a style preference.
"""
nav_css = _code(_css_section(_css(client), "mobile nav"))
closed = _declarations(nav_css, "nav.tabs")
assert closed.get("visibility") == "hidden", \
"the closed menu is not hidden by a property that can animate"
assert closed.get("opacity") == "0", "the slide lost its fade: the menu opens at full ink"
assert closed.get("transform") == "translateY(-8px)", \
"the slide lost its 8 px rise (`TODO.md:11`): the menu appears in place"
assert closed.get("pointer-events") == "none", \
"an invisible menu that takes clicks steals taps from the content under it"
assert closed.get("transition") == (
f"opacity {NAV_SLIDE_MS}, transform {NAV_SLIDE_MS}, visibility 0s linear 180ms"), (
"the closed state's transition is what runs on the way OUT; `visibility` must be "
f"delayed by exactly the {NAV_SLIDE_MS} slide or the box vanishes before it fades")
assert "display: none" not in nav_css, (
"a `display: none` in the ≤640px nav block is a box that cannot animate — the closed "
"menu became `visibility: hidden` in phase 20 task 03 for exactly this reason")
def test_the_open_phone_menu_is_the_visible_end_of_the_slide(client) -> None:
"""`transform: none`, and no `visibility` delay — or the menu is invisible for its own 180 ms.
`.is-open` out-ranks `nav.tabs` (two classes to one), so these four declarations are what a
click on `#nav-toggle` actually applies, and the transition line is restated rather than
inherited: the open state needs the same two animated properties with the delay gone.
"""
nav_css = _code(_css_section(_css(client), "mobile nav"))
open_state = _declarations(nav_css, "nav.tabs.is-open")
assert open_state.get("visibility") == "visible", "`.is-open` no longer reveals the menu"
assert open_state.get("opacity") == "1", "the open menu is still half-faded"
assert open_state.get("transform") == "none", \
"the open menu never came down the 8 px, so the slide has no end"
assert open_state.get("pointer-events") == "auto", \
"the open menu's five links would not take a tap"
assert open_state.get("transition") == (
f"opacity {NAV_SLIDE_MS}, transform {NAV_SLIDE_MS}, visibility 0s"), (
"the open state must animate the same two properties with the `visibility` delay "
"dropped — the same duration, in the other direction")
def test_the_menu_snaps_in_both_directions_under_reduced_motion(client) -> None:
"""One reduced-motion block, and it names **both** menu states.
The reference project's phase-46 comment is the reason: `.nav.tabs.is-open` out-specifies
`.nav.tabs`, so stilling only the closed state leaves the open transition running and the
menu still animates for someone who asked not to. The block is the file's only one, merged
with the pre-existing universal rule rather than a second block that would have to be kept
in step with it.
"""
code = _code(_css(client))
blocks = re.findall(r"@media\s*\(prefers-reduced-motion:\s*reduce\)", code)
assert len(blocks) == 1, (
f"dash.css carries {len(blocks)} reduced-motion blocks; the menu's rules belong in the "
"one universal rule, or the two drift")
block = re.search(r"@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{(.*?)\n\}", code, re.S)
assert block, "the reduced-motion block no longer has a body to read"
selector = re.search(r"(nav\.tabs[^{]*)\{", block.group(1))
assert selector, "the reduced-motion block stopped naming the menu at all"
named = {s.strip() for s in selector.group(1).split(",")}
assert named == {"nav.tabs", "nav.tabs.is-open"}, (
"the reduced-motion block must name both menu states: naming only the closed one "
f"leaves the open transition animating — got {sorted(named)}")
assert _declarations(block.group(1), "nav.tabs.is-open").get("transition") == "none", \
"the named open rule stopped stilling the transition"
# ---- the masthead with no polling control (phase 21 task 01) -------------- #
#
# `TODO.md:7` — "Completely remove the pause button". The control was four things: a masthead
# button on the four dashboards, a `Space` binding, the `setPaused`/`_paused` plumbing inside
# `Dash.poll()`, and a stylesheet comment naming it as a row-3 tenant. A browser is the only
# thing that can prove the operator no longer sees a button, but the served bytes are what ship
# it, so those are what fail a test when someone puts it back.
#
# The **hidden-tab** skip is deliberately *not* part of the removal and is pinned on the same
# page for that reason: a backgrounded tab asking for nothing is idle hygiene rather than a
# decision the operator made, and a poll loop "simplified" back to an unconditional `setTimeout`
# would be invisible until a laptop battery complained.
#: Every spelling the deleted control ever had, in markup, script or stylesheet.
PAUSE_ARTIFACTS = ('id="pause"', 'name="pause"', "⏸", "▶ Paused",
"bindPause", "setPaused", "_paused")
def test_no_page_serves_a_button_that_stops_the_polls(client, tmp_path: Path) -> None:
"""Nine pages, twelve assets, zero pause control.
`/login` needs its own anonymous `TestClient` for the reason every other login-page audit
gives: this `client` is signed in, and a signed-in `GET /login` is a `303` to the dashboard,
which would audit `/`'s bytes and call them `/login`. The other eight are read as themselves.
"""
bodies = {path: client.get(path).text for path in MARKUP_PAGES}
db = str(tmp_path / "no-pause-login.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
try:
with TestClient(app) as anon:
response = anon.get("/login")
assert response.status_code == 200, \
"a signed-out /login must be the form, not a bounce"
bodies["/login"] = response.text
finally:
store.close()
assert len(bodies) == 9, "the audit lost a page; every served page owes this check"
for path, html in bodies.items():
for artifact in PAUSE_ARTIFACTS:
assert artifact not in html, f"{path} serves the removed control again: {artifact!r}"
for asset in ASSETS:
body = client.get(asset).text
for artifact in PAUSE_ARTIFACTS:
assert artifact not in body, \
f"{asset} still carries the removed control: {artifact!r}"
def test_the_poll_loop_still_idles_on_a_hidden_tab_and_binds_no_key_for_the_old_control(
client,
) -> None:
"""What survives the deletion, pinned beside what must not come back.
The first block is the hidden-tab behaviour the phase keeps (`document.hidden` skips the run
and reschedules; `visibilitychange` fires one poll on return). The second is the `Space`
binding: no `event.code` check may live in the shared script, because a key that stops the
numbers with no control on screen to say so is the failure mode this task exists to retire.
The third is the half of the old listener that was never about pausing — `T` still toggles
the theme, on the same four pages it always did.
"""
app_js = client.get("/static/app.js").text
loop = app_js[app_js.index("function poll(fn, ms) {"):app_js.index(" // ---- chrome")]
assert "if (stopped || inFlight || document.hidden) { schedule(); return; }" in loop, \
"a hidden tab no longer skips its poll — the idle-hygiene guard is not the button"
assert "document.addEventListener('visibilitychange', () => { if (!document.hidden) tick(); })" in loop, \
"returning to a backgrounded tab no longer fires a poll — the page would sit on old numbers"
assert "event.code" not in app_js, \
"a key-code binding is back in the shared script — the only one it carried was the deleted control's"
assert "'Space'" not in app_js, "the deleted control's keyboard shortcut came back"
assert "event.key === 't' || event.key === 'T'" in app_js, \
"`T` stopped toggling the theme: the shortcut that shared the deleted binding survives"
for name in ("page-system.js", "page-disks.js", "page-llm.js", "page-ups.js"):
assert "Dash.bindShortcuts();" in client.get(f"/static/{name}").text, \
f"{name} no longer opts in to the theme shortcut it always had"
def test_the_header_status_dot_picks_up_the_disks_job(client) -> None:
"""`disks` gets its own dot instead of hiding inside `hosts`.
The same agent can answer the 10-second utilisation poll and have a wedged SMART pass;
one dot for both would have to be right about neither.
"""
app_js = client.get("/static/app.js").text
assert "{ key: 'disks', label: 'disks', href: '/disks' }" in app_js
assert "s.dashboard === job.key" in app_js, "the dot stopped filtering by dashboard"
def test_disks_page_draws_one_card_per_physical_drive_from_the_api(client) -> None:
"""The card is a drive, keyed by the agent's device name, patched in place.
A mount or a partition would need a filesystem field, and `/system` already answers "is
the filesystem full"; if one leaks back in here, the page stops being about the drive.
"""
html = client.get("/disks").text
js = _disks_js(client)
assert 'id="hosts"' in html and 'id="hosts-empty"' in html
assert "fetchJSON('/api/disks/snapshot')" in js
assert "row.path" in js and "/dev/" in js, "the card stopped naming the device it is about"
# identity + verdict lead the card: no hero row, no big single number
assert 'class="heroes"' not in html, "the disk card grew a hero row"
assert "Dash.hero(" not in js
for filesystem in ("mountpoint", "fstype", "free_bytes", "worst_mount"):
assert filesystem not in js, f"{filesystem} leaked into the drive page"
# built once per device and patched, so a 16-drive host does not rebuild 16 panels a poll
assert "seen.get(row.device)" in js and "renderDiskCard(panel, row)" in js
assert "pruneHosts(" in js, "a drive that left the host keeps its card forever"
def test_disk_cards_print_the_drive_numbers_and_colour_only_rated_ones(client) -> None:
"""Temp, power-on hours and wear, coloured by the API's verdicts and nothing else.
The odometer is the deliberate exception: `severity.THRESHOLDS` has no `disk.power_on_hours`
row, so the cell is written with a `null` verdict and cannot go green at 100 h or red at
60 000 h. A drive that publishes no wear (the recorded HDD shape) prints `n/a` because
`setTxt` is the only writer.
"""
js = _disks_js(client)
for field in ("smart.temp_c", "smart.power_on_hours", "smart.percentage_used",
"row.health"):
assert field in js, f"{field} is no longer on the card"
for verdict in ("severity.temp_c", "severity.percentage_used", "severity.health"):
assert verdict in js, f"the card stopped rendering {verdict}"
assert "severityFor" not in js, "the drive page wrote a threshold of its own"
body = js[js.index("function renderDiskCard("):js.index("/** Text that prints nothing")]
assert "severity.temp_c" in body and "severity.percentage_used" in body
assert re.search(r"setMetric\(card, 'hours',[^;]*null\)", body), (
"the odometer is being graded by something severity.py does not have")
# the drive's own word, and `unknown` for a drive that gave none — never a green pass
assert "'FAILED'" in body and "'unknown'" in body
def test_disk_alarm_chips_exist_only_while_a_counter_is_non_zero(client) -> None:
"""Zero and unknown both draw no chip; only a non-zero counter draws a red one.
The `/llm` preemption rule: a permanent "0 pending sectors" row trains the eye to ignore
it, and grading a counter green would call a quiet drive an achievement. `severity.py`
rates none of these, so the chip's presence is the entire signal here.
"""
js = _disks_js(client)
for counter in ("reallocated_sector_ct", "current_pending_sector_ct",
"offline_uncorrectable"):
assert counter in js, f"{counter} is no longer an alarm"
assert "value <= 0) continue" in js, "a zero now draws a chip"
assert "alarms.hidden = chips.length === 0" in js, "the empty chip row stays on screen"
assert 'class="chip fail"' in js
css = client.get("/static/dash.css").text
assert ".alarms {" in css and ".chip.fail" in css
def test_a_host_without_smartctl_is_dimmed_with_a_one_line_why_and_never_red(client) -> None:
"""Unconfigured is not broken (invariant 3), and the three answers stay three.
`smartctl_available` is `false` (install smartmontools — the agent said so, so the page
may say it), `null` (nobody told us either way, so name the fix without claiming to know
which half is missing), or the agent is not answering at all, which is a different
sentence again. None of the three reddens anything.
"""
js = _disks_js(client)
html = client.get("/disks").text
assert "smartctl not available on" in js
assert "disk group" in js, "the why stopped naming the grant that fixes it"
assert "smartctl_available === false" in js and "smartctl_available === true" in js
assert "host.unreachable" in js, "a dead agent must not be reported as a missing binary"
assert "smart_errors" in js, "the agent's own sentence is no longer printed"
assert "classList.toggle('stale'" in js
assert 'class="banner info why" hidden' in js, "the why-line has no home on the card"
assert "banner fail" not in js and html.count("banner") == 0, (
"a missing binary reddens the page")
def test_disks_charts_are_collapsed_lazy_and_cost_one_history_request_per_host(client) -> None:
"""`/system`'s chart discipline, reused: collapsed, painted only while open, one XHR.
The request still names every series the host could draw (temperature and wear per
drive), because `/api/disks/history` answers an unknown name as an empty series and a
panel should never have to discover series it did not ask for.
"""
js = _disks_js(client)
assert '<details class="charts-box"' in js
guard = js[js.index("function drawHostCharts("):js.index("async function renderHostCharts(")]
assert "if (!chartsOpen(card)) return;" in guard, (
"a collapsed stack is being built and drawn every poll")
history = js[js.index("async function renderHostCharts("):js.index("/**\n * Drop cards")]
assert history.count("fetchJSON(") == 1, "more than one history request for one host"
assert "/api/disks/history" in history
assert "streams.temp.concat(streams.wear)" in history, (
"the request no longer names every series the panel could draw")
prune = js[js.index("function pruneHosts("):js.index("// ---- fleet header")]
for entry in ("state.hosts.delete", "state.disks.delete", "state.charts.delete",
"state.history.delete", "state.payloads.delete"):
assert entry in prune, f"a pruned host keeps its {entry}"
def test_every_disks_chart_names_itself_for_assistive_tech(client) -> None:
"""An unlabelled plot is silence, and the legend is a sibling that labels nothing."""
js = _code(_disks_js(client))
labels = re.findall(r"ariaLabel: `([^`]+)`", js)
assert len(labels) == 2, "every drive chart must name itself (temperature, wear)"
assert all(label.startswith("${host.name}") for label in labels), (
"a chart that does not say which host it plots belongs on no page")
def test_the_disks_fleet_header_reads_the_api_sums_and_invents_none(client) -> None:
"""The header's arithmetic is the API's (`fleet_disk_summary`), never the page's.
Hidden below two hosts, because with one box in `HOST_TARGETS` these sums only duplicate
that host's own cards — `/llm`'s fleet-block rule. And no cell is coloured: a count of
drives is not a graded number (`severity.py` has no row for one).
"""
html = client.get("/disks").text
js = _disks_js(client)
for element in ("fleet", "fleet-dot", "fleet-count", "fleet-disks", "fleet-smart",
"fleet-failed", "fleet-alarms"):
assert f'id="{element}"' in html, f"the header lost #{element}"
fleet = js[js.index("function renderFleet("):js.index("// ---- snapshot")]
for path in ("fleet.hosts_live", "fleet.disks", "fleet.with_smart",
"fleet.health_failed", "fleet.alarm_disks"):
assert path in fleet, f"the header no longer reads {path}"
assert "reduce(" not in fleet and "+=" not in fleet, "the page is summing drives itself"
assert "severity" not in fleet, "a drive count grew a verdict"
assert "block.hidden = true" in fleet, "one host's sums duplicate its own cards"
def test_the_disk_panel_style_is_existing_vocabulary_with_no_new_palette(client) -> None:
"""`.disk` is the `.slot` shape, and the only colours it reaches for are the verdicts."""
css = _css(client)
block = _css_section(css, "drive panels")
for selector in (".disks {", ".disk {", ".disk-head", ".disk-dev", ".disk-model",
".disk .metrics", ".disk .alarms", ".disk .metric.warn .value",
".disk .metric.fail .value"):
assert selector in block, f"dash.css lost {selector}"
for token in ("var(--warn)", "var(--fail)", "var(--border)", "var(--bg-sunken)",
"var(--text)"):
assert token in block, f"the drive panel no longer uses {token}"
assert "--disk" not in css, "a drive-only colour token appeared"
assert ".stale" in css, "the dim that marks an unreadable host is gone"
def test_the_overview_prints_the_fourth_source_cadence(client) -> None:
"""`/` names every source's interval, so a drive card four minutes old reads as behaving."""
js = client.get("/static/page-index.js").text
assert "config.disks.interval_seconds" in js
@pytest.mark.parametrize("fixture", ["client", "data_client"])
def test_summary_carries_the_drive_block_for_programmatic_consumers(
request, fixture: str) -> None:
"""`disks` is additive: the same configured-ness, the same hosts, no key retired.
`client` has no targets, so it proves the block answers as `False`/`None`/`[]` instead of
disappearing; `data_client` proves it lists the host the fleet has.
"""
test_client = request.getfixturevalue(fixture)
body = test_client.get("/api/summary").json()
populated = fixture == "data_client"
assert body["disks"]["enabled"] is populated
assert (body["disks"]["fleet"] is not None) is populated
assert body["disks"]["hosts"] == ([] if not populated else [
{"name": "driveripper", "disks": None, "with_smart": None, "health_failed": None,
"alarm_disks": None, "smartctl_available": None, "failed_devices": [],
"unreachable": True}]), "a configured host with no payload is a row of nones"
assert [row["name"] for row in body["disks"]["hosts"]] == [
row["name"] for row in body["system"]["hosts"]]
def test_summary_serves_the_same_counts_the_disks_page_prints(disks_client) -> None:
"""One projection, two consumers: the summary's numbers *are* the snapshot's."""
body = disks_client.get("/api/summary").json()
snapshot = disks_client.get("/api/disks/snapshot").json()
disks = body["disks"]
assert disks["fleet"] == snapshot["fleet"], "the header would disagree with the page"
rows = {row["name"]: row for row in disks["hosts"]}
live = next(host for host in snapshot["hosts"] if host["name"] == "driveripper")
assert (rows["driveripper"]["disks"], rows["driveripper"]["with_smart"],
rows["driveripper"]["health_failed"], rows["driveripper"]["alarm_disks"]) == (
live["summary"]["disks"], live["summary"]["with_smart"],
live["summary"]["health_failed"], live["summary"]["alarm_disks"])
assert rows["driveripper"]["smartctl_available"] is True
assert rows["driveripper"]["unreachable"] is False
assert rows["deskwork"]["unreachable"] is True
assert rows["deskwork"]["disks"] is None, "unknown, never 0 (rule 2)"
assert sorted(state["key"] for state in disks["states"]) == ["disks:deskwork",
"disks:driveripper"]
def test_summary_names_the_drive_the_health_verdict_called_failed(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, disk_snapshot: Any) -> None:
"""Which drive the hardware itself called `FAILED` is part of the fleet answer.
A notifier reading `/api/summary` should not have to fetch a second endpoint to name the
device, and the count and the list have to agree or one of them is wrong.
"""
db = str(tmp_path / "summary-failed.sqlite3")
store = SampleStore(db)
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2,
host_targets=(Target("driveripper", DEAD_URL),)), store)
with TestClient(app) as test_client:
sign_in(test_client)
now = time.time()
snapshot = disk_snapshot("driveripper", mode="failed")
snapshot["ts"] = now
result = parse_disks_snapshot("driveripper", snapshot, DisksCollector(), now=now)
app.state.ctx.hub.jobs["disks:driveripper"].state.payload = result.payload
row = test_client.get("/api/summary").json()["disks"]["hosts"][0]
assert len(row["failed_devices"]) == 1
assert row["failed_devices"][0] == result.payload["disks"][0]["device"]
assert row["health_failed"] == result.payload["summary"]["health_failed"] == 1
store.close()
# ---- every table scrolls sideways on a phone (phase 27 task 01) -------------- #
#
# `TODO.md:7` — "None of the tables are horizontally scrollable on mobile." The measurement is
# AGENTS.md gap 12: at 360 px `/`'s fleet table is 450-510 px inside a 320 px `section.card` and
# `.card { overflow: hidden }` clips `WORST DISK` / `GPU BUSY` / `TEMP` / `UP` with nothing to
# reach them. The fix is one rule inside the repo's existing ≤640px block, taken from the shape
# the all-widths `table.data.scroll` modifier already proves — `display: block` plus a horizontal
# scrollbar, so the scroll container is the table and never the document (phase 18's standing
# guarantee). It is a class rule rather than hand-placed `.scroll` classes because most of those tables
# exist only in JS-built markup and the class is the whole contract.
#
# Two failure modes the pins below are aimed at, both named in the task's assumptions: a
# `max-height` migrating in from `.scroll` (a phone then gets a height-capped table on top of a
# horizontal one), and the all-widths `.scroll` rule being edited to "share" the new mobile one.
#
# The task assumed one rule would reach every table. Measured on the live fleet at 360 px it did
# not reach the disclosure tables: `/system`'s GPU / filesystem / process tables came back 520 px
# wide inside a 320 px card with `scrollWidth === clientWidth`, because the card body is
# `.body.stack` — a one-column grid — and a grid item's automatic minimum size is its content's
# min-content width, so the `<details>` grew the column instead of the table overflowing its own
# box. `min-width: 0` on the card body's items is therefore pinned as the second half of one
# mechanism: without it the scroll rule is inert for exactly the tables the task asked about.
# `data/logs/27-task01-scroll.cjs` is the browser half; task 02's audit is the phase's proof.
def _media_body(css: str, condition: str) -> str:
"""The declarations of one `@media (…)` block, brace-matched, comments stripped.
`_rule_body` stops at the first `}` because no rule nests; a media block does, and the
≤640px block's own comments contain example rules with braces in them — hence running the
section through `_code` first. The caller picks the block by condition, so a pin can ask about
*the mobile block's* `table.data` without mistaking it for the all-widths rule of the same
name further down the file.
"""
body = _code(css)
match = re.search(rf"@media\s*\({re.escape(condition)}\)\s*\{{", body)
assert match, f"dash.css no longer has an `@media ({condition})` block"
depth, index = 1, match.end()
while depth:
char = body[index]
depth += (char == "{") - (char == "}")
index += 1
return body[match.end():index - 1]
def _table_tags(text: str) -> list[str]:
"""Every `<table …>` opening tag in a document or a JS template, comments already gone."""
return re.findall(r"<table\b([^>]*)>", text)
def test_every_table_data_scrolls_sideways_at_the_phone_breakpoint(client) -> None:
"""One mobile rule, horizontal-only, no height cap — and the desktop `.scroll` untouched.
Each assertion is one half of the mechanism: `display: block` is what lets the element be the
card's width while the table inside it is wider, `overflow-x: auto` is the scrollbar that
appears only when it overflows (so a table that fits paints nothing new at 1280 px), and the
absence of `max-height` / `overflow-y` / the `overflow` shorthand is the assumption that a
phone gets a sideways scroll and *only* a sideways scroll.
"""
css = _css(client)
# Scoped to the section the nav pins read, so "the mobile rule" means the repo's one mobile
# boundary block rather than whichever `@media (max-width: 640px)` happens to come first.
mobile_block = _media_body(_css_section(css, "mobile nav"), "max-width: 640px")
mobile = _declarations(mobile_block, "table.data")
assert mobile.get("display") == "block", (
"the mobile `table.data` is not a scroll container — its tail columns are clipped by "
"`.card { overflow: hidden }` again (AGENTS.md gap 12)")
assert mobile.get("overflow-x") == "auto", (
"the mobile rule no longer gives a wide table a way to reach its last column")
for forbidden in ("max-height", "overflow-y", "overflow"):
assert forbidden not in mobile, (
f"the mobile rule declares `{forbidden}`: the 320 px height cap is `.scroll`'s "
"vertical-use contract, not what a phone needs on top of a horizontal scroll")
# The second half of the mechanism, in the same block: a grid item may not be narrower than
# its content unless something says so, and the card bodies are one-column grids — so without
# this clause the rule above cannot overflow anything inside a disclosure and the tail columns
# stay clipped by the card. Pinned together because one without the other is the bug.
stack = _declarations(mobile_block, ".body.stack > *")
assert stack.get("min-width") == "0", (
"the mobile block lost the `min-width: 0` that lets a card body be narrower than the "
"table inside it — the disclosure tables grow the grid column and clip again")
# The all-widths rules are the *other* half of the contract: `table.data` still lays out as a
# table above the breakpoint (no scrollbar where the table fits, no layout shift at 1280 px),
# and `.scroll` still caps and scrolls both axes wherever it is applied. Read out of the
# `tables` section because the mobile rule above has the same selector and comes first.
tables = _css_section(css, "tables")
base = _declarations(tables, "table.data")
assert base.get("display", "table") == "table", (
"`table.data` is a block at every width now — the mobile rule leaked out of its media "
"block and every table grew a scroll container it does not need")
assert base.get("width") == "100%", "the base table lost the width that makes it fill its card"
scroll = _declarations(tables, "table.data.scroll")
assert scroll == {"display": "block", "overflow": "auto", "max-height": "320px"}, (
f"`table.data.scroll` changed shape: {scroll} — `#agents-table`'s both-axis box and "
"320 px cap are phase 21's contract and this phase may not edit them")
def test_a_card_header_wraps_its_facts_instead_of_losing_its_tail(client) -> None:
"""`.card > header` wraps, and the two free-text pieces are allowed to become narrower.
The 6 FAILs of the first phase-27 audit run were not tables: at 360 px the header was a
single `nowrap` flex line, so its tail — the `.updated` age stamp on `/system` (right
402-415 px), `/llm` (374-393) and `/ups` (392-531), plus the `apc` card's `.chip.model`
(376) and `.chip.health` (463) — stood past the card and `.card { overflow: hidden }` ate it.
"How old is this" and "which box is this" are facts, so the row wraps instead (measured:
`data/logs/27-task03-verify.cjs`, and `27-task03-control.cjs` shows these same offenders again
with the declarations neutralised).
`min-width: 0` is pinned with `flex-wrap` because one without the other is not the fix: a
flex item's automatic minimum size is its content's min-content width, so a long OS or model
string still cannot wrap inside its own box (the `apc` chip measured 290 px against a 252 px
card). And `white-space: normal` is scoped to a header chip, not to `.chip`: a chip is a
label elsewhere in this app and keeps its one-line shape.
"""
header = _declarations(_css(client), ".card > header")
assert header.get("flex-wrap") == "wrap", (
"the card header is a single nowrap flex line again — its tail facts are painted past "
"the card and clipped (AGENTS.md gap 12's header finding)")
shrink = _declarations(_css(client), ".card > header .chip")
assert shrink.get("min-width") == "0" and shrink.get("max-width") == "100%", (
"a header chip may not become narrower than its own text: with `flex-wrap` alone the "
"longest chip still cannot wrap and the row's tail leaves the card")
assert shrink.get("white-space") == "normal", (
"a header chip that has to share a line now truncates or overflows instead of wrapping; "
"the label has to stay readable in full")
assert _declarations(_css(client), ".card > header h3").get("min-width") == "0", (
"the header title lost the same allowance — a long host name cannot wrap either")
# Read out of the chips section, because `_rule_body` anchors on the selector text and the
# header exception above *contains* `.chip {` — the base vocabulary lives further down.
assert _declarations(_css_section(_css(client), "bars, dots, chips"), ".chip").get(
"white-space") == "nowrap", (
"`.chip` itself stopped being one line: this fix is an exception for header chips, not a "
"change to the chip vocabulary every other page draws with")
def test_a_card_disclosure_scrolls_what_it_reveals_at_every_width(client) -> None:
"""The control-width half of gap 12: the *disclosure* scrolls, so the card stops clipping.
Task 01 logged this as a FINDING at 1280 px and left the decision here. Measured, `/system`
three cards across: the "More detail" item grew to its table's min-content (520 px inside a
404 px card), and because the card clips, the GPU table's last column — "97.8 GiB", tail
right 554 against a card content edge of 424 — was painted *under the next card* (which
starts at 438 px), unreachable by any gesture. The `charts-box` item beside it was stretched
to the same 520 px, so a chart's newest edge went with it.
The fix is one declaration on the disclosure, and these assertions are why it is that one:
horizontal only (no `max-height`, no `overflow-y`, no `overflow` shorthand — a height cap is
`.scroll`'s contract, and a vertical scrollport inside a card body would be a second one),
and **outside** the ≤640px block, because the defect it fixes is at the control width. It is
not the mobile rule hoisted: `table.data` above stays `display: table`, which is what keeps
every table that fits stretched across its card instead of shrink-wrapped — pinned next door
and re-measured by `data/logs/27-task03-verify.cjs`.
"""
css = _css(client)
disc = _declarations(_css_section(css, "tables"), ".card > .body.stack > details")
assert disc.get("overflow-x") == "auto", (
"a disclosure no longer scrolls what it reveals: a table wider than its card grows the "
"item past the card again and `.card { overflow: hidden }` clips the tail column")
for forbidden in ("max-height", "overflow-y", "overflow"):
assert forbidden not in disc, (
f"the disclosure rule declares `{forbidden}`: this fix is a sideways scroll only — "
"the 320 px cap belongs to `table.data.scroll` and a vertical scrollport here would "
"hide rows the operator has not chosen to hide")
mobile = _media_body(_css_section(css, "mobile nav"), "max-width: 640px")
assert ".card > .body.stack > details" not in mobile, (
"the disclosure scroll moved inside the ≤640px block — the finding it repairs was measured "
"at 1280 px, where it would then do nothing")
def test_every_table_on_every_page_carries_the_class_the_rule_targets(client) -> None:
"""The mobile rule is class-level, so a `<table>` without `data` would silently not scroll.
Both halves are read from the served bytes: the static tables in the page templates and
the five built at runtime by `page-system.js` / `page-ups.js` (four inside `/system`'s "More
detail", one the NUT variables list). This is the drift the task chose the class rule over
the hand-placed `.scroll` classes it would replace — and it cuts both ways, which is why it is a test.
"""
tags = [tag for page in MARKUP_PAGES for tag in _table_tags(_code(client.get(page).text))]
tags += [tag for script in PAGE_SCRIPTS.values()
for tag in _table_tags(_code(client.get(f"/static/{script}").text))]
assert len(tags) >= 7, f"only {len(tags)} tables found — the sweep stopped reading the pages"
for tag in tags:
assert re.search(r'class="[^"]*\bdata\b', tag), (
f"a table outside the rule's class selector: <table{tag}> — it will clip its tail "
"columns at 360 px instead of scrolling them")
# ---- events --------------------------------------------------------------- #
def test_events_are_returned_in_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
db = str(tmp_path / "events.sqlite3")
store = SampleStore(db)
store.log_event(Event(ts=time.time(), source="host", host="deskwork", kind="down",
severity="error", message="deskwork is not answering"))
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
with TestClient(app) as client:
sign_in(client)
events = client.get("/api/events").json()["events"]
assert events[0]["host"] == "deskwork"
assert events[0]["severity"] == "error"
store.close()
def test_event_rows_say_their_subject_once(client) -> None:
"""`poller.py` messages name their own subject, and the row prints a host chip too.
"gufo gufo is answering again after 1 failure" reads as two problems — the same defect
`alerts._clean_error()` already fixes for the overview's alert list. `/ups` had the
mirror-image bug: it `replace()`d `host + ':'` *anywhere*, so "Quick battery test sent
to apc: OK" printed as "Quick battery test sent to OK". All three feeds now share
`Dash.eventText()`, which strips the wrapper and a **leading** name only.
"""
app_js = client.get("/static/app.js").text
helper = app_js[app_js.index("function eventText("):app_js.index("// ---- DOM helpers")]
assert "eventText" in app_js[app_js.index("return {"):], "eventText is not exported"
assert "^UpstreamError:\\s+" in helper, "the wrapper `alerts.py` drops is back"
assert "new RegExp" in helper and "stripped.trim() ? stripped : out" in helper, \
"the host strip is no longer anchored to the front, or can empty a message"
for page in ("/static/page-system.js", "/static/page-llm.js", "/static/page-ups.js"):
js = client.get(page).text
assert "eventText(event)" in js, f"{page} prints the raw message again"
assert "replace(event.host" not in client.get("/static/page-ups.js").text, \
"/ups is mangling the host name mid-message again"
# ---- the one write endpoint ------------------------------------------------ #
def test_command_endpoint_is_post_only(ups_client) -> None:
assert ups_client.post("/api/ups/command", json={}).status_code in (400, 403, 404)
assert ups_client.put("/api/ups/command", json={}).status_code == 405
assert ups_client.delete("/api/ups/command").status_code == 405
def test_commands_are_offered_when_actions_are_enabled(ups_client) -> None:
body = wait_for_payload(ups_client, "/api/ups/snapshot")
assert body["simulated"] is True, "the simulator must label itself"
assert body["actions_enabled"] is True
ups = body["ups"][0]
enabled = [item for item in ups["commands"] if item["enabled"]]
assert enabled, "with NUT_ALLOW_ACTIONS=1 and a simulator, safe commands are sendable"
assert all(item["risk"] in ("safe", "caution", "dangerous") for item in ups["commands"])
def test_a_safe_command_actually_reaches_the_driver(ups_client) -> None:
body = wait_for_payload(ups_client, "/api/ups/snapshot")
name = body["ups"][0]["name"]
response = ups_client.post("/api/ups/command",
json={"ups": name, "command": "test.panel.start"})
assert response.status_code == 200, response.text
payload = response.json()
assert payload["ok"] is True and payload["simulated"] is True
events = ups_client.get("/api/events").json()["events"]
assert any("test.panel.start" in event["message"] or "Panel test" in event["message"]
for event in events), "every sent command must leave an audit trail"
def test_a_dangerous_command_needs_the_confirmation_word(ups_client) -> None:
name = wait_for_payload(ups_client, "/api/ups/snapshot")["ups"][0]["name"]
deep = "test.battery.start.deep"
refusal = ups_client.post("/api/ups/command", json={"ups": name, "command": deep})
assert refusal.status_code == 422
assert "DANGER" in refusal.json()["detail"]
assert ups_client.post("/api/ups/command",
json={"ups": name, "command": deep, "confirm": "DANGER"}).status_code == 200
def test_a_command_the_driver_does_not_advertise_is_refused_before_it_is_sent(
ups_client) -> None:
"""Being in the risk catalog is not permission; `LIST CMD` from the driver is (invariant 1).
`shutdown.return` is a real NUT command the catalog knows and this UPS does not offer,
so the refusal must come from the advertisement check and not from the confirmation gate.
"""
name = wait_for_payload(ups_client, "/api/ups/snapshot")["ups"][0]["name"]
response = ups_client.post("/api/ups/command",
json={"ups": name, "command": "shutdown.return"})
assert response.status_code == 400
assert "does not advertise" in response.json()["detail"]
def test_cutting_the_output_is_refused_while_the_ups_is_already_on_battery(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Invariant 1, the one that matters most: no power cut while the pack is carrying the load.
Driven through the simulator's own state machine — a quick battery test puts the pack on
battery — so the 409 comes from the same poll the buttons are drawn from, with a 0.5s
interval to keep the wait short. Nothing here can touch real hardware: `NUT_SIMULATE=1`.
"""
with build_client(tmp_path, monkeypatch, simulate=True, nut_host="127.0.0.1",
nut_allow_actions=True, nut_interval=0.5) as test_client:
name = wait_for_payload(test_client, "/api/ups/snapshot")["ups"][0]["name"]
start = test_client.post("/api/ups/command",
json={"ups": name, "command": "test.battery.start.quick"})
assert start.status_code == 200
deadline = time.time() + 6.0
card: dict[str, Any] = {}
while time.time() < deadline:
card = next(item for item in test_client.get("/api/ups/snapshot").json()["ups"]
if item["name"] == name)
if card["on_battery"]:
break
time.sleep(0.05)
assert card.get("on_battery") is True, "the quick test must put the simulated pack on battery"
refusal = test_client.post("/api/ups/command",
json={"ups": name, "command": "load.off",
"confirm": "DANGER"})
assert refusal.status_code == 409
assert "on battery" in refusal.json()["detail"]
# and the same command is still offered once the pack is back on line power
assert not any(item["command"] == "load.off" and item["enabled"]
for item in card["commands"]), "the panel must not offer a blocked button"
class ScriptedNutClient:
"""`nut.NutClient` with the socket left out: the real code path, no battery attached.
`AGENTS.md` keeps live tests read-only, so this is where the one write endpoint's real
branch gets proven. The variable names are the ones `VAR_MAP` already pins from real
upsd captures; only the replies are scripted (a fake for behaviour, per the test policy).
"""
#: set per test: an exception instance to raise from `instcmd`, e.g. a refusal
refusal: Exception | None = None
sent: list[tuple[str, str]]
logged_in: list[str]
def __init__(self, host: str, port: int, *, timeout: float = 4.0,
username: str | None = None, password: str | None = None) -> None:
self.credentials = (username, password)
async def __aenter__(self) -> ScriptedNutClient:
return self
async def __aexit__(self, *_exc: object) -> None:
return None
async def list_ups(self) -> list[dict[str, str]]:
return [{"name": "apc", "description": "APC Back-UPS RS 1500MS"}]
async def list_vars(self, ups: str) -> dict[str, str]:
return {"ups.mfr": "APC", "ups.model": "Back-UPS RS 1500MS",
"ups.status": "OL", "battery.charge": "100", "ups.load": "18",
"battery.voltage": "13.6", "input.voltage": "230",
"battery.mfr.date": "2022-04", "ups.test.result": "Ok last test passed"}
async def list_cmds(self, ups: str) -> list[str]:
return ["test.panel.start", "beeper.mute", "load.off"]
async def list_rw_vars(self, ups: str) -> list[str]:
return []
async def login(self) -> None:
assert self.credentials[0], "a command send must authenticate as a named user"
ScriptedNutClient.logged_in.append(str(self.credentials[0]))
async def instcmd(self, ups: str, command: str) -> str:
if ScriptedNutClient.refusal is not None:
raise ScriptedNutClient.refusal
ScriptedNutClient.sent.append((ups, command))
return "OK Start Panel Test"
@pytest.fixture
def real_upsd_client(monkeypatch: pytest.MonkeyPatch) -> Iterator[type[ScriptedNutClient]]:
"""Swap the module-level `NutClient` so the collector and the send use the same stand-in."""
ScriptedNutClient.sent = []
ScriptedNutClient.logged_in = []
ScriptedNutClient.refusal = None
monkeypatch.setattr(nut, "NutClient", ScriptedNutClient)
yield ScriptedNutClient
@pytest.fixture
def nut_client_app(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
real_upsd_client: type[ScriptedNutClient]) -> Iterator[TestClient]:
"""A non-simulated UPS app: real code paths, scripted upsd, 0.5s polls."""
with build_client(tmp_path, monkeypatch, nut_host="127.0.0.1", nut_user="dashboards",
nut_password="not-the-real-one", nut_allow_actions=True,
nut_interval=0.5) as test_client:
yield test_client
def test_a_real_upsd_send_authenticates_sends_and_audits(nut_client_app,
real_upsd_client) -> None:
body = wait_for_payload(nut_client_app, "/api/ups/snapshot")
assert body["simulated"] is False, "this test is about the branch that talks to upsd"
name = body["ups"][0]["name"]
response = nut_client_app.post("/api/ups/command",
json={"ups": name, "command": "test.panel.start"})
assert response.status_code == 200, response.text
assert response.json()["simulated"] is False
assert real_upsd_client.sent == [(name, "test.panel.start")]
assert real_upsd_client.logged_in, "instcmd requires a LOGIN first (upsd refuses otherwise)"
events = nut_client_app.get("/api/events").json()["events"]
assert any(event["kind"] == "action" and name in event["message"] for event in events)
def test_a_refusal_from_upsd_is_a_502_and_an_error_event(nut_client_app,
real_upsd_client) -> None:
real_upsd_client.refusal = nut.NutError("Access denied", code="ACCESS-DENIED")
name = wait_for_payload(nut_client_app, "/api/ups/snapshot")["ups"][0]["name"]
response = nut_client_app.post("/api/ups/command",
json={"ups": name, "command": "beeper.mute"})
assert response.status_code == 502
detail = response.json()["detail"]
assert "ACCESS-DENIED" in detail or "Access denied" in detail, detail
assert real_upsd_client.sent == [], "a refused command was never accepted"
events = nut_client_app.get("/api/events").json()["events"]
failures = [event for event in events if event["kind"] == "action"
and event["severity"] == "error"]
assert failures, "an upsd refusal has to leave a trace, not just an HTTP status"
def test_unknown_commands_are_refused_even_with_actions_on(ups_client) -> None:
name = wait_for_payload(ups_client, "/api/ups/snapshot")["ups"][0]["name"]
for command in ("load.format", "rm -rf /", "INSTCMD x y"):
response = ups_client.post("/api/ups/command", json={"ups": name, "command": command})
assert response.status_code == 400, command
def test_commands_for_an_unknown_ups_are_404(ups_client) -> None:
wait_for_payload(ups_client, "/api/ups/snapshot")
response = ups_client.post("/api/ups/command",
json={"ups": "not-real", "command": "beeper.mute"})
assert response.status_code == 404
def test_missing_fields_are_a_400(ups_client) -> None:
assert ups_client.post("/api/ups/command", json={"ups": ""}).status_code == 400
def test_actions_flag_off_blocks_every_command(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
with build_client(tmp_path, monkeypatch, simulate=True, nut_host="127.0.0.1") as client:
body = wait_for_payload(client, "/api/ups/snapshot")
name = body["ups"][0]["name"]
assert body["actions_enabled"] is False
response = client.post("/api/ups/command",
json={"ups": name, "command": "beeper.mute"})
assert response.status_code == 403
assert "NUT_ALLOW_ACTIONS" in response.json()["detail"]
def test_read_only_beats_the_actions_flag(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
with build_client(tmp_path, monkeypatch, simulate=True, nut_host="127.0.0.1",
nut_allow_actions=True, read_only=True) as client:
body = wait_for_payload(client, "/api/ups/snapshot")
name = body["ups"][0]["name"]
response = client.post("/api/ups/command",
json={"ups": name, "command": "beeper.mute"})
assert response.status_code == 403
commands = wait_for_payload(client, "/api/ups/commands")
assert commands["read_only"] is True
assert all(item["enabled"] is False for ups in commands["ups"]
for item in ups["commands"])
def test_ups_history_answers_for_simulated_ups(ups_client) -> None:
wait_for_payload(ups_client, "/api/ups/snapshot")
time.sleep(1.2)
body = ups_client.get("/api/ups/history", params={"metrics": "ups.charge_pct",
"range": "1h"}).json()
assert body["ups"], "the UPS page needs a name to chart before any history exists"
def response_ok(response) -> bool:
assert response.status_code == 200, response.text
return True
def test_samples_written_by_the_poller_are_queryable(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
"""The write path (poller -> store) and the read path (/api/history) must agree on names."""
db = str(tmp_path / "roundtrip.sqlite3")
store = SampleStore(db)
store.add_many([Sample("llm", "gufo", "llm.decode_tokens_per_second", 40.0, time.time() - 60),
Sample("llm", "gufo", "llm.decode_tokens_per_second", 45.0, time.time() - 30)])
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
with TestClient(app) as client:
sign_in(client)
metrics = client.get("/api/metrics", params={"source": "llm"}).json()["metrics"]
assert "llm.decode_tokens_per_second" in metrics
points = client.get("/api/history", params={"source": "llm", "host": "gufo",
"metric": "llm.decode_tokens_per_second",
"range": "15m"}).json()["points"]
assert [p["value"] for p in points] == [40.0, 45.0]
store.close()
# --------------------------------------------------------------------------- #
# The certificate authority surface (phase 15 task 04)
#
# These routes mint, re-sign, and revoke the identities every agent in the fleet is dialled with,
# so the assertions are about **stored rows and wire effects** rather than about JSON being shaped
# nicely: a read that issued a certificate, a typo that wrote a row, a revocation that did not
# reach the collector, and a key that left `meta` are the four ways this surface can be wrong while
# every status code still reads 200.
#
# The fleet here is loopback ports nobody listens on. That is deliberate: nothing on this surface
# is allowed to need a live agent, so a refused connection is both the whole environment and the
# control that says "a dial was attempted, and it was not this".
# --------------------------------------------------------------------------- #
#: Three configured hosts, none of them reachable, on the scheme a protected exporter is dialled
#: by. The URL host is also what a rotated certificate's SAN must carry, so these are the SANs the
#: assertions below read.
DEAD_TARGETS = (Target(name="deskwork", url="https://127.0.0.1:1"),
Target(name="driveripper", url="https://127.0.0.1:2"),
Target(name="gateway", url="https://127.0.0.1:3"))
#: The whole `GET /api/certs` document and one agent row, pinned as a table so a panel that
#: retires can never shrink the payload (`/api/disks`' idiom) and task 05's page has a contract.
CERT_DOCUMENT_KEYS = {"root", "client", "agents"}
CERT_TIMESERIAL_KEYS = {"serial", "not_after"}
CERT_AGENT_KEYS = {"name", "configured", "serial", "not_after", "status", "bundle_rev",
"deployed_bundle_rev", "san"}
def cert_document(client: TestClient) -> dict[str, Any]:
"""`GET /api/certs`, with the status asserted: a refusal here would pass a `.get()` chain."""
response = client.get("/api/certs")
assert response.status_code == 200, response.text
body = response.json()
assert set(body) == CERT_DOCUMENT_KEYS, "the CA document grew or lost a top-level key"
for block in ("root", "client"):
assert set(body[block]) == CERT_TIMESERIAL_KEYS, block
assert int(body[block]["serial"]) > 0, "a serial is the decimal text of a positive int"
assert body[block]["not_after"] > time.time(), "a stored CA may not already be expired"
return body
def cert_agent_rows(client: TestClient, name: str | None = None) -> list[dict[str, Any]]:
"""The agent rows, every one of them shaped, optionally just one host's."""
rows = cert_document(client)["agents"]
for row in rows:
assert set(row) == CERT_AGENT_KEYS, sorted(row)
if name is None:
return rows
return [row for row in rows if row["name"] == name]
def cert_context(client: TestClient) -> Any:
"""The app's `AppContext`.
`TestClient.app` is typed as the ASGI callable it wraps, so the one cast lives here instead of
one `# type: ignore` at every assertion that needs the store or a job.
"""
return client.app.state.ctx # type: ignore[attr-defined]
def cert_meta(store: SampleStore) -> dict[str, str]:
"""Every `certs.*` row, for the before/after comparisons that prove a route wrote nothing.
Coalesced to `""` because the comparison is "did any byte of these five rows move", and an
absent row is the empty answer to that question rather than a second kind of nothing.
"""
return {key: store.get_meta(key) or "" for key in certs.META_KEYS}
def revoked_serials(bundle: str) -> list[str]:
"""The denylist of a bundle, as the agent reads it: the `# REVOKED <serial>` lines."""
return [line.removeprefix(certs.REVOKED_LINE).strip()
for line in bundle.splitlines() if line.startswith(certs.REVOKED_LINE)]
#: One PEM block out of a bundle, by text — the same read task 02's agent does with stdlib.
_PEM_BLOCK = re.compile(r"-----BEGIN [A-Z0-9 ]+-----.*?-----END [A-Z0-9 ]+-----\n", re.DOTALL)
def bundle_certs(bundle: str) -> list[x509.Certificate]:
"""The two certificates of a bundle, in the order the format writes them.
`[0]` is the agent's own server certificate and `[1]` the root it chains to (`certs.py`'s
`_bundle_text` order: metadata lines, agent cert, agent key, root cert). Reading them out of the
served bytes rather than trusting the response's echo is what makes "did the rotation reach the
artifact?" a checked claim.
"""
blocks = [block for block in _PEM_BLOCK.findall(bundle) if "CERTIFICATE" in block]
assert len(blocks) == 2, f"a bundle is two certificates and a key, got {len(blocks)}"
return [x509.load_pem_x509_certificate(block.encode()) for block in blocks]
def bundle_of(client: TestClient, name: str) -> str:
"""The served bundle, asserted to still be a bundle (2 certs, 1 key) before it is compared."""
response = client.get(f"/api/certs/{name}/bundle")
assert response.status_code == 200, response.text
assert response.headers["content-type"].startswith("application/x-pem-file")
assert "no-store" in response.headers["cache-control"], "a private key may not be cached"
blocks = re.findall(r"-----BEGIN [A-Z0-9 ]+-----", response.text)
assert blocks.count("-----BEGIN CERTIFICATE-----") == 2, blocks
assert blocks.count("-----BEGIN PRIVATE KEY-----") == 1, blocks
return response.text
def stored_bundle(store: SampleStore, name: str) -> str:
"""The bundle exactly as the `certs.agents` row holds it."""
row: dict[str, Any] = json.loads(store.get_meta(certs.META_AGENTS) or "{}")
return str(row[name]["bundle"])
def cert_events(client: TestClient) -> list[dict[str, Any]]:
return client.get("/api/events", params={"source": "certs"}).json()["events"]
def cert_hosts(count: int) -> tuple[Target, ...]:
return DEAD_TARGETS[:count]
@pytest.mark.parametrize("hosts", [0, 1, 3], ids=["none", "one", "three"])
def test_the_cert_list_is_the_stored_table_for_zero_one_and_three_hosts(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, hosts: int) -> None:
"""The list works with no fleet at all, and says *unissued* rather than omitting a host.
The CA exists whether or not `HOST_TARGETS` does — a fresh database boots with a root and a
client certificate — so this is not the `/api/disks` "not configured" surface and does not 404.
A configured host with nothing issued still gets a row (invariant 3: a machine that is deployed
and not yet installed is not an absent machine), with `None` and not `0` where there is no
certificate (rule 2).
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(hosts),
host_interval=3600.0) as client:
rows = cert_agent_rows(client)
assert len(rows) == hosts, "one row per configured host, and no more"
assert [row["name"] for row in rows] == sorted(row["name"] for row in rows), (
"one order in the payload and one in the table")
for row in rows:
assert row["configured"] is True
assert row["status"] == "unissued", "nothing has been signed for it yet"
assert row["serial"] is None and row["bundle_rev"] is None, "no invented zeros"
assert row["deployed_bundle_rev"] is None, "nobody has answered, so nobody reported"
assert row["san"] == [], "no names on record, not a certificate with no names"
def test_the_cert_list_carries_the_rev_the_agent_reported(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""`deployed_bundle_rev` is what the host said about itself, not what the CA hopes.
Task 05's "stale bundle" sentence is a comparison of two numbers that come from two different
places: the row this dashboard issued and the `agent` block of the last `system` payload. Both
halves are pinned here, including the direction that matters — issued 2 against deployed 1 is
the operator's "install the new bundle" case, and it has to be visible rather than smoothed
over.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(1),
host_interval=3600.0) as client:
assert client.post("/api/certs/deskwork/rotate").status_code == 200
job = cert_context(client).hub.jobs["host:deskwork"]
assert cert_agent_rows(client, "deskwork")[0]["deployed_bundle_rev"] is None, (
"a host that has never answered reports nothing")
job.state.payload = {"name": "deskwork", "agent": {"tls": True, "bundle_rev": 1}}
assert cert_agent_rows(client, "deskwork")[0]["deployed_bundle_rev"] == 1
assert client.post("/api/certs/deskwork/rotate").json()["bundle_rev"] == 2
row = cert_agent_rows(client, "deskwork")[0]
assert (row["bundle_rev"], row["deployed_bundle_rev"]) == (2, 1), "the stale pair, both"
# A token-mode agent publishes no `agent.tls` block at all, and that is `n/a`, not rev 0.
job.state.payload = {"name": "deskwork", "agent": {"version": "1.0.0"}}
assert cert_agent_rows(client, "deskwork")[0]["deployed_bundle_rev"] is None
def test_an_entry_for_a_host_that_left_the_config_is_listed_and_not_writable(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A row issued for a machine that has gone is a key still sitting in the table.
Listing it is the honest answer (`/certs` is where you learn you still hold one); refusing to
rotate or revoke it is the same invariant from the other side — `HOST_TARGETS` is what this
dashboard dials, and a name it no longer dials is not a name to install a bundle on. The row is
written straight to the store because that is the only way this state arises: a host removed
from the config, never a route that created it.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(1),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
certs.issue_agent_cert(store, certs.read_state(store), "old-host", ["10.9.8.7"])
rows = cert_agent_rows(client)
assert [row["name"] for row in rows] == ["deskwork", "old-host"]
ghost = rows[1]
assert ghost["configured"] is False, "listed, but this dashboard does not dial it"
assert ghost["status"] == certs.ACTIVE and ghost["bundle_rev"] == 1
assert ghost["deployed_bundle_rev"] is None, "no job means no report, not rev 0"
assert client.post("/api/certs/old-host/rotate").status_code == 404
assert client.post("/api/certs/old-host/revoke").status_code == 404
assert cert_events(client) == [], "a refusal is not an audit line"
def test_a_cert_route_for_an_unknown_name_dials_nothing_and_writes_nothing(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The invariant-3 gate on the per-host routes, taken before anything can have a side effect.
`not-installed` is refused in the first line of each route, so no client is built, no store row
is written, and no key is minted. All three are measured: the two network spies raise, `set_meta`
is watched for any `certs.*` write, and the five rows are compared byte for byte. A typo that
minted a certificate would be invisible on every status code and permanent in the table.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(1),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
calls: list[str] = []
written: list[str] = []
async def spy_get(self: Any, url: Any, **kwargs: Any) -> Any:
calls.append(f"GET {url}")
raise AssertionError(f"an unconfigured cert name dialled {url}")
def spy_fetch(*args: Any, **kwargs: Any) -> Any:
calls.append("fetch_snapshot")
raise AssertionError("an unconfigured cert name fetched an agent snapshot")
real_set_meta = SampleStore.set_meta
def spy_set_meta(self: SampleStore, key: str, value: str) -> None:
written.append(key)
real_set_meta(self, key, value)
monkeypatch.setattr(httpx.AsyncClient, "get", spy_get)
monkeypatch.setattr("dashboards.collectors.fetch_snapshot", spy_fetch)
monkeypatch.setattr(SampleStore, "set_meta", spy_set_meta)
before = cert_meta(store)
for response in (client.get("/api/certs/not-configured/bundle"),
client.post("/api/certs/not-configured/rotate"),
client.post("/api/certs/not-configured/revoke")):
assert response.status_code == 404, response.text
body = response.json()
assert body["configured"] is False
assert "HOST_TARGETS" in body["detail"], "the sentence names the fix"
assert calls == [], "the gate is in front of the network, not behind it"
assert [key for key in written if key.startswith("certs.")] == [], "a write happened"
assert cert_meta(store) == before, "the stored CA and agent table did not move"
assert cert_events(client) == [], "and no audit line was written for a name that is not here"
# …and the same route still works for the host that *is* configured, so the refusal above
# is the gate and not a broken router.
assert client.post("/api/certs/deskwork/rotate").status_code == 200
def test_rotating_a_host_mints_the_bundle_it_asks_for(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Rotate is also *issue*: first call is `bundle_rev` 1, and the SAN is the config's address.
The SAN assertion is the one a rollout fails on silently — a certificate that does not name the
address the dashboard dials is a handshake error and a red source, not a warning — so it is read
out of the served bundle's own certificate rather than out of the response's echo.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(1),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
body = client.post("/api/certs/deskwork/rotate").json()
assert body["ok"] is True and body["status"] == certs.ACTIVE
assert body["bundle_rev"] == 1 and body["serial"] and body["not_after"] > time.time()
assert body["san"] == ["127.0.0.1", "deskwork"], "the dialled address and the name"
cert = bundle_certs(bundle_of(client, "deskwork"))[0]
names = cert.extensions.get_extension_for_class(
x509.SubjectAlternativeName).value.get_values_for_type(x509.IPAddress)
assert names == [ipaddress.ip_address("127.0.0.1")], "an IP SAN, or an IP dial fails"
assert cert.subject == x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "deskwork")])
events = cert_events(client)
assert len(events) == 1 and events[0]["kind"] == "cert-rotated"
assert events[0]["host"] == "deskwork" and events[0]["message"] == (
"agent certificate rotated for deskwork")
assert events[0]["data"] == {"bundle_rev": 1}
second = client.post("/api/certs/deskwork/rotate").json()
assert second["bundle_rev"] == 2 and second["serial"] != body["serial"]
# The new identity is not on the denylist: rotation replaces an identity, it does not strike
# one out, and a bundle that listed its own serial would refuse the client it ships.
bundle = bundle_of(client, "deskwork")
assert revoked_serials(bundle) == [], revoked_serials(bundle)
assert second["serial"] not in bundle.splitlines(), "no denylist line for the live serial"
assert stored_bundle(store, "deskwork") == bundle, "served bytes are the stored bytes"
def test_revoking_a_host_marks_the_table_and_the_next_poll_refuses_without_a_dial(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The API half of the revocation chain: this route is what task 03's refusal reads.
The control is the sentence from *before* the click (`unreachable`, which is what a dial to a
dead port says), and after it the poll has to say `agent certificate revoked` with a spy on
`httpx.AsyncClient.get` proving no socket was opened. The stored denylist is the other half: an
agent holding a bundle that omits the serial keeps accepting the client it was given.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(1),
host_interval=0.05, disks_interval=0.05) as client:
store: SampleStore = cert_context(client).store
wait_for_source_error(client, "host:deskwork", "unreachable")
serial = client.post("/api/certs/deskwork/rotate").json()["serial"]
body = client.post("/api/certs/deskwork/revoke").json()
assert body["ok"] is True and body["status"] == certs.REVOKED
assert body["serial"] == serial, "revocation marks the identity it has, it does not replace"
assert body["hint"] and "next poll" in body["hint"]
calls: list[str] = []
async def spy_get(self: Any, url: Any, **kwargs: Any) -> Any:
calls.append(str(url))
raise AssertionError(f"a revoked agent was dialled at {url}")
monkeypatch.setattr(httpx.AsyncClient, "get", spy_get)
for key in ("host:deskwork", "disks:deskwork"):
error = wait_for_source_error(client, key, "agent certificate revoked")
assert "unreachable" not in error
assert calls == []
assert revoked_serials(bundle_of(client, "deskwork")) == [serial]
events = cert_events(client)
assert [row["kind"] for row in events] == ["cert-revoked", "cert-rotated"]
assert events[0]["message"] == "agent certificate revoked for deskwork"
assert certs.agent_status(store, "deskwork") == certs.REVOKED, (
"the collector's own read agrees with the row the route wrote")
def test_root_rotation_reissues_every_host_and_answers_restart_required(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""One action re-arms the fleet — and says the part it cannot do from inside the process.
`restart_required` is not decoration: the CA and the client pair are startup material, so the
table is new immediately while the sockets keep the old identity. The page (task 05) prints
that sentence because the API answered it, and the alternative is an operator who rotated a
root and watches a fleet that still chains to the previous one.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(3),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
for target in DEAD_TARGETS:
assert client.post(f"/api/certs/{target.name}/rotate").status_code == 200
before = cert_document(client)
rows_before = {row["name"]: row for row in before["agents"]}
root_pem_before = store.get_meta(certs.META_ROOT_CERT) or ""
key_before = store.get_meta(certs.META_ROOT_KEY) or ""
assert root_pem_before and key_before, "both halves of the CA exist before the rotation"
body = client.post("/api/certs/root/rotate").json()
assert body["ok"] is True and body["restart_required"] is True
assert body["agents"] == 3
assert body["root"]["serial"] != before["root"]["serial"], "a fresh root"
after = cert_document(client)
assert after["root"] == body["root"], "the list reports what the rotation wrote"
assert after["client"]["serial"] != before["client"]["serial"], "and a fresh client leaf"
for row in after["agents"]:
was = rows_before[row["name"]]
assert row["bundle_rev"] == was["bundle_rev"] + 1, "every host reissued, rev bumped"
assert row["serial"] != was["serial"] and row["status"] == certs.ACTIVE
bundle = bundle_of(client, row["name"])
leaf, root = bundle_certs(bundle)
assert leaf.serial_number == int(row["serial"]), "the served bundle is the listed one"
assert root.serial_number == int(after["root"]["serial"]), (
"and it chains to the authority the list now reports")
assert revoked_serials(bundle) == [], "the denylist resets with the generation"
assert store.get_meta(certs.META_ROOT_KEY) != key_before
assert store.get_meta(certs.META_ROOT_CERT) != root_pem_before
assert root_pem_before not in "".join(cert_meta(store).values()), (
"the old root is gone")
events = cert_events(client)
assert events[0]["kind"] == "cert-root-rotated"
assert events[0]["message"] == (
"root certificate rotated — every agent certificate reissued, restart required")
assert events[0]["data"] == {"agents": 3}
def test_the_bundle_route_names_a_file_and_changes_when_the_identity_does(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Byte-identical to the stored row, an `attachment`, and different bytes after a reissue.
This is the installer's input (task 06), so the claim is about bytes and not about a bundle
that merely re-verifies: `install-agent.sh` is handed the file and compares nothing else.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(1),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
missing = client.get("/api/certs/deskwork/bundle")
assert missing.status_code == 404, "nothing issued yet, so nothing to download"
body = missing.json()
assert body["configured"] is True and body["issued"] is False, "not the same 404"
assert "rotate" in body["detail"], "and the answer names the way to get one"
first = client.post("/api/certs/deskwork/rotate").json()["serial"]
response = client.get("/api/certs/deskwork/bundle")
assert response.headers["content-disposition"] == (
'attachment; filename="agent-deskwork.bundle"')
assert response.text == stored_bundle(store, "deskwork")
client.post("/api/certs/deskwork/rotate")
second = client.post("/api/certs/deskwork/rotate").json()["serial"]
assert second != first
updated = client.get("/api/certs/deskwork/bundle")
assert updated.text != response.text, "a reissue changes the served bytes"
assert updated.text == stored_bundle(store, "deskwork")
def test_no_certificate_material_leaves_meta_through_the_cert_surface(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""invariant 6 on the API that owns the keys: serials and dates go out, PEMs do not.
Every write here is exercised first, so the grep is over a table that has been rotated and
revoked rather than a pristine one, and it covers both the payloads a browser reads and the
stored event rows those payloads are built from — an event row is kept forever, which is what
makes a PEM in a message permanent rather than transient.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(2),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
client.post("/api/certs/deskwork/rotate")
client.post("/api/certs/driveripper/rotate")
client.post("/api/certs/driveripper/revoke")
client.post("/api/certs/root/rotate")
secrets = [value for value in cert_meta(store).values() if value]
assert any("PRIVATE KEY" in value for value in secrets), "the table really does hold keys"
for path in ("/api/certs", "/api/health", "/api/events", "/api/summary", "/api/config"):
text = client.get(path).text
for marker in ("BEGIN CERTIFICATE", "PRIVATE KEY", "BUNDLE-REV"):
assert marker not in text, f"{path} served {marker}"
assert client.get("/api/health").json()["db"], "health still answers, keys or no keys"
for event in store.recent_events(50, source="certs"):
assert "BEGIN" not in event.message and "PRIVATE KEY" not in event.message
assert not any(secret in json.dumps(event.as_json()) for secret in secrets)
def test_a_host_named_root_cannot_be_rotated_past_the_certificate_authority(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""`/api/certs/root/rotate` and `/api/certs/{name}/rotate` are one URL for a host called root.
The CA route is registered first, so it wins — which means the wrong button would rotate the
whole authority instead of one host's leaf. Refusing with the reason is the only honest answer:
the alternative is a fleet-wide outage behind a click that looked like routine maintenance.
"""
with build_client(tmp_path, monkeypatch,
host_targets=(Target(name="root", url="https://127.0.0.1:1"),),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
before = cert_document(client)["root"]["serial"]
refused = client.post("/api/certs/root/rotate")
assert refused.status_code == 409, refused.text
assert "rename" in refused.json()["detail"], "the sentence says what to do"
assert cert_document(client)["root"]["serial"] == before, "and the CA did not move"
assert cert_events(client) == []
assert cert_meta(store)[certs.META_AGENTS] == "{}", "nothing was issued for the name either"
def test_a_read_of_the_cert_surface_issues_nothing_and_writes_nothing(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A GET that mints a certificate is the surprise this route must never be.
`certs.ensure_certs` is the bootstrapping read and `certs.read_state` is the request-time one;
making `ensure_certs` explode is what proves the difference rather than asserting it in prose,
and the meta rows are compared byte for byte across three reads.
"""
with build_client(tmp_path, monkeypatch, host_targets=cert_hosts(1),
host_interval=3600.0) as client:
store: SampleStore = cert_context(client).store
before = cert_meta(store)
def explode(*args: Any, **kwargs: Any) -> Any:
raise AssertionError("a GET tried to bootstrap the certificate authority")
monkeypatch.setattr(certs, "ensure_certs", explode)
assert cert_agent_rows(client)
assert cert_document(client)["root"]["serial"] == str(
x509.load_pem_x509_certificate(
str(before[certs.META_ROOT_CERT]).encode()).serial_number)
# A bundle that has not been issued is still a 404 rather than a mint, on a read too.
assert client.get("/api/certs/deskwork/bundle").status_code == 404
assert cert_meta(store) == before, "three reads, no write"
assert cert_events(client) == []
# --------------------------------------------------------------------------- #
# phase 14 task 04: the endpoint budgets
#
# `store_perf.BUDGETS` is the one table of ceilings (it is what the probe's report and the notes
# render, so a ceiling relaxed in one place is relaxed everywhere). The ceilings are 2-3x the
# measured "after" on the seeded production-shaped store, which makes them a regression gate
# rather than a stopwatch: the `/system` page's own chart request costs ~2 ms and still gets
# 100 ms, while putting a full-table scan back on a per-request path crosses by three orders of
# magnitude. Wall time lives behind `--store-perf` for that reason; what runs in every `uv run
# pytest` is the key-name pin below, because a budget that silently stops being priced is as
# useless as a budget that is crossed.
# --------------------------------------------------------------------------- #
def test_every_http_budget_names_a_route_the_probe_prices(tmp_path: Path) -> None:
"""The stale-pointer check for the HTTP half, on a throwaway store.
`measure_http` is what the priced test runs, and it owns the request shapes; if a key there is
renamed, or a route stops being measured, the priced test would report the gap rather than the
cost — which is why this asserts the key set here, cheaply, on every run.
"""
report = store_perf.measure_budget_http(tmp_path / "budget-keys.sqlite3",
hosts=("driveripper",), metrics=("cpu.usage_pct",))
assert set(report["http"]) == set(store_perf.HTTP_MEASURED_KEYS), (
"the HTTP pass and the budget table no longer describe the same requests")
assert store_perf.missing_budget_keys(report, ("http",)) == []
# and the request the budgets describe is the one the task names: 3 hosts x 9 series
nine = store_perf.HISTORY_METRICS
assert len(nine) == 9, "the 90 d budget is priced against a 3x9 request, not some other shape"
def test_metrics_stops_scanning_for_series_names_once_the_hub_is_polling(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The live shape behind one budgeted endpoint, guarded by statements rather than a clock.
Task 04's watch caught `/api/metrics` answering with its full `SELECT DISTINCT` scan on
practically every call of the running server (535 ms, measured): task 02's cache was invalidated
by every sample write, and the hub writes every 2-300 s, so the cache never lived long enough to
serve. The store-level half of this lives in `tests/test_store.py`; this is the half that says
the *endpoint* is what stops scanning, on the app the operator runs.
"""
db = str(tmp_path / "metrics-busy.sqlite3")
store = SampleStore(db)
store.add_many([Sample("host", "driveripper", "cpu.usage_pct", 1.0, time.time()),
Sample("host", "deskwork", "cpu.usage_pct", 1.0, time.time())])
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
pbkdf2_iterations=FAST_PBKDF2), store)
try:
with TestClient(app) as http:
sign_in(http)
assert "cpu.usage_pct" in http.get("/api/metrics", params={"source": "host"}).json()["metrics"]
# a poll of series the store already has, then the same request with the log open
store.add_many([Sample("host", "driveripper", "cpu.usage_pct", 2.0, time.time()),
Sample("host", "deskwork", "cpu.usage_pct", 2.0, time.time())])
seen: list[str] = []
# the statement log is the test: `SampleStore._db` is the handle the request runs on
store._db.set_trace_callback(seen.append)
try:
assert "cpu.usage_pct" in http.get("/api/metrics",
params={"source": "host"}).json()["metrics"]
finally:
store._db.set_trace_callback(None)
assert [s for s in seen if "DISTINCT metric" in s] == [], (
f"`/api/metrics` went back to the table while nothing changed: {seen}")
# and a source that starts publishing something new is in the very next answer
store.add("host", "driveripper", "gpu.busy_pct@0", 1.0)
assert "gpu.busy_pct@0" in http.get("/api/metrics",
params={"source": "host"}).json()["metrics"]
finally:
store.close()
@pytest.mark.store_perf
def test_the_budgeted_http_paths_fit_their_pinned_margins(perf_store_path: Path) -> None:
"""`/api/health`, `/api/metrics`, `/api/system/history` (1 h and 90 d), `/api/summary`.
Priced through the real ASGI gate with a signed-in session, on the seeded 6.4 M-row store —
the same call the operator's browser makes, in-process so no network is measured. This is the
line `TODO.md:7` is about: `/api/health` used to answer in 1.27 s because it counted the whole
`sample` table on every poll, on every tab.
"""
report = store_perf.measure_budget_http(perf_store_path)
print()
print(store_perf.render_budgets_markdown(store_perf.budget_rows(report, ("http",))))
assert store_perf.budget_violations(report, ("http",)) == []