Files
dashboards/tests/slots_contract.py
ducoterra 3d5ccd7a39 phase: 07_llm_monitor_style_pages
**Phase 07 final verification pass — all tasks already in `complete/`; every criterion re-verified live.**

- **Old slot table gone**: no `#slots-card`/`#slots-table`/`renderSlots` in HTML/JS/CSS (only the new grid's `.slots-empty` *class*); pinned by `test_the_broken_slot_table_is_gone_from_html_js_and_css`.
- **`tests/fixtures/slots_gufo.json`**: present; live `GET ${GUFO_BASE_URL}/slots` this pass matches it key-for-key (row + `next_token[0]`), so the capture is reproducible; drift proven to fail (mutation tests + live `test_gufo_slots_answer_the_pinned_shape`, 9 live tests pass).
- **Aggregate block**: live page shows `2 of 2 engines`, running/waiting/prompt/decode/total-tokens straight from `fleet_totals()`, dot `ok`, no verdict colour; on a throwaway single-engine instance (:8091, private temp DB) `#fleet` is `hidden`, one full-width strip card + one panel, zero banners → "one clean half".
- **Strip + slot grid on live data**: 2 `.card.stat` (KV bars `bar ok` 13 %/0 %), gufo grid rendered real cards incl. a busy slot (`#2189`, 67849 prompt tok, 99 % cache share, `processing`), vLLM note-only with no grid; no `NaN`/`undefined` anywhere. Screenshots: `data/logs/llm-phase07-final-live-{two-engines,single-engine}.png`, `ups-charts-aria-labels.png`.
- **Defect found in earlier work and fixed**: `/ups`' four `LineChart`s shipped no `ariaLabel`, violating AGENTS.md's "every chart names itself for assistive tech" (`/system`+`/llm` were pinned, `/ups` wasn't) → added labels to `page-ups.js` and `test_every_ups_chart_names_itself_for_assistive_tech`; browser-verified `role="img"` + label on all four.
- **Gate**: `bash .agents/validate.sh` → **exit 0**; `uv run pytest --cov --cov-fail-under=94` → **604 passed, 9 skipped, coverage 97.91 %**; `uv run ruff check src tests agent` → All checks passed; `uv run pyright` → **0 errors**; `node --check` every page script ok; `uv run pytest -m live --live` → **9 passed**; `uv pip list --outdated` empty.
- **Docs**: README + AGENTS.md reviewed end-to-end; fixed test count (582→604), `/llm` source column + both architecture diagrams now name gufo `/slots`, AGENTS.md agent line gained `GET /schema` + status page, ariaLabel convention now covers all three chart pages, README Testing block now lists `bash .agents/validate.sh`. Env-var table, agent/install flags, Containerfile facts, `/api/*` shapes, range chips, remote URL all verified accurate — no other changes needed.
- **Live-infra**: no fleet/host/config change; the temp :8091 instance, its temp DB and the fake processes are removed; dev server restarted and `/api/health` `ok` (all 6 sources ok). One thing I deliberately did **not** touch: the gitignored local `.env.dev` still carries a comment claiming the NUT `[dashboards]` account has only battery-test `instcmds`, whereas AGENTS.md/README record `instcmds = all` (verified on hardware 2026-10-17) — operator-owned file, flag for the operator to reword.
- **Next pending phase**: `08_events_paging_and_copy`.
2026-10-17 17:50:19 -04:00

205 lines
11 KiB
Python

"""The `/slots` row-shape contract behind the `/llm` slot grid (phase 07, task 01).
The old gufo slot table was deleted unread: it looked for `slot.state` as a *string*,
`slot.n_prompt`, `slot.n_predict`, `slot.cache` and `slot.decode_per_second`, and the live
server publishes none of those names — every row rendered em-dashes forever. That is
`AGENTS.md` rule 7 failing in slow motion ("trust a memory of an upstream"), so the
replacement grid (task 04) is written against a *recorded* ground truth instead:
`tests/fixtures/slots_gufo.json`, the verbatim `/slots` reply from the running gufo,
re-captured by `./scripts/capture-fixtures.sh`.
The same device as `tests/card_contract.py`: the tables live here so the hermetic test
(`tests/test_llm_slots_contract.py`, driven from the fixture) and the live test
(`tests/test_live_fleet.py::test_gufo_slots_answer_the_pinned_shape`) assert one list and
cannot drift apart.
====================== ==========================================================
Slot card cell Path in a `/slots` row
====================== ==========================================================
card key ``id`` (unique per row — the grid keys one card per slot)
state pill ``is_processing`` (authoritative bool) / ``state`` (legacy 0/1)
request ``id_task`` — canonical; the ``task_id`` alias is captured but
**never rendered**
prompt tok ``n_prompt_tokens`` (+ ``n_prompt_tokens_processed`` sub-line)
cache hit ``n_prompt_tokens_cache`` (+ ratio against ``n_prompt_tokens``)
generated ``next_token[0].n_decoded`` (+ optional ``prob`` sub-line)
remaining ``next_token[0].n_remain`` — where the capture puts it, never a
row-level ``n_remain`` fallback
context ``n_ctx``
model name ``model``
====================== ==========================================================
The field names and types are the live capture's. `llm_monitor` reads the same set with
``a ?? b`` fallbacks (the task-id alias pair, ``n_remain`` at either level); the grid drops
them. Task 04's rule is that **this contract names the canonical key of each fact and the
page renders that one**, so a rename fails a test here instead of quietly reading a different
field. `llm_monitor` is the design reference, not a dependency.
``prompt`` is deliberately **not** a card cell: on a busy slot it is the user's in-flight
request text. `./scripts/capture-fixtures.sh` blanks it in the fixture, and the page must
never read it.
The capture is an **idle** engine — gufo answers one row per parallel slot even when
nothing is generating, so "every slot idle, prompt blank" is the shape the grid must
handle; that assumption is pinned in the hermetic test, and the mutation tests below keep
the contract honest for busy rows too.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
FIXTURES = Path(__file__).resolve().parent / "fixtures"
#: The one name the grid prints for "which request is in this slot". The live capture answers
#: **both** `id_task` and `task_id` for the same fact (and `llm_monitor` reads `id_task`
#: first), so a name has to be made canonical rather than chained: `slot.id_task ??
#: slot.task_id` is exactly the guessing that made the deleted table impossible to trust. If a
#: re-capture ever answers only the alias, this constant changes and the grid follows — the
#: fallback never moves into the page (`page-llm.js`'s `SLOT_FIELDS` names this key once).
CANONICAL_TASK_ID_FIELD: str = "id_task"
#: Other names a row may answer for that one fact: type-checked when present, rendered by
#: nothing.
TASK_ID_ALIASES: tuple[str, ...] = ("task_id",)
#: Every name known for the request id, canonical first (for messages and cross-checks).
TASK_ID_FIELDS: tuple[str, ...] = (CANONICAL_TASK_ID_FIELD, *TASK_ID_ALIASES)
#: Doc table: slot-grid cell -> the row path(s) that cell reads. The authoritative
#: presence/type rules are the three tables below it.
SLOT_CELLS: dict[str, tuple[str, ...]] = {
"card key": ("id",),
"state pill": ("is_processing", "state"),
"request": (CANONICAL_TASK_ID_FIELD,),
"prompt tok": ("n_prompt_tokens", "n_prompt_tokens_processed"),
"cache hit": ("n_prompt_tokens_cache",),
"generated": ("next_token.n_decoded", "next_token.prob"),
"remaining": ("next_token.n_remain",),
"context": ("n_ctx",),
"model name": ("model",),
}
#: Field -> (accepted types, why the grid needs it). Every row must carry every one of
#: these; a missing key is the drift that made the old table render dashes.
REQUIRED_ROW_FIELDS: dict[str, tuple[tuple[type, ...], str]] = {
"id": ((int,), "the grid keys one card per slot — a duplicate or absent id is two "
"cards for one slot or a slot nobody updates"),
"is_processing": (((bool,), "the authoritative busy/idle verdict behind the state pill")),
"state": ((int,), "the legacy numeric state (0 idle / 1 processing); the pill prefers "
"is_processing but labels anything else it cannot name"),
CANONICAL_TASK_ID_FIELD: ((int,), "the request cell — the only name the grid reads for "
"it, so its absence is drift rather than a blank cell"),
"n_ctx": ((int,), "the context cell — the slot's window size"),
"n_prompt_tokens": ((int, float), "the prompt cell"),
"n_prompt_tokens_processed": ((int, float), "the `processed …` sub-line while prefill runs"),
"n_prompt_tokens_cache": ((int, float), "the cache-hit cell (ratio against n_prompt_tokens)"),
"next_token": ((list,), "the generated cell reads the first entry's n_decoded"),
}
#: Fields the grid reads *when present* — absence degrades to `n/a`, presence must keep the
#: type. `model` names the slot header; `prob` is a sub-line llm_monitor prints only if
#: the server publishes it (this gufo does not, on the captured rows).
OPTIONAL_ROW_FIELDS: dict[str, tuple[tuple[type, ...], str]] = {
"model": ((str,), "the model name beside the slot id"),
"speculative": ((bool,), "whether speculative decoding is on for this slot"),
"n_remain": ((int, float), "some builds answer it at the row level; the grid reads "
"next_token[0].n_remain, which every captured row carries"),
"task_id": ((int,), f"the alias of {CANONICAL_TASK_ID_FIELD} — captured, never rendered"),
}
#: Every entry of `next_token` that is present must carry these; an empty list is legal
# (the grid prints `n/a` for generated/remaining, never a fabricated 0).
REQUIRED_NEXT_TOKEN_FIELDS: dict[str, tuple[tuple[type, ...], str]] = {
"n_decoded": ((int, float), "the generated cell"),
"n_remain": ((int, float), "the remaining cell — the captured rows carry it here, "
"not at the row's top level"),
}
OPTIONAL_NEXT_TOKEN_FIELDS: dict[str, tuple[tuple[type, ...], str]] = {
"prob": ((int, float), "the `p=…` sub-line; this capture omits it and the grid copes"),
}
#: Row fields that are user content: captured blanked, never rendered by the page.
USER_CONTENT_FIELDS: tuple[str, ...] = ("prompt",)
def load_slots_capture(name: str = "gufo") -> list[dict[str, Any]]:
"""The verbatim `/slots` capture, as the live server answered it."""
path = FIXTURES / f"slots_{name}.json"
if not path.exists():
raise AssertionError(
f"{path} is missing — run ./scripts/capture-fixtures.sh (it captures gufo's "
"/slots whenever GUFO_BASE_URL is set)"
)
rows = json.loads(path.read_text())
assert isinstance(rows, list), f"{path.name}: /slots answered {type(rows).__name__}, not a list"
return rows
def _matches(value: Any, types: tuple[type, ...]) -> bool:
"""`bool` is a subclass of `int`; never let `true` answer for a number or vice versa."""
if types == (bool,):
return isinstance(value, bool)
if any(t in (int, float) for t in types):
return isinstance(value, (int, float)) and not isinstance(value, bool)
return isinstance(value, types)
def _check_fields(row: dict[str, Any], table: dict[str, tuple[tuple[type, ...], str]],
*, require: bool, where: str) -> None:
for field, (types, why) in table.items():
if require:
assert field in row, (
f"{where}: no key {field!r} — {why}. The deleted slot table rendered dashes "
"for exactly this kind of drift; fail here instead"
)
elif field not in row:
continue
assert _matches(row[field], types), (
f"{where}.{field} is {type(row[field]).__name__}, expected "
f"{'/'.join(t.__name__ for t in types)} — {why}"
)
def assert_slot_shape(rows: list[Any]) -> None:
"""Every `/slots` row has the shape the slot grid reads. Empty list is legal.
Drift in either direction fails: a renamed/missing key (including the canonical request
id — the alias does not excuse its absence), a type the grid cannot format, or a
duplicated slot id (keyed cards would collide).
"""
assert isinstance(rows, list), f"/slots answered {type(rows).__name__}, not a list of rows"
seen_ids: list[int] = []
for index, row in enumerate(rows):
where = f"row[{index}]"
assert isinstance(row, dict), f"{where} is {type(row).__name__}, not an object"
_check_fields(row, REQUIRED_ROW_FIELDS, require=True, where=where)
_check_fields(row, OPTIONAL_ROW_FIELDS, require=False, where=where)
# The canonical request id is required above. An alias may ride along (every
# captured row answers both) and the grid ignores it.
tokens = row["next_token"]
for entry in tokens:
assert isinstance(entry, dict), f"{where}.next_token entry is not an object"
_check_fields(entry, REQUIRED_NEXT_TOKEN_FIELDS, require=True,
where=f"{where}.next_token")
_check_fields(entry, OPTIONAL_NEXT_TOKEN_FIELDS, require=False,
where=f"{where}.next_token")
# The pill: `is_processing` is authoritative, and `state == 0` must not contradict
# it — llm_monitor's exact reading, so a busy row and an idle row stay tellable.
if row["is_processing"]:
assert row["state"] != 0, (
f"{where}: is_processing=True with state=0 — the pill cannot tell the operator "
"which field to believe"
)
seen_ids.append(row["id"])
duplicates = sorted({i for i in seen_ids if seen_ids.count(i) > 1})
assert not duplicates, (
f"duplicate slot ids {duplicates} — the grid keys one card per slot and a dupe "
"renders one card twice (or drops the other)"
)