fix(poller): F1 — the poller factory re-reads entry.options at call time (audit-sanctioned option), so the next built poller targets the NEW address after a zeroconf IP change; the stale live-read comment in config_flow points at the actual mechanism; TestIpChangeSelfHealF1 drives an IP change and pins the next poller host (t32)
This commit is contained in:
1 parent
c1a82a2fa5
commit
8f40b6435c
3 files changed
+91
-2
No files matched your search
@@ -130,7 +130,18 @@ async def _build_poller_factory(hass: HomeAssistant, entry: ConfigEntry):
|
||||
)
|
||||
return None
|
||||
|
||||
return lambda: AdbMediaSessionPoller(adb.host, adb.port, adb.key_path)
|
||||
# t32 F1 (review repair): the audit-sanctioned FACTORY RE-READ — the
|
||||
# host/port resolve from entry.options AT CALL TIME, so the poller built
|
||||
# after a zeroconf IP change targets the NEW address without a reload
|
||||
# (the setup-time closure pinned the old address). A key-path change
|
||||
# still needs the Pair ADB button / reconfigure reload, by design.
|
||||
def _factory():
|
||||
adb_now = resolve_adb_config(hass, entry)
|
||||
if adb_now is None:
|
||||
return None
|
||||
return AdbMediaSessionPoller(adb_now.host, adb_now.port, adb_now.key_path)
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -363,7 +363,8 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if options != entry.options:
|
||||
# t29 G3a: the deprecated update+schedule-reload combo is
|
||||
# gone (2026.12 error) — the entry updates ONLY here; the
|
||||
# manager reads the host options live on the next cycle.
|
||||
# self-heal rides on the t32 F1 poller factory, which
|
||||
# re-reads entry.options at call time (no reload).
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry, options=options
|
||||
)
|
||||
|
||||
@@ -10,11 +10,16 @@ migration validity for v2 entries without the field.
|
||||
import asyncio # noqa: F401 (async steps need the event loop)
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path # noqa: E402 (F1 test key staging)
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from conftest import _SHIELD_DIR, _load_module
|
||||
from nsh.const import (
|
||||
CONF_ADB_ENABLED,
|
||||
CONF_ADB_HOST,
|
||||
CONF_ADB_PORT,
|
||||
CONF_ANDROIDTV_ENTITY,
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
CONF_PROTOCOL_HOST,
|
||||
@@ -758,6 +763,78 @@ class TestLegacyUniqueIdMigration:
|
||||
assert hass.config_entries.updating_entry_calls == [] # untouched
|
||||
|
||||
|
||||
class TestIpChangeSelfHealF1:
|
||||
"""t32 F1 (HIGH): the poller factory re-reads entry.options at call
|
||||
time, so the NEXT built poller targets the NEW address after a
|
||||
zeroconf IP change (the audit-sanctioned factory re-read option)."""
|
||||
|
||||
def _adb_entry(self, host="192.168.1.50"):
|
||||
return SimpleNamespace(
|
||||
entry_id="e1",
|
||||
title="Shield",
|
||||
data={CONF_ANDROIDTV_ENTITY: "media_player.shield"},
|
||||
options={
|
||||
CONF_ADB_ENABLED: True,
|
||||
CONF_ADB_HOST: host,
|
||||
CONF_ADB_PORT: 5555,
|
||||
},
|
||||
runtime_data=None,
|
||||
)
|
||||
|
||||
def _hass_with_key(self, tmp_path):
|
||||
import conftest
|
||||
import entity_stublib
|
||||
|
||||
entity_stublib.install()
|
||||
if "nsh.init" not in sys.modules:
|
||||
conftest._load_module("nsh.init", conftest._SHIELD_DIR / "__init__.py")
|
||||
init_module = sys.modules["nsh.init"]
|
||||
hass = FakeHass(str(tmp_path))
|
||||
key_dir = Path(hass.config.config_dir) / ".storage" / "nvidia_shield"
|
||||
key_dir.mkdir(parents=True, exist_ok=True)
|
||||
(key_dir / "adbkey").write_text("test-key")
|
||||
return init_module, hass
|
||||
|
||||
async def test_built_poller_targets_the_new_address(self, tmp_path):
|
||||
"""The delivered self-heal: after the zeroconf host update the
|
||||
NEXT built poller targets the new address (no reload involved)."""
|
||||
init_module, hass = self._hass_with_key(tmp_path)
|
||||
entry = self._adb_entry()
|
||||
hass.config_entries.entries = [entry]
|
||||
|
||||
factory = await init_module._build_poller_factory(hass, entry)
|
||||
assert factory is not None
|
||||
|
||||
first = factory()
|
||||
assert first is not None
|
||||
# AdbClientBase exposes the connection config as a dict (t27 F-NEW3)
|
||||
assert first.config["host"] == "192.168.1.50"
|
||||
|
||||
# the config-flow zeroconf path updates the entry's hosts via
|
||||
# async_update_entry — the NEXT built poller targets the NEW address
|
||||
entry.options = {
|
||||
**entry.options,
|
||||
CONF_ADB_HOST: "192.168.1.77",
|
||||
CONF_PROTOCOL_HOST: "192.168.1.77",
|
||||
}
|
||||
|
||||
second = factory()
|
||||
assert second is not None
|
||||
assert second.config["host"] == "192.168.1.77"
|
||||
|
||||
async def test_unresolvable_call_stays_passive(self, tmp_path):
|
||||
"""A momentary unresolvable host yields None (passive cycle) — the
|
||||
watchdog's existing None handling stays intact."""
|
||||
init_module, hass = self._hass_with_key(tmp_path)
|
||||
entry = self._adb_entry(host="10.255.255.1")
|
||||
|
||||
factory = await init_module._build_poller_factory(hass, entry)
|
||||
assert factory is not None
|
||||
|
||||
with mock.patch("nsh.init.resolve_adb_config", return_value=None):
|
||||
assert factory() is None
|
||||
|
||||
|
||||
class TestZeroconfDiscovery:
|
||||
"""P4-1a: _androidtvremote2._tcp discovery → pre-filled user step."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user