From fb481c3c145b2b2642aafe9936cf32c81b6fc3ac Mon Sep 17 00:00:00 2001 From: James Mackay Date: Sun, 11 Oct 2026 06:20:41 +0100 Subject: [PATCH] feat(config): enable both native transports by default (t41) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transports are ours now, so setup offers them ON rather than OFF: a fresh form renders "Read state over ADB" and "Add the Nvidia Remote Protocol" checked, with the Shield's address pre-filled by discovery (or typed by the user). The schema default, the async_step_user fallback and the _create_entry fallback all carry True, so the checkbox the user sees and the entry that gets written cannot disagree — the t33 F-A lesson applied to the new defaults. Still a choice, not a mandate: * unchecking either toggle routes to the other transport's pairing step; * each pairing step keeps its "continue without pairing" escape (the transport stays off until paired), so a passive entry is still one click away; * no_state_source now means exactly "neither transport enabled" — and a payload that names neither is REJECTED rather than silently enabling both (fail closed; a real submission always carries the schema defaults). * the YAML seed keeps t32 F4's shape rule: an address turns ADB on, a protocol-only block leaves it off, an explicit adb_enabled wins. Verified: 323 passed / 2 skipped, 324 / 1 with androidtvremote2, ruff clean, drift engine exit 0. --- .../nvidia_shield/config_flow.py | 18 +-- tests/test_config_flow_ux.py | 104 ++++++++++++++++++ 2 files changed, 114 insertions(+), 8 deletions(-) diff --git a/custom_components/nvidia_shield/config_flow.py b/custom_components/nvidia_shield/config_flow.py index cc67c26..a0ded60 100644 --- a/custom_components/nvidia_shield/config_flow.py +++ b/custom_components/nvidia_shield/config_flow.py @@ -163,13 +163,15 @@ def _user_schema_dict(sel, defaults: dict | None = None) -> dict: schema_dict[vol.Optional(CONF_POWER_ENTITY)] = sel["power"] - schema_dict[vol.Optional(CONF_ADB_ENABLED)] = bool - # the code's own fallback for this flag is True (async_step_user) — the - # rendered checkbox must agree with it + # t41: both transports are OURS and both are ON by default — the user opts + # out (or skips pairing) rather than opting in. The rendered checkboxes must + # agree with the code's fallbacks in async_step_user / _create_entry, so all + # three carry True (t33 F-A: a default the schema cannot see is a bug). + schema_dict[vol.Optional(CONF_ADB_ENABLED, default=True)] = bool schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO, default=True)] = bool schema_dict[_host_field_key(CONF_HOST, host)] = str schema_dict[vol.Optional(CONF_ADB_PORT, default=port)] = int - schema_dict[vol.Optional(CONF_PROTOCOL_ENABLED)] = bool + schema_dict[vol.Optional(CONF_PROTOCOL_ENABLED, default=True)] = bool return schema_dict @@ -249,11 +251,11 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_RECEIVER_ENTITY: user_input.get(CONF_RECEIVER_ENTITY, ""), CONF_RECEIVER_INPUT: user_input.get(CONF_RECEIVER_INPUT, ""), CONF_POWER_ENTITY: user_input.get(CONF_POWER_ENTITY, ""), - CONF_ADB_ENABLED: user_input.get(CONF_ADB_ENABLED, False), + CONF_ADB_ENABLED: user_input.get(CONF_ADB_ENABLED, True), CONF_ADB_STORE_APP_INFO: user_input.get(CONF_ADB_STORE_APP_INFO, True), CONF_HOST: str(user_input.get(CONF_HOST) or "").strip(), CONF_ADB_PORT: user_input.get(CONF_ADB_PORT, DEFAULT_ADB_PORT), - CONF_PROTOCOL_ENABLED: user_input.get(CONF_PROTOCOL_ENABLED, False), + CONF_PROTOCOL_ENABLED: user_input.get(CONF_PROTOCOL_ENABLED, True), } # If ADB is enabled, go to pairing step @@ -386,11 +388,11 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_RECEIVER_ENTITY: d.get(CONF_RECEIVER_ENTITY, ""), CONF_RECEIVER_INPUT: d.get(CONF_RECEIVER_INPUT, ""), CONF_POWER_ENTITY: d.get(CONF_POWER_ENTITY, ""), - CONF_ADB_ENABLED: d.get(CONF_ADB_ENABLED, False), + CONF_ADB_ENABLED: d.get(CONF_ADB_ENABLED, True), CONF_ADB_STORE_APP_INFO: d.get(CONF_ADB_STORE_APP_INFO, True), CONF_HOST: d.get(CONF_HOST, ""), CONF_ADB_PORT: d.get(CONF_ADB_PORT, DEFAULT_ADB_PORT), - CONF_PROTOCOL_ENABLED: d.get(CONF_PROTOCOL_ENABLED, False), + CONF_PROTOCOL_ENABLED: d.get(CONF_PROTOCOL_ENABLED, True), }, ) diff --git a/tests/test_config_flow_ux.py b/tests/test_config_flow_ux.py index fa0cb89..04c6212 100644 --- a/tests/test_config_flow_ux.py +++ b/tests/test_config_flow_ux.py @@ -581,6 +581,110 @@ class TestDiscoveryPrefill: assert defaults[CONF_HOST] == DISCOVERED_HOST +class TestTransportsDefaultOn: + """t41: the two native transports are OURS — both start enabled. + + The ruling's other half: they are still choices. Turning either off (or + skipping its pairing) stays a first-class path, and unchecking BOTH is the + only way to reach ``no_state_source``. + """ + + async def test_the_fresh_form_renders_both_toggles_checked(self, tmp_path): + module = _form_module() + flow, _hass = _new_flow(module, tmp_path) + + result = await flow.async_step_user(None) + + defaults = vol_mirror.defaults_by_key(result["data_schema"]) + assert defaults[CONF_ADB_ENABLED] is True + assert defaults[CONF_PROTOCOL_ENABLED] is True + # the ONE address field is offered, empty until discovery or typing + assert CONF_HOST in vol_mirror.declared_fields(result["data_schema"]) + + async def test_a_discovered_form_prefills_the_host_and_both_toggles( + self, tmp_path + ): + module = _form_module() + flow, _hass = _new_flow(module, tmp_path) + + result = await flow.async_step_zeroconf(_discovery()) + + defaults = vol_mirror.defaults_by_key(result["data_schema"]) + assert defaults[CONF_HOST] == DISCOVERED_HOST + assert defaults[CONF_ADB_ENABLED] is True + assert defaults[CONF_PROTOCOL_ENABLED] is True + + async def test_a_submission_naming_neither_transport_is_rejected( + self, tmp_path, monkeypatch + ): + """Fail closed: the step never assumes a transport the form did not send. + + The rendered schema carries both defaults, so a real submission always + names them (t33 F-A) — but a payload without them must not silently turn + both transports on. + """ + module = _form_module() + _patch_protocol(monkeypatch, module) + flow, _hass = _new_flow(module, tmp_path) + + result = await flow.async_step_user( + {"name": "mdarcy", CONF_HOST: DISCOVERED_HOST, CONF_ADB_PORT: 5555} + ) + + assert result["errors"] == {"base": "no_state_source"} + + async def test_the_code_fallback_matches_the_rendered_default(self, tmp_path): + """`_create_entry` records ON when its data never named the toggles. + + t33 F-A's rule: the code's fallback must reproduce what the checkbox + promised, so a partially-filled `_user_data` (a YAML seed) cannot come + out of setup with both transports off. + """ + module = _form_module() + flow, _hass = _new_flow(module, tmp_path) + flow._user_data = {"name": "mdarcy", CONF_HOST: DISCOVERED_HOST} + + result = flow._create_entry() + + assert result["options"][CONF_ADB_ENABLED] is True + assert result["options"][CONF_PROTOCOL_ENABLED] is True + + async def test_adb_alone_is_still_reachable(self, tmp_path, monkeypatch): + module = _form_module() + _patch_adb(monkeypatch, module) + flow, _hass = _new_flow(module, tmp_path) + + result = await flow.async_step_user( + _user_input(**{CONF_ADB_ENABLED: True, CONF_PROTOCOL_ENABLED: False}) + ) + + assert result["step_id"] == "adb_pair" + assert flow._user_data[CONF_PROTOCOL_ENABLED] is False + assert flow._user_data[CONF_ADB_ENABLED] is True + + async def test_protocol_alone_is_still_reachable(self, tmp_path, monkeypatch): + module = _form_module() + _patch_protocol(monkeypatch, module) + flow, _hass = _new_flow(module, tmp_path) + + result = await flow.async_step_user( + _user_input(**{CONF_ADB_ENABLED: False, CONF_PROTOCOL_ENABLED: True}) + ) + + assert result["step_id"] == "protocol_pair" + assert flow._user_data[CONF_ADB_ENABLED] is False + + async def test_neither_transport_is_the_only_error_path(self, tmp_path): + module = _form_module() + flow, _hass = _new_flow(module, tmp_path) + + result = await flow.async_step_user( + _user_input(**{CONF_ADB_ENABLED: False, CONF_PROTOCOL_ENABLED: False}) + ) + + assert result["errors"] == {"base": "no_state_source"} + + class TestNoDeadEndPairing: """F-B: both pairing steps offer 'continue without pairing'."""