a1ce28996ed922b1a1237f4c95da465e141115a9
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eb62011ded |
fix(media-player): the browse root carries a real string id (t63)
The live report: "Browse media seems to spin and never finish loading". The cause is in what async_browse_media HANDS BACK, not in the method — it returns with no awaits, so nothing inside it can hang. The root node's media_content_id was None, and HA core never serves a node without a string id: across the 43 BrowseMedia constructions in the installed core media_player platforms, zero pass None (the roots use a literal/module constant — arcam_fmj's "root", "apps", BROWSE_ROOT, TUNER_PRESETS_ROOT — or "" for a pure container). The media-browser frontend keys the node it is loading on that id, so a null id left the dialog re-requesting the same node for ever. BROWSE_ROOT_ID = "root" is now what the entity serves for EVERY requested id — the root id, None, or anything a stale frontend asks for — so a repeat request is idempotent rather than a loop. The type was reassessed as well: MediaType.APPS (t61's choice) is what core pairs with children=[...] — a folder OF apps — so on a zero-child node it invited the frontend to treat it as an unloaded collection. No MediaType member means "a directory with nothing in it", so the node carries the class it already declares (MediaClass.DIRECTORY -> "directory"), which is exactly what can_expand=False and children=[] say. Verified over the real path the frontend uses — websocket_browse_media -> player.async_browse_media -> payload.as_dict() — by driving HA's own decorated handler against a real entity (tests/test_ha_api_drift.py, which the CI drift job runs with Home Assistant installed): the root is a genuine BrowseMedia, its as_dict() survives json.dumps, the id is the string "root", and browse(root_id) == browse(None) in shape. Red proof on the pre-fix source: the new real-HA test fails at `assert isinstance(payload["media_content_id"], str)` -> `isinstance(None, str)`. The frontend bundle is not installed here (no hass_frontend) and there is no browser to drive, so the rendered dialog is NOT exercised: this pins the backend half of that path, end to end through the handler's decorators. |
||
|
|
93d0afcdcf |
test(drift): flag unreachable statements and raise the call-argument floor (t62 F2)
Two gate gaps let the t62 findings ship, and both are closed here. A pure-AST unreachable-statement pass (scan(): pass 6, entry point scan_unreachable()): in every statement block, a statement after return/raise/continue/break can never run. Per-block on purpose — an `else`, an except handler or a nested function is a different block and not dead, so the pass stays signal. Red proof: the pre-fix connection.py, saved from HEAD, is flagged as `connection.py:1113: unreachable-statement: return at line 1110 ends this block`; the live tree is clean. A new counter (statement_blocks, floor 500 of ~1102) keeps the "no findings" verdict from being a pass that stopped parsing. The call-argument floor was 20 while the walk counted 46, so deleting a flow-display owner from HA_CALL_OWNERS did not trip it. It is 44 now, and the suite proves the trip: removing async_show_menu yields 40 checked arguments and `only 40 call_args (floor: 44)`. Measured removals: show_form 25, abort 38, show_menu 40, create_entry 41, update_entry 41 all violate; the single-call-site async_update_reload_and_abort (45) is the one owner a count floor cannot watch, which is why the floor sits two below the count. Also (t62 nit a): the two comments that pinned the t40 entity-attribute finding to ``media_player.py:828`` now name the file and the attribute instead — the line had rotted to an unrelated property. |
||
|
|
3a55519b74 |
fix(config-flow): the setup menu was unreachable under real HA (t57 UNIT 0a)
t61 F1 (BLOCKER) + F2 + F3 + F5, all reproduced against a real FlowManager
before fixing. The OPTIONS flow was verified good and is untouched by this
commit; the SETUP flow was broken in three independent ways.
F1 — `_show_setup_menu` passed `errors=` to `async_show_menu`, which has no
such parameter (HA 2026.10.1: step_id / menu_options / sort /
description_placeholders). Every path that went near the menu therefore
raised `TypeError` the moment the first form was submitted or ADB pairing
was skipped. The path map, driven end to end: FOUR of the five ordinary
paths died before create_entry ("ADB skip", "ADB paired + protocol off",
"ADB skip + protocol skip", "pair ADB"); only "protocol only" survived, and
it never showed the menu — so a new entry could only be created on that one
path and the sub-screens were unreachable.
F2 — the menu's rows broke the rule this repo documents: HA dispatches a row
to `async_step_<row id>` AND dispatches a submitted form by its OWN step_id,
so a row named `adb` with a handler called `async_step_adb_pair` is an
UnknownStep, and a row named `receiver` rendering `step_id="setup_receiver"`
is a form that cannot be submitted. The rows are now their handler names
(`receiver`, `connection`, `adb_pair`, `protocol_pair`, `finish`) and the
two sub-screens render their row id. There is also a `_paired_adb` guard
now, so a completed ADB pairing stops offering that row — the rule the
protocol row already followed.
F3 — both menus keyed their row labels under a bare `menu`, which core does
not read (56 core strings files use `menu_options`), so every row fell back
to its raw id in the browser. Renamed in both strings files.
F5 — the per-command script merge is field-wise now: a `script_<command>`
key that is present and empty removes that command, one with a value sets
it, and a key the submission does not carry is untouched — the same rule as
every other option. The wholesale rebuild depended on the page always being
complete, so a partial submission silently wiped the commands it omitted.
THE SYSTEMIC HALF (F1), closed as asked: `ha_api_drift.HA_CALL_OWNERS` now
covers the flow-display APIs (`async_show_menu`, `async_show_progress`,
`async_show_progress_done`, plus the existing form/abort/entry owners), so
the drift engine resolves their REAL signatures and flags an unknown
keyword. `tests/test_ha_api_drift.py` carries a pre-fix source using
`errors=` to prove the new owner is non-vacuous. Every harness stub now
mirrors the real signature (no `errors` on a menu) and a stub-level test
pins that parameter set, so a stub cannot go back to accepting anything.
On the pre-fix tree the drift engine reports exactly this:
config_flow.py:898: unknown-kwarg: async_show_menu() has no 'errors'
parameter; real signature: (self, *, step_id=None, menu_options, sort,
description_placeholders) -> _FlowResultT
TESTS: tests/test_config_flow_real_ha.py gains a SETUP section that drives
the real flow — the first form, the ADB skip, the pairing guard, EVERY menu
row (dispatch AND re-submittable step_id), an accepted sub-screen
submission, a full connection+receiver+finish walk merging into one entry,
the menu's pairing note, and the five-path map as a parametrised test.
11 of those fail on the pre-fix tree; the drift engine failure above is the
12th. The stub suite additionally pins the menu stub's signature and the
`menu_options` key in both strings files.
Also in this commit (t57 F4): the real-HA file claimed entity-valued pages
could not be driven there, which was false (it has always submitted
`switch.soundbar`). The claim is gone and the scripts page is driven through
SET (two scripts selected, real entity ids) → CLEAR (one selector emptied)
→ SAVE → REOPEN, so the SET half is pinned in the real flow and not only
against the selector objects.
|
||
|
|
be3abae3e7 |
test(ci): F4 — entity-contract pass in the drift sweep + real-HA state-write proof (t40)
The F1 blocker was invisible to every existing test: the stub harnesses call the
entity and replace ``async_write_ha_state``, so a non-property
``extra_state_attributes`` looked perfectly healthy. Two new gates close that
hole.
1. ``ha_api_drift.scan_entity_contracts()`` — a pure-AST pass (so it needs no
homeassistant, unlike passes 1-3) that walks every class inheriting from an
HA entity base, including our own intermediate ``NvidiaShieldSensorBase``, and
flags any HA-READ attribute (``extra_state_attributes``, ``available``,
``state``, ``native_value``, ``media_*``, …) declared as a plain method
instead of ``@property``. It is wired into ``scan()`` before the
homeassistant gate and exported for the fast suite, so ONE implementation
backs the sweep, the CLI red proof and tests/test_entity_contract.py (the t39
F1 lesson).
Red proof on the pre-fix tree:
media_player.py:828: entity-attribute-is-a-method:
NvidiaShieldMediaPlayer.extra_state_attributes is a plain method, but HA
reads it as an attribute (``attr |= self.<name>``) — declare it @property
…and HEAD is clean (13 modules, 60 imports, 35 call arguments, 7 awaited).
2. tests/test_ha_api_drift.py now builds each entity under REAL homeassistant
(real ``PlatformData``, real ``async_write_ha_state``) and asserts HA's own
``__async_calculate_state`` succeeds and that the media player's app_id/
app_name survive the merge — for the media player, both sensors, the remote
and the pairing button. A coverage test compares that matrix against the
sweep's classification, so a new entity class cannot quietly skip it.
Verified: 296 passed / 2 skipped (fast suite), 16 passed (drift suite under
Python 3.14 + homeassistant 2026.10.1), ruff clean; the entity-contract red
proof above reproduces exactly the reported line.
|
||
|
|
fd62d850b7 |
fix(drift): F1+F2 — the sweep refuses false greens, and CI provisions the real floor (t39)
t38 found two holes in the drift gate itself. F2 — the job could never run where it mattered. Home Assistant 2026.10.1 declares ``Requires-Python >=3.14.2`` (PyPI metadata), so the old guard let a 3.13 runner through and the ``homeassistant>=2026.10`` install then failed with a resolver error. The job now provisions its own interpreter (``actions/setup-python@v5`` with ``python-version: "3.14"``) and the guard runs against the REAL floor, read from ``ha_api_drift.HOMEASSISTANT_PYTHON_FLOOR`` — one source of truth, pinned by a test against the installed distribution's own ``Requires-Python`` metadata. Verified: on 3.13.16 the guard refuses with "Home Assistant needs Python >= 3.14.2, but this job runs 3.13.16 / Fix: raise actions/setup-python…"; on 3.14.8 it reports "interpreter ok"; and ``uvx --python 3.14 --with 'homeassistant>=2026.10'`` resolves 2026.10.1. F1 — the standalone entry point was a false green. It printed counters and exited 0 even when it had walked nothing, which is exactly how the manual red-proof tool gets misused. The non-vacuity floors now live in ``ha_api_drift.FLOORS`` and are enforced in ONE place, ``Report.floor_violations()``, used by the pytest wrapper and by ``__main__`` (so the constants cannot drift apart; a test asserts the wrapper holds no floor-like constants of its own). On top of that the sweep can no longer lie in either direction: * an empty or mis-targeted directory reports ``FLOOR:`` lines and ``drift sweep FAILED`` and exits 1 (a non-existent path is a ``bad-path`` finding, not a traceback and not a silent zero-module sweep); * with no homeassistant installed it returns a single ``no-homeassistant`` finding with zero counters — never a wall of phantom "broken-import"s, never a green exit. Verified in the CI-equivalent environment (Python 3.14.8 + homeassistant 2026.10.1): HEAD exits 0 with "no Home-Assistant API drift (13 modules, 60 imports, 35 call arguments, 7 awaited HA calls)"; the empty directory exits 1 with 4 FLOOR lines and no findings; a missing path exits 1 with the ``bad-path`` finding; and the full pre-fix tree (``git archive 5ede4fe``) exits 1 with exactly the three t36 drifts (media_player.py:965 BrowseMediaSource, diagnostics.py:93 homeassistant.util.diagnostic, config_flow.py:425 missing-await) and 0 floor violations. The fast ``test`` job and ``release.yml`` are untouched; the workflow is unchanged in shape except the new provisioning + guard steps, and parses as valid YAML. |
||
|
|
824d6192a1 |
test(ci): D1/D2/D3 — repo-owned HA API-drift sweep with a CI job (t37)
The unit suite validates production modules against hand-written stubs, so a
call site that no longer matches real Home Assistant stays green — that is how
D1, D2 and D3 reached master. This adds the check that would have caught them.
`tests/ha_api_drift.py` (stdlib-only, documented inline) parses every module of
custom_components/nvidia_shield and resolves each Home-Assistant dependency
against an INSTALLED homeassistant:
1. every `homeassistant.*` import — module present, attribute present (or a
submodule of that name exists, which is what `from package import submodule`
means) — catches D1/D2 and any future move;
2. every keyword argument passed to a known HA API (HA_CALL_OWNERS: the
ConfigFlow/ConfigEntries APIs we call) must exist in the real signature —
catches the `options_updates` class of bug;
3. every `self.<method>()` call in a class that inherits from an HA class is
compared with the real definition: a coroutine must be awaited and an
awaited call must be a coroutine — catches D3.
`tests/test_ha_api_drift.py` runs it against the production tree, asserts the
sweep is not vacuous (module/import/argument/await counters), and pins the red
proof with the four pre-fix forms, plus the green proof for the fixed forms. It
skips cleanly when homeassistant is absent, so the stdlib-only jobs are
unaffected.
`.gitea/workflows/test.yml` gains the `ha-api-drift` job — the one place Home
Assistant is installed — guarded by an explicit Python >= 3.13 check (Home
Assistant 2026.x requires it) and the same PEP 668 retry the test job uses.
Red proof against the real pre-fix revisions (svn-style checkout of
`git show HEAD:…` into a temp dir, and the pre-t35 config_flow.py for the kwarg):
media_player.py:965: broken-import: from homeassistant.components.media_player
import BrowseMediaSource: …BrowseMediaSource does not exist
diagnostics.py:93: broken-import: from homeassistant.util.diagnostic import
async_redact_data: module 'homeassistant.util.diagnostic' is not importable
config_flow.py:425: missing-await: self._async_handle_discovery_without_unique_id()
is a coroutine on homeassistant.config_entries.ConfigFlow
config_flow.py:700: unknown-kwarg: async_update_reload_and_abort() has no
'options_updates' parameter (exit 1 on that tree, exit 0 on this one)
Verified: variant 1 289 passed / 2 skipped; variant 2 (androidtvremote2) 290
passed / 1 skipped; ruff clean; `UV_PYTHON=3.13 uvx --with homeassistant --with
pytest-asyncio pytest tests/test_ha_api_drift.py -q` → 4 passed (the bare form of
that command needs the pin here only because this sandbox's default Python is
3.15 and pycares has no wheel for it / no CMake to build it; CI pins the
interpreter explicitly).
|