fix(config): degrade an unreachable Shield to the pairing form (t44 F1)
`_start_protocol_pairing` called androidtvremote2's cert generation and `async_start_pairing` with no try/except, so `CannotConnect` propagated straight out of `async_step_protocol_pair`. The caller already had the shape for a soft failure — `_async_begin_protocol_pairing()` returns `(client, None) | (None, error)` and `_async_restart_protocol_pairing` renders `errors["base"]` — the exception simply bypassed it, and the protocol_pair form never appeared: not the address field, not the "continue without pairing" escape. That made it a default-path crash rather than an edge case: t41 turned the protocol transport ON by default, so an asleep, rebooting or mis-addressed Shield (whenever someone adds the integration, or skips ADB pairing into this step) raised out of the flow instead of showing the form. The reviewer reproduced both under real HA. Now the step returns `(None, "protocol_unreachable")`, logs the address and the library error, discards the half-built client so a retry starts clean, and renders the same form the other failures render — with the address editable and the skip option intact. `protocol_unreachable` is documented in strings.json AND translations/en.json (a form error without copy shows a bare key in the UI). Tests (tests/test_config_flow_ux.py::TestUnreachableShieldDuringPairing): the fake client can now fail to start pairing, and five cases cover fresh setup, the ADB-skip route into the protocol step, reconfigure, reauth and the skip path that still finishes the entry — all as FORMS, never exceptions. Red proof on HEAD: the same drive raises CannotConnectStubError out of async_step_user. Verified: 361 passed / 2 skipped, 362 / 1 with androidtvremote2, ruff clean.
This commit is contained in:
1 parent
7608518d97
commit
3175582419
5 files changed
+240
-16
No files matched your search
+52
-2
@@ -172,9 +172,45 @@ class RemoteEntity:
|
||||
return None
|
||||
|
||||
|
||||
#: Every face install() registers. The guard below checks the WHOLE set, not one
|
||||
#: sentinel: test files scope their stubs per file (snapshot + pop at teardown, the
|
||||
#: t7 discipline), so one file can leave `homeassistant.components.media_player`
|
||||
#: behind while another name it popped — e.g. `voluptuous` — is gone. A sentinel
|
||||
#: guard then made install() a no-op in the next file, and whichever module needed
|
||||
#: the missing face failed with ModuleNotFoundError depending on file order (t44).
|
||||
_STUB_FACES = (
|
||||
"voluptuous",
|
||||
"homeassistant",
|
||||
"homeassistant.config_entries",
|
||||
"homeassistant.core",
|
||||
"homeassistant.const",
|
||||
"homeassistant.exceptions",
|
||||
"homeassistant.components",
|
||||
"homeassistant.components.media_player",
|
||||
"homeassistant.components.remote",
|
||||
"homeassistant.components.button",
|
||||
"homeassistant.components.sensor",
|
||||
"homeassistant.helpers",
|
||||
"homeassistant.helpers.event",
|
||||
"homeassistant.helpers.entity",
|
||||
"homeassistant.helpers.entity_platform",
|
||||
"homeassistant.helpers.device_registry",
|
||||
"homeassistant.helpers.entity_registry",
|
||||
"homeassistant.helpers.redact",
|
||||
"homeassistant.helpers.selector",
|
||||
)
|
||||
|
||||
|
||||
def install() -> None:
|
||||
"""Register the stub modules (idempotent)."""
|
||||
if "homeassistant.components.media_player" in sys.modules:
|
||||
"""Register the stub modules (idempotent, and self-healing).
|
||||
|
||||
Callers must be able to rely on this regardless of what a previous test file
|
||||
popped: a missing face rebuilds the whole set, so any subset or file order
|
||||
works. A complete set is left alone — a file that installed richer faces of
|
||||
its own (tests/test_config_flow_ux.py's voluptuous mirror, the discovery
|
||||
stub) keeps them.
|
||||
"""
|
||||
if all(name in sys.modules for name in _STUB_FACES):
|
||||
return
|
||||
|
||||
vol = types.ModuleType("voluptuous")
|
||||
@@ -455,6 +491,20 @@ def install() -> None:
|
||||
sys.modules["homeassistant.helpers.event"] = event
|
||||
sys.modules["homeassistant.helpers.entity"] = entity
|
||||
sys.modules["homeassistant.helpers.entity_platform"] = entity_platform
|
||||
|
||||
# t44: config_flow imports `selector` from this package, and the per-file stub
|
||||
# installers register it as its own face. Installing it here too keeps
|
||||
# `_STUB_FACES` honest: install() then really does provide every face a test
|
||||
# file may have popped, which is what its guard checks.
|
||||
selector = types.ModuleType("homeassistant.helpers.selector")
|
||||
selector.EntitySelector = lambda *args, **kwargs: None
|
||||
selector.EntitySelectorConfig = lambda *args, **kwargs: ""
|
||||
selector.SelectSelector = lambda *args, **kwargs: None
|
||||
selector.SelectSelectorConfig = lambda **kwargs: ""
|
||||
selector.SelectSelectorMode = types.SimpleNamespace(DROPDOWN="dropdown")
|
||||
helpers.selector = selector
|
||||
sys.modules["homeassistant.helpers.selector"] = selector
|
||||
|
||||
sys.modules["homeassistant.helpers"] = helpers
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user