test(discovery): fixtures for the parsers, the bounded sweep and the surfaces

tests/test_installed_apps.py is new and is where the discovery claims are
pinned. NO REAL SHIELD WAS AVAILABLE, so the command output is fixture-based
in the documented Android shapes — `pm list packages` lines, the verbose
`resolve-activity` header followed by `package/activity`, and `pm dump`
carrying `application-label` — including a localized label (the default
locale wins), a label-less app (a definitive negative, and still a row) and
the launcher (a system app that must be present).

Covered: both parsers plus their tolerances (noise, duplicates, blank,
a bare `package/activity` line, the settings entry point that is not a
launcher), the user+launcher merge and its de-duplication, the icon
heuristic and that it is always a string, the sweep's commands, its
concurrency bound (peak <= APP_SWEEP_CONCURRENCY and > 1, so it is bounded
AND genuinely concurrent), cache reuse with zero shells, the TTL, shared
sweeps for concurrent callers, and every degradation path (no poller,
disconnected, mute transport — empty list, no loop, no raise). The sensor's
count/table/reason and the "a failed refresh is a no-op, not an exception"
behaviour are pinned too.

The config-flow side is in tests/test_config_flow_ux.py: the table lists
every discovered app, a discovered app the map never mentions is appended,
the user's name wins for the same package, an `mdi:` value never reaches the
icon column, rendering issues no shell, the Discover row runs one forced
sweep and returns to the menu, a failed sweep still renders, and the merged
table round-trips into the saved options.

Also: tests/test_config_flow_real_ha.py learns the new row, the standalone
stub accepts `description_placeholders` (which the apps form now passes),
and the state=6 wording in media_session.py/media_player.py is the t57
UNIT 0c reword.
This commit is contained in:
James Mackay committed 2026-10-12 07:29:01 +01:00
1 parent a56ec38170
commit 5eefd0c6d0
6 files changed
+669 -16

No files matched your search

@@ -20,9 +20,11 @@ class MediaSessionInfo:
``playing``/``paused``/``buffering`` are independent flags mapped from the
Android PlaybackState integer: 2 = paused, 3 = playing, 6 = buffering
(t55). The Shield's own ExoPlayer/MediaSession sessions report state 6
while buffering just as stock Android does, so no vendor-specific state
integer is treated as buffering.
(t55). 6 is the stock-Android buffering value and the mapping is INFERRED
from the ``state=PlaybackState {state=N, ...}`` line shape, not from a
captured Shield dump: no fixture in this repo contains state=6, and no
real device was available. Nothing here is vendor-specific either way —
every integer is read straight out of the dump.
"""
title: str | None = None
@@ -25,7 +25,8 @@ source that has something to say, and the earlier steps always win:
a slow Jellyfin poll confirms it; self-heal in 10 s).
3. Jellyfin — STATE_PLAYING / STATE_PAUSED of the configured entity.
4. session — the ADB media session's PlaybackState, including BUFFERING
(Android state 6). Session state 3 → playing, 2 → paused.
(Android state 6, inferred from the dump's line shape rather
than observed on hardware). State 3 → playing, 2 → paused.
5. IDLE — ADB says the device is asleep (standby), OR the device is
powered on (protocol is_on True / ADB awake / the plug is on)
and NOTHING is playing. "Powered on, app open, nothing
@@ -654,9 +655,10 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity):
One place decides what a session means, so ``state`` and
``supported_features`` cannot disagree about it. Buffering is checked
FIRST: a session refilling its buffer is mid-playback, and while the
Shield never sets state 6 together with 3, BUFFERING is the more
informative of the two if a future firmware ever did.
FIRST: a session refilling its buffer is mid-playback, and state 6 is
the stock-Android buffering value (an inference from the dump's line
shape — no captured fixture here carries it), so BUFFERING is the
more informative of the two if a firmware ever set 6 together with 3.
"""
if info is None:
return None