fix(ups): offer every advertised command and gate its aliases too
Two halves of the same bug: the page offered less than the hardware does, and the gate matched only the name it rendered. `available_commands()` drops anything `COMMANDS.get()` misses, and 12 of the 23 commands the two real UPSes advertise had no entry (`beeper.on/off`, `load.on/off.delay`, `shutdown.default/reboot/stayoff`, `driver.reload*`, `driver.killpower`, `test.panel.stop`) — an invisible button the operator had no way to learn about. Every one is catalogued now with a risk tier, and the power-cutting ones are pinned to `RISK_DANGEROUS` (`shutdown.stop` is the exception the name hides: it cancels a shutdown). Both real drivers advertise `beeper.enable` *and* `beeper.on`, `beeper.disable` *and* `beeper.off`, so rendering every advertised name drew "Beeper on" twice. `COMMAND_ALIASES` collapses one action to one entry (named for a name this driver answers, so a driver that only implements the alias still gets its button) and records the other names in `also`. The POST gate resolved the request through that table instead of comparing the literal string: previously `beeper.off` walked past a `disabled` verdict recorded against `beeper.disable` — the same action, and the gate is the control, not the button. LLM-Generated: true
This commit is contained in:
@@ -143,6 +143,107 @@ def test_each_capture_is_a_real_upsd_reply_not_an_empty_stub(captures: dict[str,
|
||||
assert card["identity"]["model"], f"{name}: no model to put beside the numbers"
|
||||
|
||||
|
||||
def test_every_command_the_hardware_advertises_is_catalogued(captures: dict[str, nut.UpsReading]) -> None:
|
||||
"""An advertised command with no `COMMANDS` entry is a button that silently does not exist.
|
||||
|
||||
`available_commands()` skips anything `COMMANDS.get()` misses, so the page renders a
|
||||
shorter list than the driver offers and the operator has no way to learn the command is
|
||||
there — 12 of the 23 commands these two UPSes advertise were invisible this way
|
||||
(`beeper.on/off`, `load.off.delay`, `shutdown.default/reboot/stayoff`, `driver.*`,
|
||||
`test.panel.stop`). The captures are ground truth, so a command that appears on real
|
||||
hardware must be catalogued with a risk tier before the fixture is allowed to drift.
|
||||
"""
|
||||
advertised = {command for reading in captures.values() for command in reading.commands}
|
||||
missing = sorted(advertised - set(nut.COMMANDS))
|
||||
assert not missing, f"advertised but not in nut.COMMANDS (the UI cannot offer them): {missing}"
|
||||
|
||||
|
||||
def test_every_catalogued_command_carries_a_risk_tier_and_help() -> None:
|
||||
"""No tier means no confirmation gate; no help means an unlabeled dangerous button."""
|
||||
tiers = {nut.RISK_SAFE, nut.RISK_CAUTION, nut.RISK_DANGEROUS}
|
||||
for name, advice in nut.COMMANDS.items():
|
||||
assert advice["risk"] in tiers, f"{name}: risk {advice['risk']!r} is not a known tier"
|
||||
assert advice["label"].strip(), f"{name}: no label"
|
||||
assert len(advice["help"].strip()) > 15, f"{name}: help text too thin to be a warning"
|
||||
|
||||
|
||||
# The command list is attached by `collectors`, not by `payload()`, so these two tests build it
|
||||
# with `available_commands()` — the function under test — from each capture's real `LIST CMD`.
|
||||
offered = {"allow_actions": True, "read_only": False, "has_credentials": True}
|
||||
|
||||
|
||||
def test_no_two_offered_buttons_carry_the_same_label(captures: dict[str, nut.UpsReading]) -> None:
|
||||
"""`beeper.on`/`beeper.enable` are one action with two driver names and one label.
|
||||
|
||||
Both names are advertised by both real drivers, so a page that renders every advertised
|
||||
command shows "Beeper on" twice — which reads as a rendering bug and makes the operator
|
||||
wonder whether the two buttons do different things. They do not, and only one appears.
|
||||
"""
|
||||
for name, reading in captures.items():
|
||||
labels = [item["label"] for item in nut.available_commands(reading.commands, **offered)]
|
||||
dupes = sorted({label for label in labels if labels.count(label) > 1})
|
||||
assert not dupes, f"{name}: duplicated button labels {dupes}"
|
||||
|
||||
|
||||
def test_alias_names_collapse_one_button_per_action(captures: dict[str, nut.UpsReading]) -> None:
|
||||
"""The collapsed entry says which other names the driver answers, so nothing is hidden."""
|
||||
for ups, reading in captures.items():
|
||||
cmds = nut.available_commands(reading.commands, **offered)
|
||||
by_command = {item["command"]: item for item in cmds}
|
||||
assert len(cmds) == len(by_command), f"{ups}: a command is offered twice"
|
||||
on = by_command.get("beeper.enable")
|
||||
assert on is not None, f"{ups}: lost the beeper-on button entirely"
|
||||
if "beeper.on" in reading.commands:
|
||||
assert on["also"] == ["beeper.on"], f"{ups}: beeper.on not recorded on the entry"
|
||||
assert "beeper.on" not in by_command, f"{ups}: beeper.on rendered as its own button"
|
||||
# every advertised command is still reachable, by the name offered or by its alias
|
||||
for advertised_name in reading.commands:
|
||||
assert nut.resolve_command(cmds, advertised_name) is not None, \
|
||||
f"{ups}: {advertised_name} is advertised but nothing in the UI answers for it"
|
||||
|
||||
|
||||
def test_a_driver_offering_only_the_alias_still_gets_its_button() -> None:
|
||||
"""Emit the name this driver answers, not the preferred one it does not have.
|
||||
|
||||
Sending `beeper.enable` to a driver that only implements `beeper.on` is a refusal the
|
||||
operator cannot act on — the button looks right and the UPS says the command does not exist.
|
||||
"""
|
||||
cmds = nut.available_commands(["beeper.on"], allow_actions=True, read_only=False,
|
||||
has_credentials=True)
|
||||
assert [c["command"] for c in cmds] == ["beeper.on"]
|
||||
assert cmds[0]["label"] == "Beeper on"
|
||||
|
||||
|
||||
def test_an_alias_cannot_walk_past_a_disabled_verdict() -> None:
|
||||
"""The gate compares against the entry, so the unlisted alias is refused too."""
|
||||
cmds = nut.available_commands(["load.off", "load.off.delay", "beeper.disable", "beeper.off"],
|
||||
allow_actions=True, read_only=False,
|
||||
has_credentials=True, on_battery=True)
|
||||
off = nut.resolve_command(cmds, "load.off")
|
||||
assert off is not None and not off["enabled"], "power cut must be refused on battery"
|
||||
# an alias-shaped name for a collapsed action resolves to the same verdict
|
||||
disabled = {"command": "beeper.disable", "enabled": False, "also": ["beeper.off"]}
|
||||
assert nut.resolve_command([disabled], "beeper.off") is disabled
|
||||
assert nut.resolve_command([disabled], "beeper.disable") is disabled
|
||||
assert nut.resolve_command([disabled], "load.off") is None
|
||||
|
||||
|
||||
def test_every_command_that_can_cut_power_is_in_the_dangerous_tier() -> None:
|
||||
"""The tier is the safety control, not the label. `RISK_DANGEROUS` is what makes the page
|
||||
ask for the word `DANGER` and what makes the server refuse the command while the UPS is
|
||||
already on battery — so a power-cutting command left at `safe` or `caution` would be a
|
||||
one-click outage. Derived from the command names the drivers use, not from a list of them.
|
||||
"""
|
||||
# `shutdown.stop` is the exception the name hides: it cancels a pending shutdown, so it
|
||||
# keeps power on and belongs in `safe`, next to `beeper.mute`.
|
||||
for name, advice in nut.COMMANDS.items():
|
||||
can_cut_power = (name.startswith(("load.off", "shutdown.")) and name != "shutdown.stop") \
|
||||
or name in {"load.reboot", "driver.killpower", "calibrate.start"}
|
||||
if can_cut_power:
|
||||
assert advice["risk"] == nut.RISK_DANGEROUS, \
|
||||
f"{name} can cut output power but its risk is {advice['risk']!r}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Every field the hero row and the glyph read
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user