phase: 21_page_cleanups

**Phase 21 final verification pass** (all 3 task files already in `complete/`, working tree carries the implementation)

- **Verified** task 01: `#pause`/`Space`/`setPaused`/`_paused` gone — `grep -ri pause src/dashboards/web/` empty, 0 pause hits in all 8 served pages + 10 static assets; re-ran `data/logs/21-task01-nopause.cjs` → 30/30 PASS (hidden tab: 0 data fetches in 22 s, one poll 2 ms after return).
- **Verified** task 02: re-ran `21-task02-gpu-values.cjs` on the live fleet → all PASS (deskwork/driveripper show Model/VRAM/Pool/Temp at 16.3 px; gateway = 5 heroes, slot `hidden`, no `n/a`; hero order unchanged; GPU history chart still builds per host).
- **Verified** task 03: re-ran `21-task03-scroll.cjs` → 14/14 PASS (`class="data scroll"`, scrollWidth 1021 vs clientWidth 292 at 360 px, Rotate/Revoke/bundle reachable and hit-testable; unchanged at 1280 px). Screenshots refreshed in `data/logs/`.
- **Gate**: `uv run pytest` → **1471 passed, 15 skipped**; `--cov --cov-fail-under=94` → **98.94%**; `ruff check src tests agent` clean; `pyright` **0 errors**; `bash .agents/validate.sh` → **EXIT 0** (incl. `node --check`); `uv run pytest -m live --live` → **11 passed**; `/api/health` all 9 sources `ok`.
- **Docs fixed** (pre-existing + this phase): "all **eight** signed-in pages" was a miscount of `MARKUP_PAGES` (7 = 5 dashboards + `/settings` + `/certs`; `/login` is the 8th, unsigned) → corrected in `README.md`, `AGENTS.md` (×2) and the `tests/test_api.py` docstring/comment; `AGENTS.md` Commands hermetic tally 1470 → **1471**.
- **Docs reviewed, consistent**: Quickstart/commands/flags (`install-agent.sh`, `capture-fixtures.sh`, `dev-server.sh`), the env table vs `config.py`, every documented route vs the served route table, masthead/`/system` GPU/`/certs` scroll descriptions vs code, and all `data/logs/*` + `tests/fixtures/*` references.
- **Deviation**: `validate.sh`'s advisory `fastapi 0.153.0 → 0.156.4` left alone — no dependency was touched this phase, so AGENTS rule 8 ("check after touching dependencies") is not triggered; upgrading here would be an unrelated behaviour change.
- No live-infra changes made (repo-side + browser verification only).
- **Next pending phase**: `22_disks_hide_unconfigured`.
This commit is contained in:
2026-10-21 01:44:52 -04:00
parent 973b7299fe
commit 5e267737dc
36 changed files with 830 additions and 139 deletions
+7 -4
View File
@@ -35,8 +35,10 @@ utilisation — the *absence* of the dict is pinned below, the way ``network``'s
``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.
``gpu.temp_c@N`` — so the card prints a measurement rather than a new field, and the value
may honestly be ``None`` on a box whose driver exposes no temperature input (phase 21 task
02 made it one of the card's four labelled values, and the GPU-less host that has no card at
all prints no values at all — see ``GPU_VALUE_CELLS`` in ``tests/test_api.py``).
"""
from __future__ import annotations
@@ -63,8 +65,9 @@ GPU_METRICS: tuple[str, ...] = (
"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)
# the Temp cell of the card's four labelled values (phase 21 task 02): the agent's max
# over this card's hwmon labels. None is legal — a driver with no temperature input is
# `n/a` in that cell and must never be rendered as 0 °C.
"temp_c",
)
+292 -17
View File
@@ -26,14 +26,17 @@ 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, auth, certs
from dashboards.api import MIN_PASSWORD_LENGTH, RANGE_SECONDS, parse_range
from dashboards.app import create_app
from dashboards.collectors import UpstreamError
from dashboards.config import Settings, Target
from dashboards.rates import RateEstimator
from dashboards.severity import classify
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,
@@ -999,7 +1002,8 @@ def test_the_settings_page_refuses_a_caller_with_no_session(tmp_path: Path) -> N
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, because the controls are now the *same* on all of
One loop over all seven signed-in pages (`MARKUP_PAGES`), because the controls are now the *same*
on all of
them: clock → `⚷ Certs` → `⚙ Account` → theme toggle, in `.masthead-right` and **outside**
`.tabs` (the tab strip's structure — five dashboards, one of them current — is pinned by
`test_the_disks_tab_is_in_the_masthead_of_every_page`, and a sixth entry with no
@@ -1015,7 +1019,7 @@ def test_every_page_links_to_both_admin_pages(client, tmp_path: Path) -> None:
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 eight markups
# 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">'
@@ -1099,6 +1103,25 @@ def test_the_certs_page_carries_the_authority_and_the_agents_table(client) -> No
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 **only** 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.
"""
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`.
@@ -1427,42 +1450,212 @@ def test_system_page_colours_from_the_payload_not_from_js_thresholds(client) ->
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.
#: 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",)),
)
`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.
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
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"
)
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 leads with exactly six heroes: CPU, MEM, TEMP, DISK, GPU, NET.
"""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
@@ -4459,6 +4652,88 @@ def test_the_menu_snaps_in_both_directions_under_reduced_motion(client) -> 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:
"""Eight pages, eleven 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 seven 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) == 8, "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`.
+8 -5
View File
@@ -150,8 +150,9 @@ def test_the_discrete_gpu_capture_supplies_real_numbers(
assert gpu["busy_pct"] == pytest.approx(snapshot["gpus"][0]["busy_pct"])
assert gpu["vram_used_pct"] == pytest.approx(
gpu["vram_used_bytes"] / gpu["vram_total_bytes"] * 100.0)
# the hero's footnote is the agent's own max over this card's hwmon labels (phase 06),
# so the card and the `gpu.temp_c@N` history series can never disagree
# the Temp cell is the agent's own max over this card's hwmon labels (phase 06, printed
# as one of the card's four labelled values since phase 21 task 02), so the card and the
# `gpu.temp_c@N` history series can never disagree
assert gpu["temp_c"] == max(snapshot["gpus"][0]["temps"].values())
# one scrape is not a rate: usage and throughput are honestly n/a until the second
@@ -243,10 +244,12 @@ def test_the_apu_reports_vram_and_the_whole_addressable_pool(
def test_a_gpu_with_no_temperature_input_reports_none_not_zero(agent_snapshot: dict[str, Any],
) -> None:
"""Phase 06 put `gpus[].temp_c` on the hero footnote, so its absence must stay honest.
"""Phase 06 put `gpus[].temp_c` on the card and phase 21 task 02 gave it its own Temp
cell, so its absence must stay honest.
A driver exposing no hwmon temperature has to come back as `None` (the page prints no
temperature at all) rather than `0 °C`, the same rule
A driver exposing no hwmon temperature has to come back as `None` (the cell prints `n/a`,
the same faint `n/a` every other unmeasured value gets — the *host* with no card at all
is the other fact, and that card is omitted) rather than `0 °C`, the same rule
`tests/test_host_source.py::test_missing_sensors_stay_missing` holds for the flat sensor
list — and no `gpu.temp_c@N` sample may be written for a number nobody measured.
"""