test(intents): the real-HA action proof, the README pins and the raised drift floor (t17, part 3)
The evidence half of the ticket. Two things here are new facts about the
installed Home Assistant rather than about our code.
THE ACTION, DRIVEN BY REAL HOME ASSISTANT (tests/test_send_intent_real_ha.py).
The claim "the action returns the device's output" cannot be settled with a stub:
whether a service ANSWERS is HA's decision. On 2026.10.1, with the real service
registry and the real websocket command the frontend calls:
* `supports_response` is OPTIONAL, which is what keeps every caller that does not
ask for a response working — REQUIRED would break them;
* `get_services` serves the intent field as a dropdown whose options are the
registered names AND keeps `custom_value` on, so a name typed by hand is still
accepted;
* the entity target is `{entity: [{domain: [media_player]}]}` — the picker, the
same mechanism t14 proved for adb_command (async_register has no target
parameter; the runtime description is what the UI reads);
* a real `async_call(..., return_response=True)` comes back as
`{"intent_response": "Starting: Intent { … }"}`, and the command that reached the
ADB surface is asserted VERBATIM, including the `--es`/`--ez` flags and the
resolved URL;
* an unknown intent, a missing required field, a refused intent (the device's own
"does not exist" text is kept and the call RAISES), ADB being down and a missing
or unsupported target each raise ServiceValidationError with our translation key
and a message a user can act on.
The dropdown is published from `hass.data[DOMAIN]` — the store a real entry setup
fills — so the file also pins the EMPTY state: nothing stored means no options, not
invented ones.
THE OPTIONS SCREEN, DRIVEN BY THE REAL FLOW MANAGER
(tests/test_config_flow_real_ha.py). The Intents menu, the Add-new sentinel as a
real selector option, a registration that round-trips to `create_entry` (JSON-safe,
as the options store requires), a refused row coming back as a real form error with
its note, and a removal that only takes effect after the confirmation. This is the
file that exists because a step id which does not match its method name is an
UnknownStep that only a real FlowManager raises.
THE FLOOR (tests/ha_api_drift.py). The sweep now walks 26 modules, 77 imports, 282
relative imports, 65 call arguments and 1637 statement blocks (was 76/242/57/1417),
so the non-vacuity floors move with it: imports 70→75, relative_imports 235→275,
call_args 55→63, modules 20→24, statement_blocks 1400→1620. They stay just under
the walk on purpose — a floor's job is to notice a walk that silently stopped
covering the new surfaces, and the unreachable-statement pass is exactly the gate
that must keep covering the ~240 new blocks. Raised, never lowered.
THE README (tests/test_readme.py). The intents example is parsed and its keys are
required to be ones the seed honours (documented-but-ignored is the failure this
catches, t30's rule applied to the nested block), the screen and the action are
named, and every honest failure the README promises has its key. Plus a cross-check
in the intents suite: the row keys the README documents are exactly the keys the
seed validator KEEPS, so the docs cannot describe a key that silently vanishes.
Verify (all five, in the contract order):
uvx --with pytest-asyncio pytest tests -q → 872 passed, 5 skipped
uvx --with pytest-asyncio --with androidtvremote2 pytest tests -q → 873 passed, 4 skipped
uvx --python 3.14 --with 'homeassistant>=2026.10' --with adb-shell \
--with pytest-asyncio pytest tests/test_config_flow_real_ha.py -q → 49 passed
uvx ruff check custom_components tests → clean
uvx --python 3.14 --with 'homeassistant>=2026.10' --with pyyaml \
python tests/ha_api_drift.py → exit 0, no drift
The real-HA pattern collected and RAN three files (no skips):
tests/test_adb_command_real_ha.py, tests/test_config_flow_real_ha.py,
tests/test_send_intent_real_ha.py → 74 passed.
This commit is contained in:
1 parent
259a6c29bd
commit
e6c6bdb1e6
5 files changed
+760
-11
No files matched your search
+20
-7
@@ -142,24 +142,30 @@ HA_CALL_OWNERS: dict[str, tuple[str, str]] = {
|
||||
|
||||
#: Non-vacuity floors — the ONE source of truth for the standalone entry point
|
||||
#: and the pytest wrapper (t39 F1). They sit below the current walk (26 modules,
|
||||
#: 76 imports, 242 relative imports, 57 checked arguments, 6 awaited HA calls,
|
||||
#: 1417 statement blocks) so a broken or mis-targeted walk fails loudly instead of
|
||||
#: 77 imports, 282 relative imports, 65 checked arguments, 6 awaited HA calls,
|
||||
#: 1637 statement blocks) so a broken or mis-targeted walk fails loudly instead of
|
||||
#: printing zeros and exiting 0.
|
||||
FLOORS: dict[str, int] = {
|
||||
# t41: the sweep is recursive, so the floor sits above the top-level-only
|
||||
# count (13) — a regression to the shallow glob trips it.
|
||||
"modules": 20,
|
||||
# t17: raised 20 -> 24, just under the current 26.
|
||||
"modules": 24,
|
||||
# t15: raised from 40/100 to just under the current 76/206, because the remote
|
||||
# parity work is an IMPORTS-and-coupling feature: `RemoteEntityFeature`,
|
||||
# `ATTR_ACTIVITY`, the transport-state accessors and the shared app-directory
|
||||
# helpers all arrive as imports, and none of them may vanish unnoticed.
|
||||
"imports": 70,
|
||||
# t17: raised 70 -> 75, just under the current 77 (the intents work imports
|
||||
# the scan/status vocabulary into the flow and the engine into the platform).
|
||||
"imports": 75,
|
||||
# t16: raised 190 -> 195 -> 235, again just under the walk. The intent
|
||||
# discovery module adds the package's exports to `ha_adb_common`'s surface and
|
||||
# is imported through it by the manager, so the relative-import count is exactly
|
||||
# where a new module must show up — a floor left behind would let the manager's
|
||||
# import of it disappear unnoticed.
|
||||
"relative_imports": 235,
|
||||
# t17: raised 235 -> 275, just under the current 282. The registered-intent
|
||||
# model, its statuses and its helpers move between modules, so this is again
|
||||
# exactly where a detached import must show up.
|
||||
"relative_imports": 275,
|
||||
# t62 F2: raised from 20 to just under the current 46. A floor's job is to
|
||||
# notice an API OWNER going away, not only a broken walk: at 20, deleting
|
||||
# `async_show_menu` from HA_CALL_OWNERS left 40 checked arguments and still
|
||||
@@ -176,7 +182,10 @@ FLOORS: dict[str, int] = {
|
||||
# t15: raised from 52 to just under the current 57 — the options-flow pairing
|
||||
# step is a real flow-display surface of its own (its own async_show_form with
|
||||
# step_id/data_schema/errors), so those call sites must not be able to vanish.
|
||||
"call_args": 55,
|
||||
# t17: raised 55 -> 63, just under the current 65 — the Intents screen adds
|
||||
# real flow-display call sites of its own (its menu, its three forms and the
|
||||
# scan's result step), none of which may vanish unnoticed.
|
||||
"call_args": 63,
|
||||
"awaited_calls": 5,
|
||||
# t62 F2: the unreachable-statement pass walks ~1300 statement blocks. A
|
||||
# floor here means a pass that silently stopped parsing cannot look green.
|
||||
@@ -185,7 +194,11 @@ FLOORS: dict[str, int] = {
|
||||
# t16: raised 1200 -> 1300 -> 1400: the parser, the cache and the manager's
|
||||
# scan path are new statement blocks, and the unreachable pass must keep
|
||||
# covering them.
|
||||
"statement_blocks": 1400,
|
||||
# t17: raised 1400 -> 1620, just under the current 1637. The screen, the seed
|
||||
# validator, the command builder and the button are new statement blocks, and
|
||||
# the unreachable pass is what keeps covering them — which is the point of the
|
||||
# floor: a pass that silently stopped parsing must not look green.
|
||||
"statement_blocks": 1620,
|
||||
}
|
||||
|
||||
#: The interpreter floor Home Assistant itself declares: PyPI metadata for
|
||||
|
||||
Reference in new issue
Block a user