**Verified (final pass, phase 11 — all 7 tasks already in `complete/`)**
- Gate from the wire on a restarted dev server: `/system`→`303 /login?next=/system` (`/llm?range=1h`→`/login?next=/llm%3Frange%3D1h`), `/api/summary`+`/api/config`→`401 {"ok":false,…}`, `/api/health`→`200`; signed-in `GET` of all six pages `200`, `/login`→`303 /`.
- Auth flows on a throwaway server (`:8199`, `/tmp` DB — dev `auth.verifier` untouched): wrong pw `401`, `testpass` `200`, password change `200`, **old cookie `401` / new cookie `200`**, old pw `401` / new pw `200`, short new `422`, wrong current `403`, cookie write without CSRF `403`, logout `200`; 5 failures then `429 retry-after: 60`; `login-denied`/`password-changed`/`password-denied` events; `meta` = the two auth keys.
- Fail-closed boot (no seed, empty DB): **exit 3**, message names `DASH_ADMIN_PASSWORD`, `auth.verifier`, and the `DELETE FROM meta …` reset. `dev-server.sh status` + the Containerfile `HEALTHCHECK` python one-liner both `200`/exit 0 unauthenticated.
- Live payloads unchanged (phases 01–10 walk): `/api/summary` keys + 8 alerts + hosts 3/2 live; UPS `simulated:false` with charge/load/runtime severity; deskwork `nvme0n1` 51 °C/19 159 h/11 % wear; gateway `smartctl_available:false`; driveripper `unreachable` stub.
**Tests/lint (all green)**: `bash .agents/validate.sh` → exit 0 — `1140 passed, 11 skipped`, coverage `98.62%` (floor 94), ruff `All checks passed!`, pyright `0 errors`, `node --check` clean, repo hygiene clean. `uv run pytest -m live --live` → `8 passed, 3 failed`, all `driveripper: 10.1.0.126:9120 … ConnectError` (pre-existing, auth-free; probe confirms nothing listening).
**Completion criteria**: all met except the live-suite caveat above — installing the driveripper agent needs root on that host (no `ops` grant), so per the live-infra rule I made **no live change**; task 07's five screenshots (`auth-login/-rejected/-locked-out/-signed-in-overview/-settings.png`) remain the browser evidence, and the login/rotation/lockout flows were re-proved over HTTP this pass.
**Docs**: README — fixed "only place the CSRF token is published" (login/password responses mint one), added the undocumented `DASH_RELOAD` row, stated that Sign out ends only this browser's cookie. `AGENTS.md` — new known gap 8 (stateless logout cannot revoke a copied token), gap-4 dev-store sizes dated, fleet section notes `data/` is gitignored. `api.py` — same `status` docstring imprecision fixed. Verified the 4 `DASH_*` rows, reset command, seed-wins rule and invariant 12 against `app.py`/`config.py`.
**Deviations**: no dependency bump (`uv pip list --outdated` advisory: certifi/fastapi/starlette — phase added none); verification notes appended to `data/logs/NOTES-auth.md` §10.
**Next pending phase**: `12_nut_self_test_verdict`.
254 lines
11 KiB
Python
254 lines
11 KiB
Python
"""The `/slots` shape contract, driven from the live capture (phase 07, task 01).
|
|
|
|
The old gufo slot table read field names the server never published and rendered dashes
|
|
forever; this file is the fix for the *process*, not just the markup: the shape the slot
|
|
grid (task 04) will read is pinned against `tests/fixtures/slots_gufo.json` — the verbatim
|
|
`/slots` reply recorded by `./scripts/capture-fixtures.sh` — and against the running gufo
|
|
in `tests/test_live_fleet.py::test_gufo_slots_answer_the_pinned_shape`. If the fixture (or
|
|
the live server) drifts, a test fails here, not a dashboard at 2am (`AGENTS.md` rule 7).
|
|
|
|
The table and the assertion live in `tests/slots_contract.py` so both suites read one
|
|
list, exactly like `tests/card_contract.py` does for `/system`.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import re
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from auth_client import sign_in
|
|
from auth_contract import ADMIN_PASSWORD, FAST_PBKDF2
|
|
from dashboards.app import create_app
|
|
from dashboards.config import Settings
|
|
from dashboards.store import SampleStore
|
|
from slots_contract import (
|
|
CANONICAL_TASK_ID_FIELD,
|
|
REQUIRED_NEXT_TOKEN_FIELDS,
|
|
REQUIRED_ROW_FIELDS,
|
|
SLOT_CELLS,
|
|
TASK_ID_ALIASES,
|
|
assert_slot_shape,
|
|
load_slots_capture,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def rows() -> list[dict[str, Any]]:
|
|
"""A fresh mutable copy of the capture — mutations in one test cannot leak."""
|
|
return copy.deepcopy(load_slots_capture())
|
|
|
|
|
|
@pytest.fixture
|
|
def client(tmp_path: Path) -> Iterator[TestClient]:
|
|
"""A running app, so the deletion assertions run on the bytes a browser would get."""
|
|
db = str(tmp_path / "slots-contract.sqlite3")
|
|
store = SampleStore(db)
|
|
app = create_app(Settings(db_path=db, admin_password=ADMIN_PASSWORD,
|
|
pbkdf2_iterations=FAST_PBKDF2), store)
|
|
test_client = TestClient(app)
|
|
test_client.__enter__()
|
|
try:
|
|
# Signed in because this fixture reads `/llm` as served bytes, and `TestClient` follows
|
|
# the 303 to /login: an unsigned client would check the deleted slot table against the
|
|
# *login page* and pass without ever looking at /llm.
|
|
sign_in(test_client)
|
|
yield test_client
|
|
finally:
|
|
test_client.__exit__(None, None, None)
|
|
store.close()
|
|
|
|
|
|
def _code(body: str) -> str:
|
|
"""An asset with its comments stripped — prose may *name* a dead thing the code must not be."""
|
|
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 capture is real, and the contract passes on it
|
|
# --------------------------------------------------------------------------- #
|
|
def test_the_gufo_slots_capture_is_on_disk_and_is_a_real_answer(
|
|
rows: list[dict[str, Any]]) -> None:
|
|
"""Every row carries a dozen fields — the whole point of capturing the wire.
|
|
|
|
Without this, a two-key hand-written stub would satisfy "the key exists" and the
|
|
contract would prove nothing (`test_ups_contract.py` holds the same line for the UPS
|
|
captures).
|
|
"""
|
|
assert rows, (
|
|
"gufo answered /slots with no rows — even an idle engine reports one row per "
|
|
"parallel slot, and that idle shape is exactly what the grid must handle"
|
|
)
|
|
for row in rows:
|
|
assert len(row) >= 10, f"{len(row)} keys — this is not a capture, it is a guess"
|
|
|
|
|
|
def test_the_slot_grid_fields_hold_on_every_captured_row(
|
|
rows: list[dict[str, Any]]) -> None:
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
def test_the_capture_records_the_idle_shape_the_grid_must_handle(
|
|
rows: list[dict[str, Any]]) -> None:
|
|
"""The assumption written into the phase: a fresh capture is an idle engine.
|
|
|
|
Every row is idle with a blank prompt — the grid must render that as N idle cards, not
|
|
as "no data". If a re-capture lands mid-request this test names the drift loudly
|
|
instead of letting the contract quietly widen; busy rows are covered below.
|
|
"""
|
|
for row in rows:
|
|
assert row["is_processing"] is False, "the capture caught a busy slot; re-run it when idle"
|
|
assert row["state"] == 0
|
|
assert row["n_prompt_tokens"] == 0
|
|
|
|
|
|
def test_a_busy_row_of_the_same_shape_still_satisfies_the_contract(
|
|
rows: list[dict[str, Any]]) -> None:
|
|
"""The contract is the row shape, not the idle numbers — a busy slot must pass too."""
|
|
busy = rows[0]
|
|
busy.update({"is_processing": True, "state": 1, "id_task": 42,
|
|
"n_prompt_tokens": 128, "n_prompt_tokens_processed": 64,
|
|
"n_prompt_tokens_cache": 16})
|
|
busy["next_token"][0]["n_decoded"] = 7
|
|
busy["next_token"][0]["prob"] = 0.93 # llm_monitor prints it only when present
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
def test_an_empty_row_list_passes_the_contract() -> None:
|
|
"""Zero slots is a state the grid renders (an explicit empty message), not a crash."""
|
|
assert_slot_shape([])
|
|
|
|
|
|
def test_the_contract_names_every_cell_the_grid_reads() -> None:
|
|
"""The doc table covers the llm_monitor cell set; a rename here is a rename there."""
|
|
assert set(SLOT_CELLS) == {
|
|
"card key", "state pill", "request", "prompt tok", "cache hit",
|
|
"generated", "remaining", "context", "model name",
|
|
}
|
|
for label, paths in SLOT_CELLS.items():
|
|
assert paths, f"{label} lists no row path"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Drift fails: the contract is only real if mutations are caught
|
|
# --------------------------------------------------------------------------- #
|
|
@pytest.mark.parametrize("field", sorted(REQUIRED_ROW_FIELDS))
|
|
def test_the_contract_fails_when_a_row_drops_a_field_the_grid_reads(
|
|
rows: list[dict[str, Any]], field: str) -> None:
|
|
del rows[0][field]
|
|
with pytest.raises(AssertionError, match=field):
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
@pytest.mark.parametrize(("field", "value"), [
|
|
("id", "0"), # a string id would key two cards "0" and 0 apart
|
|
("is_processing", 0), # the legacy numeric state is not the authoritative bool
|
|
("state", "OL"), # this is not NUT
|
|
("n_ctx", None), # None would reach the number formatter
|
|
("n_prompt_tokens", True), # bool sneaks past a bare isinstance(x, int)
|
|
("next_token", {"n_decoded": 1}),
|
|
])
|
|
def test_the_contract_fails_on_a_type_the_grid_cannot_render(
|
|
rows: list[dict[str, Any]], field: str, value: Any) -> None:
|
|
rows[0][field] = value
|
|
with pytest.raises(AssertionError, match=field):
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
@pytest.mark.parametrize("field", sorted(REQUIRED_NEXT_TOKEN_FIELDS))
|
|
def test_the_contract_fails_when_next_token_loses_a_field(
|
|
rows: list[dict[str, Any]], field: str) -> None:
|
|
del rows[0]["next_token"][0][field]
|
|
with pytest.raises(AssertionError, match=field):
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
def test_the_contract_fails_when_the_canonical_request_id_is_missing(
|
|
rows: list[dict[str, Any]]) -> None:
|
|
"""`id_task` is the key the grid prints, so its absence is drift, not a blank cell.
|
|
|
|
The alias exists in the same row, and `llm_monitor` would have fallen back to it. This
|
|
contract does not: a renamed fact fails here rather than rendering a plausible-looking
|
|
card out of a different field (`page-llm.js`'s `SLOT_FIELDS` names one key per cell).
|
|
"""
|
|
del rows[0][CANONICAL_TASK_ID_FIELD]
|
|
with pytest.raises(AssertionError, match=CANONICAL_TASK_ID_FIELD):
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
def test_the_request_id_alias_is_recorded_and_never_needed(rows: list[dict[str, Any]]) -> None:
|
|
"""Dropping the alias must not fail the contract — proof it is not a second source."""
|
|
assert set(TASK_ID_ALIASES) == {"task_id"}
|
|
for row in rows:
|
|
del row["task_id"]
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
def test_the_contract_fails_on_duplicate_slot_ids(rows: list[dict[str, Any]]) -> None:
|
|
assert len(rows) > 1, "the capture needs two slots for a dupe to be possible"
|
|
rows[1]["id"] = rows[0]["id"]
|
|
with pytest.raises(AssertionError, match="duplicate slot ids"):
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
def test_the_contract_fails_when_the_two_state_fields_disagree(
|
|
rows: list[dict[str, Any]]) -> None:
|
|
rows[0]["is_processing"] = True # state stays 0 — which field is the pill to believe?
|
|
with pytest.raises(AssertionError, match="state"):
|
|
assert_slot_shape(rows)
|
|
|
|
|
|
def test_the_contract_fails_when_a_row_is_not_an_object(rows: list[dict[str, Any]]) -> None:
|
|
broken: list[Any] = list(rows)
|
|
broken[0] = "idle"
|
|
with pytest.raises(AssertionError, match="not an object"):
|
|
assert_slot_shape(broken)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# The deleted table stays deleted (completion criterion, on served bytes)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_the_broken_slot_table_is_gone_from_html_js_and_css(client: TestClient) -> None:
|
|
"""No `#slots-card` / `#slots-table` / `#slots-empty` / `renderSlots` survives.
|
|
|
|
Asserted on served bytes because there is no JS test runner (a locked anchor). The
|
|
guessed field names go with it — those are the memory of an upstream the capture
|
|
replaced. The `.chip.info` rule only ever painted that table's "has msg" cell, so it
|
|
goes too (orphan-rule policy, `AGENTS.md`).
|
|
"""
|
|
html = _code(client.get("/llm").text)
|
|
for gone in ("slots-card", "slots-table", "slots-empty"):
|
|
assert f'id="{gone}"' not in html, f"/llm still renders the {gone}"
|
|
js = _code(client.get("/static/page-llm.js").text)
|
|
for gone in ("renderSlots", "with_message", "n_predict", "decode_per_second",
|
|
"cache_usable"):
|
|
assert gone not in js, f"page-llm.js still reads {gone}"
|
|
css = client.get("/static/dash.css").text
|
|
assert ".chip.info" not in css, "dash.css still styles a class nothing renders"
|
|
|
|
|
|
def test_the_prompt_field_is_user_content_that_never_reaches_the_page(
|
|
client: TestClient, rows: list[dict[str, Any]]) -> None:
|
|
"""A busy slot's `prompt` is the operator's in-flight request text.
|
|
|
|
The capture script blanks it (fixture stays shape-true, content-free), and the page
|
|
never reads the key — the grid renders tokens, not somebody's prompt.
|
|
"""
|
|
for row in rows:
|
|
assert row.get("prompt", "") == "", "the capture script must blank prompt content"
|
|
js = _code(client.get("/static/page-llm.js").text)
|
|
# A slot-row read is `slot.prompt` / `row.prompt` / `s.prompt` / `["prompt"]`. A bare
|
|
# `.prompt` ban is impossible — `state.charts.prompt` is a chart object — so the name
|
|
# before the dot must be a whole word (the lookbehind is what stops `charts.prompt`
|
|
# from matching through its trailing "s").
|
|
assert not re.search(r"(?<![\w.])(?:slot|row|s|item)\.prompt\b", js), \
|
|
"page-llm.js reads a slot row's user-content field"
|
|
assert '["prompt"]' not in js and "['prompt']" not in js, \
|
|
"page-llm.js indexes a row's prompt by name"
|