"""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 re import time from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path from typing import Any import pytest from fastapi import HTTPException from fastapi.testclient import TestClient from dashboards import alerts from dashboards.api import RANGE_SECONDS, parse_range from dashboards.app import create_app from dashboards.config import Settings, Target from dashboards.severity import classify from dashboards.sources import nut from dashboards.sources.llm import HISTORY_METRICS from dashboards.store import Event, Sample, SampleStore # refused immediately, and never dials out of the test environment DEAD_URL = "http://127.0.0.1:1" @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, **overrides) store = SampleStore(db) client = TestClient(create_app(settings, store)) client.__enter__() try: 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_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", host_targets=(Target("driveripper", DEAD_URL),)), store) test_client = TestClient(app) test_client.__enter__() try: 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, nothing past a threshold, no UPS configured. 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. """ 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) async def collect() -> Result: return Result(payload=payload, samples=[]) job = test_client.app.state.ctx.hub.jobs["host:driveripper"] # type: ignore[attr-defined] job.fn = collect # 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) job.state.payload = payload job.state.ok = True job.state.error = None job.state.consecutive_failures = 0 job.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]) assert set(body) == {"ts", "system", "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. SUMMARY_CONTRACT = ("system.enabled", "system.fleet", "system.hosts", "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()) batteries = {item["name"] for item in dig(body, "ups.ups")} assert batteries == ({"apc", "cyberpower"} if populated else set()) 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"] # ---- 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["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"] == [] @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"] # ---- 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, host_targets=tuple(targets)), store) with TestClient(app) as 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, 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: 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/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 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, gufo_url="http://SEKRIT-ENGINE-TOKEN@deskwork:8731", vllm_url="http://driveripper:8000") store = SampleStore(db) with TestClient(create_app(settings, store)) as 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() 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"] == [] # ---- pages ---------------------------------------------------------------- # @pytest.mark.parametrize("path,title", [ ("/", "overview"), ("/system", "system"), ("/llm", "llm"), ("/ups", "ups"), ]) 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.""" for path in ("/", "/system", "/llm", "/ups"): 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 ("/static/dash.css", "/static/app.js", "/static/charts.js", "/static/page-system.js", "/static/page-llm.js", "/static/page-ups.js", "/static/page-index.js"): assert client.get(asset).status_code == 200, asset # ---- 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-system.js", "/static/page-llm.js", "/static/page-ups.js") def _css(client: TestClient) -> str: return client.get("/static/dash.css").text 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) 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, "/", "/system", "/llm", "/ups"): 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-index.js", "page-system.js", "page-llm.js", "page-ups.js"): 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" def test_gpu_hero_footnote_prints_the_temperature_the_agent_read(client) -> None: """TODO.md L9: the GPU temperature is glanceable on the hero row, not only in the table. `gpus[].temp_c` is the agent's max over that card's hwmon labels (`sources/host.py`, pinned by `tests/test_system_contract.py`), so the hero needed no new field and the `gpu.temp_c@N` history series kept its name. The null guard *is* the behaviour, so that is what the assertion pins (there is no JS test runner — a locked anchor): a card whose driver exposes no temperature prints none, because `n/a` is for numbers the card is supposed to have and a `0 °C` would be a cool GPU that does not exist. """ js = client.get("/static/page-system.js").text sub = js[js.index("function gpuSub("):js.index("function renderGpuSlot(")] assert "gpu.temp_c" in sub, "the GPU hero stopped printing the temperature" assert "if (gpu.temp_c != null)" in sub, "the temperature lost its null guard" assert sub.count("°C") == 1, ( "a second °C in gpuSub is a branch printing a temperature nobody measured" ) # "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_system_host_card_is_six_heroes_without_per_core(client) -> None: """Each host card leads with exactly six heroes: 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*). """ js = client.get("/static/page-system.js").text card = js[js.index("function hostCard("):js.index("function renderHostCard(")] slots = re.findall(r'
]*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 `