phase: 05_ups_hero_battery_glyph

**Phase 05 final pass — all criteria met; whole project 100 % green.**

Verified (live upsd + isolated simulator):
- Real `apc`/`cyberpower`: Charge/Load/Runtime are the card's three largest elements (38.4 px vs 15.2 px next), coloured only by API `severity` — live right now 100 % ok / 79.0 % **warn** / 5m **fail**; glyph fill = charge (100/100 viewBox units, `--ok` token), `<title>` "apc: Charge 100 percent, on line"; SIMULATED hidden; battery °C `n/a` uncoloured; every button disabled with its reason; `confirm_text: DANGER` on power-cutting commands; **0 non-GET requests** → `data/logs/ups-verify-live-{card,full}.png`.
- Simulator on `:8091` (own `/tmp` DB, killed after): caution dialog → Cancel, dangerous dialog → Run disabled until `DANGER` → Cancel, **0 POSTs**; one deliberate quick test *to the sim only* flipped the card to `OB` (glyph `stroke-dasharray="7 5"`, "on battery", health "discharging, on battery") then `OL CHRG` (bolt) then `OL` → `ups-verify-sim-{dialog-caution,dialog-danger,onbattery-button,onbattery-page,charging-button}.png`; glyph matrix `ups-glyph-*.png` (100/mid/low/unknown/empty/unrated, dark/light/200 %) present; `grep -c "function battery"` = 1.

Defects found in earlier work and fixed:
1. **`poller.py` could stop polling in silence** — 4 of 6 dev-server sources had frozen `attempts` (ages 12–38 min) while `/api/health` still said `ok: true`: the store-write half of `run_once` was outside the `try`, and a dead task's exception is never retrieved. Now `_commit()` moves `last_ok` only after the write lands, `_store_refused()` reports it (`ok:false`, real reason, one `store-error` event + one `recording` on recovery, payload kept), and `_loop()` catches anything that escapes and keeps polling — 4 new tests in `tests/test_poller.py`.
2. **`nut_sim.py` (modified by this phase) sat at 83 % coverage**; its whole command path was untested. Added tests (every advertised command implemented, unknown → `ValueError`, alarms → card + `assess()` fail, `NUT_SIMULATE` truthy rule) and fixed two model lies: `beeper.*` answered OK while changing nothing (now publishes `ups.beeper.status` enabled/muted/disabled) and a finished self-test never wrote `ups.test.result` (aborted writes none). → 100 %.
3. `nut_sim.from_env()` was dead code duplicating `app._truthy`; `app.py` now builds the simulator through it, so the enable gate lives once.

Tests / lint (exact): `bash .agents/validate.sh` → **EXIT=0** — `uv run pytest --cov --cov-fail-under=94` **543 passed, 8 skipped, coverage 97.91 %**; `uv run ruff check src tests agent` clean; `uv run pyright` 0 errors; `node --check` clean; repo hygiene clean. `uv run pytest -m live --live` **with `.env.dev` sourced → 8 passed** (one earlier run: 2 transient `9121/snapshot timed out` against the 4 s `HOST_TIMEOUT`; three later runs 8/8). `uv pip list --outdated` empty. Dev server restarted on the final code: `health ok`, 6/6 sources fresh.

Docs: AGENTS.md — invariant 4 extended ("nothing may stop polling in silence" + store-refusal semantics), `poller.py`/`nut_sim.py` responsibility lines, 533→543 and 96.9 %→97.9 %, "~97 %"→"~98 %". README.md — poller bullet gained the same guarantee, Testing gained "source `.env.dev` before `-m live --live`" (bare runs skip 7 of 8 live tests, which reads like a pass), Battery-test section gained the `deploy/nut-sudoers` install line and the read-before-write warning.

