"""The `/system` hero-card payload contract, shared by the hermetic and the live tests. Phase 03 makes `/system` one panel per host with six big colour-coded numbers. The paths below are the payload fields those cards are allowed to read; they were read out of `parse_host_snapshot()` and are pinned hermetically in `tests/test_system_contract.py` (driven from `tests/fixtures/agent_*.json`) and against the real fleet in `tests/test_live_fleet.py::test_every_host_serves_the_hero_card_contract`. Keeping the list here rather than in one test file means the live assertions cannot drift away from the fixture assertions: there is one table, and it is the thing under test. =========================== ========================================================== Card Path in `/api/system/snapshot` `hosts[]` =========================== ========================================================== cpu % ``cpu.usage_pct`` (+ ``cpu.severity.usage_pct``) mem % ``memory.used_pct`` (+ ``memory.severity.used_pct``) cpu temp ``cpu.temp_c`` (+ ``cpu.severity.temp_c``) disk usage % ``disks.worst_used_pct`` + ``disks.worst_mount`` (+ verdict) graphics card ``gpus[].model/busy_pct/vram_*/mem_used_pct/temp_c`` (no verdict) network down/up ``network.rx_bytes_per_s`` / ``tx_bytes_per_s`` (no verdict) =========================== ========================================================== CPU temp comes from ``cpu.temp_c`` rather than ``temperatures.cpu_c``: both are the agent's own ``cpu_temperature_celsius``, and ``cpu.temp_c`` is the field ``/api/summary`` flattens into its fleet row, so the card and the overview cannot drift apart. Deliberately *not* card metrics (legibility, not missing data — the series stay in SQLite): per-core usage (``cpu.usage_pct@coreN``), swap, the process table, the raw sensor list, and per-interface rows. Network carries no ``severity`` on purpose; throughput has no good/bad, so ``severity.THRESHOLDS`` has no ``net.*`` row. GPU rows carry no ``severity`` either (phase 06): these are LLM servers, a 95 %-busy card is the job and not a fault, so ``THRESHOLDS`` has no ``gpu.*`` row and the card prints its numbers uncoloured at every utilisation — the *absence* of the dict is pinned below, the way ``network``'s is. ``gpus[].temp_c`` is on the card because the number worth reading *while* a card is busy is how hot it is (phase 06, TODO.md L9). It is the agent's **max over that card's hwmon labels** — the same max the agent already computes and the same value stored as ``gpu.temp_c@N`` — so the hero prints a measurement rather than a new field, and the value may honestly be ``None`` on a box whose driver exposes no temperature input. """ from __future__ import annotations from typing import Any #: Card metric -> dotted payload paths. Present is the contract; a value may be ``None`` #: on a host that genuinely does not have the number (gateway reports no CPU temperature). CARD_METRICS: dict[str, tuple[str, ...]] = { "cpu %": ("cpu.usage_pct", "cpu.severity.usage_pct"), "mem %": ("memory.used_pct", "memory.severity.used_pct"), "cpu temp": ("cpu.temp_c", "cpu.severity.temp_c", "temperatures.cpu_c"), # the mount is what makes the percentage actionable: 88 % used — of which disk? "disk %": ("disks.worst_used_pct", "disks.worst_mount", "disks.severity.worst_used_pct"), "network down/up": ("network.rx_bytes_per_s", "network.tx_bytes_per_s"), } #: Per-GPU paths, checked on every row of `gpus[]` — and on none of them when the list is #: empty, which is how a host without a GPU stays honest (no card, not a card at 0 % busy). GPU_METRICS: tuple[str, ...] = ( "model", "busy_pct", "vram_used_bytes", "vram_total_bytes", "vram_used_pct", # the honest APU view: vram + GTT, because a Strix Halo keeps the model in GTT "mem_used_pct", # the hero's footnote: the agent's max over this card's hwmon labels (None is legal — # a driver with no temperature input must not be rendered as 0 °C) "temp_c", ) #: `network` must never grow a `severity` dict (see the module docstring). SEVERITY_FREE_SECTIONS: tuple[str, ...] = ("network",) #: The host shapes the captures in `tests/fixtures/` cover, and therefore the shapes a card #: must handle: a discrete GPU, an APU (VRAM *and* GTT), and no GPU at all. A new capture #: has to be added here — `test_the_contract_covers_every_captured_host` fails otherwise. HOST_SHAPES: tuple[str, ...] = ("driveripper", "deskwork", "gateway") def dig(node: Any, path: str) -> Any: """Follow a dotted payload path, failing with the path in the message. A missing key is a contract break; a ``None`` value is not (that renders `n/a`), which is why this returns the value instead of asserting on it. """ walked = "" for part in path.split("."): assert isinstance(node, dict), f"{path}: hit a non-dict at {walked or 'payload'}" assert part in node, ( f"{path}: no key {part!r} under {walked or 'payload'} — a hero card would print " f"n/a for a number the API used to serve" ) node = node[part] walked = f"{walked}.{part}" if walked else part return node def assert_card_contract(payload: Any) -> None: """Every card path exists on this host payload, GPU rows included.""" for paths in CARD_METRICS.values(): for path in paths: dig(payload, path) gpus = payload.get("gpus") assert isinstance(gpus, list), "gpus must be a list — [] when the agent reports no GPU" for gpu in gpus: for path in GPU_METRICS: dig(gpu, path) assert "severity" not in gpu, ( "gpus[].severity would grade utilisation on a fleet built to be busy — " "severity.THRESHOLDS has no gpu.* row and a busy GPU is the job, not a fault" ) for section in SEVERITY_FREE_SECTIONS: block = payload.get(section) assert isinstance(block, dict), f"{section} must be a dict" assert "severity" not in block, ( f"{section}.severity would be a colour nobody measured — throughput has no " f"good/bad and severity.THRESHOLDS has no {section}.* row" )