The user's live run could not select his Shield for
nvidia_shield.adb_command, so he ran his diagnostics through the deprecated
androidtv integration instead. Measured against the installed HA (2026.10.1),
the cause is a contract, not a bug in the handler:
* ServiceRegistry.async_register has NO target parameter, so a target cannot be
declared at registration;
* the frontend renders the target picker from the service DESCRIPTION, which
HA builds from services.yaml or from the runtime async_set_service_schema —
and the runtime description WINS.
So async_setup now registers with supports_response=OPTIONAL and publishes
ADB_COMMAND_SERVICE_DESCRIPTION (target: {entity: [{domain: media_player}]},
fields, prose mirrored in strings.json/translations/en.json and pinned against
both by a test). No services.yaml is needed, and none is added.
The target resolution is strict, because the failure mode of guessing is a raw
shell command on the wrong device: exactly one PARTICIPANT, told apart as
no_target (nothing selected), unsupported_target (not a participant / option
off / another integration's entity) and ambiguous_target (two selected — sent
to neither). Accepts what HA actually sends: entity_id as text or as the LIST a
picked target produces, and device_id (an automation targeting the DEVICE),
expanded only through this integration's own registry entries and only to media
players. The output now comes back in the response under adb_response — the key
the user's captures read — while an unreachable Shield stays adb_unavailable,
an error rather than an empty response, and a call that does not ask for a
response is unchanged.
Evidence: 12 real-HA tests (new tests/test_adb_command_real_ha.py) drive the
real service registry, the real get_services websocket payload (the exact JSON
bytes the browser parses: target present, response optional, both fields) and
real async_call for every shape above. t14's CI gate globs tests/test_*real_ha.py
so a new real-HA file cannot skip silently; the skip/empty-collection guards stay
and were executed. The real-HA probe also caught async_entries_for_device being
a MODULE-LEVEL helper (not a registry method) that the stub had hidden, so the
drift gate's owner map now covers async_register, async_set_service_schema and
async_entries_for_device, with the call-argument floor raised 44 -> 52.
Verify: suites 681/4 and 682/3; real-HA 38 + the new file 12 (50 with the glob);
ruff clean; drift exit 0 (54 call arguments).
A subset of test files in an order the suite was not written for failed 3
test_init.py::TestImportStep tests with ModuleNotFoundError: voluptuous, while the
full suite stayed green. Two order-only mechanisms, both now fixed at the source:
1. entity_stublib.install() guarded on ONE sentinel
(`homeassistant.components.media_player`). A file whose teardown popped
`voluptuous` but left the sentinel turned the next file's install() into a no-op,
so importing config_flow failed. The guard now checks every face a file scopes on
(`_STUB_FACES`, derived from ENTITIES_STUB_NAMES so the two cannot drift) and
rebuilds the set when anything is missing. install() also registers the
`homeassistant.helpers.selector` face itself, which the per-file installers had
been adding separately — without it the derived list would never be complete.
2. Teardowns restored only the names their file had listed, so a module the file
loaded (`nsh.diagnostics`, `nsh.media_player`) survived into the next file still
holding stub objects — a stub exception class, the redact helper — that had since
been replaced; comparisons against the new objects then failed (5 tests in
test_command_routing on an unlucky order). Every file now ends through
entity_stublib.snapshot_modules()/restore_modules(), which drops EVERYTHING the
file created under the harness namespaces (homeassistant/voluptuous/nsh) and puts
back everything it replaced. Hand-written lists were the bug, so there is none to
keep in sync.
The t7 no-leak discipline is unchanged: a file's stubs still never reach another
file, and a file's loaded modules still do not survive it. A session-scoped baseline
install was tried first and rejected: files whose own installer guards on "the stubs
are already there" skipped installing their richer faces, which produced exactly the
inconsistent state a baseline is meant to prevent (10 test_standalone_setup failures).
tests/test_harness_scoping.py: the sentinel poisoning, the wholesale-rebuild
invariant, a teardown that removes what the file loaded, the stale-object case
(diagnostics vs the redact helper), and — because order is the thing under test —
two subprocess runs of the reported orderings.
Verified: the reported order (test_command_routing, test_protocol_transport,
test_standalone_setup, test_init) 152 passed / 1 skipped; the minimal pair both ways;
the previously failing 6-test order now 167 passed; every test file in REVERSE
alphabetical order 369 passed / 2 skipped; two scrambled thirds green; full suite
369 / 2 and 370 / 1 with androidtvremote2; ruff clean.
The two address fields were the same Shield spelled twice, and every bug in the
zeroconf/IP-change path came from keeping them in sync. One field now:
* const.CONF_HOST replaces CONF_ADB_HOST / CONF_PROTOCOL_HOST in the setup form,
the options page, the pairing forms, the reconfigure/reauth seeding and the
description copy. ADB port 5555 stays the only port field;
* helpers.entry_host(entry) is the single resolver (options first, then data);
resolve_adb_config and the protocol settings both go through it, so both
transports read the same value at call time (the t32 F1 factory re-read keeps
working: the NEXT built poller sees the new address);
* helpers.seed_host / seed_adb_enabled own the YAML seed's shape rule. A block
may use `host` or the legacy protocol_host/adb_host keys — t41 keeps the old
spelling working — and t32 F4's outcome is unchanged: a protocol-only block
leaves ADB off, an address (or an explicit adb_enabled) turns it on;
* the zeroconf IP-change path collapses the t21-F2 per-host dance into one rule:
the discovery address IS the entry's address, so the entry is updated when it
differs and left alone when it matches. The blank-self-heal and
deliberately-different-ADB-host cases are gone with the duplication — a
second address field cannot express them anymore, and the tests now pin that
(previously "an explicit override is never clobbered");
* the options page exposes the protocol toggle it never had (the page could turn
ADB off but not the protocol);
* button.py (granted to this task) reads the entry's own address instead of
another integration's entity.
The v3 entry migration that maps existing adb_host/protocol_host onto CONF_HOST
and drops the androidtv key lands in the next commit; HA runs migrations before
setup, so an existing entry is never read with the old shape.
Verified: 311 passed / 2 skipped, 312 / 1 with androidtvremote2, ruff clean,
drift engine exit 0 on HEAD. The UI-copy contract tests (every rendered field
has a label in strings.json AND translations/en.json, key trees identical, host
errors name the field) drove the string edits.