Notes/deviations: no live instant command was ever sent to real hardware (the four gates leave them disabled — AGENTS gap 1 unchanged); the on-battery proof was driven through the real button against `NUT_SIMULATE=1`, which is stronger than the NOTES hand-driven states. `NOTES-verify.md` under `todo/` left untouched per instructions. Next phase: **none** — 05 is the last in the PLAN.md roadmap.
This commit is contained in:
2026-10-17 10:27:23 -04:00
parent f44a617ef9
commit 34854d32af
44 changed files with 2374 additions and 105 deletions
+75
View File
@@ -0,0 +1,75 @@
# Verbatim upsd reply (LIST UPS / LIST VAR / LIST CMD), recorded by
# ./scripts/capture-fixtures.sh — one `VAR`/`CMD` line per protocol line.
# Volatile values (ups.load, battery.runtime, the voltages) move every poll,
# so a diff here on re-capture is expected; the contract asserts shape, units
# and which variables the drivers omit, never those numbers.
UPS apc "APC Back-UPS RS 1500MS (USB)"
VAR apc battery.charge "100"
VAR apc battery.charge.low "10"
VAR apc battery.charge.warning "50"
VAR apc battery.date "2001/09/25"
VAR apc battery.mfr.date "2021/06/24"
VAR apc battery.runtime "648"
VAR apc battery.runtime.low "120"
VAR apc battery.type "PbAc"
VAR apc battery.voltage "27.3"
VAR apc battery.voltage.nominal "24.0"
VAR apc device.mfr "American Power Conversion"
VAR apc device.model "Back-UPS RS 1500MS"
VAR apc device.serial "SCRUBBEDSERIAL1 "
VAR apc device.type "ups"
VAR apc driver.debug "0"
VAR apc driver.flag.allow_killpower "0"
VAR apc driver.name "usbhid-ups"
VAR apc driver.parameter.interrupt_pipe_no_events_tolerance "-1"
VAR apc driver.parameter.pollfreq "30"
VAR apc driver.parameter.pollinterval "2"
VAR apc driver.parameter.port "auto"
VAR apc driver.parameter.productid "0002"
VAR apc driver.parameter.synchronous "auto"
VAR apc driver.parameter.vendorid "051d"
VAR apc driver.state "quiet"
VAR apc driver.version "2.8.5"
VAR apc driver.version.data "APC HID 0.101"
VAR apc driver.version.internal "0.71"
VAR apc driver.version.usb "libusb-1.0.30 (API: 0x0100010C)"
VAR apc input.sensitivity "medium"
VAR apc input.transfer.high "144"
VAR apc input.transfer.low "88"
VAR apc input.voltage "123.0"
VAR apc input.voltage.nominal "120"
VAR apc ups.beeper.status "disabled"
VAR apc ups.delay.shutdown "20"
VAR apc ups.firmware "952.e5 .D"
VAR apc ups.firmware.aux "e5 "
VAR apc ups.load "48"
VAR apc ups.mfr "American Power Conversion"
VAR apc ups.mfr.date "2021/01/08"
VAR apc ups.model "Back-UPS RS 1500MS"
VAR apc ups.productid "0002"
VAR apc ups.realpower.nominal "900"
VAR apc ups.serial "SCRUBBEDSERIAL1 "
VAR apc ups.status "OL"
VAR apc ups.test.result "No test initiated"
VAR apc ups.timer.reboot "0"
VAR apc ups.timer.shutdown "-1"
VAR apc ups.vendorid "051d"
CMD apc beeper.disable
CMD apc beeper.enable
CMD apc beeper.mute
CMD apc beeper.off
CMD apc beeper.on
CMD apc driver.killpower
CMD apc driver.reload
CMD apc driver.reload-or-error
CMD apc driver.reload-or-exit
CMD apc load.off
CMD apc load.off.delay
CMD apc shutdown.default
CMD apc shutdown.reboot
CMD apc shutdown.stop
CMD apc test.battery.start.deep
CMD apc test.battery.start.quick
CMD apc test.battery.stop
CMD apc test.panel.start
CMD apc test.panel.stop