diff --git a/tests/test_config_flow_pairing.py b/tests/test_config_flow_pairing.py index 8497956..2849f6e 100644 --- a/tests/test_config_flow_pairing.py +++ b/tests/test_config_flow_pairing.py @@ -28,6 +28,20 @@ from nsh.const import ( _KEY_PATH = "/tmp/nsh-test-adbkey" +_STUB_NAMES = ( + "voluptuous", + "homeassistant", + "homeassistant.config_entries", + "homeassistant.core", + "homeassistant.helpers", + "homeassistant.helpers.entity_registry", + "homeassistant.helpers.selector", + # Loaded execution runs beneath the stubs; never escape this file either. + "nsh.helpers", + "nsh.config_flow", +) + + def _install_import_stubs() -> None: """Register import-time stand-ins for HA/voluptuous (idempotent).""" if sys.modules.get("voluptuous") is not None: @@ -139,6 +153,25 @@ def _flow(user_input_host: str, monkeypatch=None): return flow +@pytest.fixture(autouse=True) +def scoped_stubs(): + """Contain the import stubs to this module's tests (t7 INFO). + + Snapshots every injected sys.modules entry and restores or removes it on + teardown, so later tests never observe the fake homeassistant/voluptuous + (nor the stub-loaded nsh.helpers/nsh.config_flow) modules. + """ + snapshot = {name: sys.modules.get(name) for name in _STUB_NAMES} + _install_import_stubs() + yield + for name in _STUB_NAMES: + original = snapshot[name] + if original is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = original + + @pytest.fixture def mocked_test_connect(monkeypatch): from nsh.connection import ShieldConnectionManager diff --git a/tests/test_connection.py b/tests/test_connection.py index d59034f..8131182 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -211,15 +211,22 @@ class TestReconnectBackoff: manager = _manager(poller) delays = [] + backoffs = [] backoff = 5.0 for _ in range(7): + backoffs.append(backoff) delays.append(await manager._watchdog_cycle(backoff)) backoff = min(backoff * 2, 300.0) - # 5→10→20→40→80→160→300 growth with easing; the last must not exceed - # the cap (with jitter). - assert max(delays) <= 300.0 * 1.25 - assert delays[1] >= delays[0] * 1.5 + # F5 soundness: the previous ±25 % jitter could flake the claim + # delays[1] > delays[0] * 1.5: BACKOFF=10 jittered to 9.17 vs + # BACKOFF=5 jittered to 6.25 → 6.25*1.5 = 9.375). Assert only what + # jitter guarantees: each delay sits inside ITS OWN backoff's band + # (non-overlapping consecutive bands ⇒ delays[1] >= delays[0], + # since min next = 7.5 >= max first = 6.25), and never above the cap. + for backoff, delay in zip(backoffs, delays, strict=True): + assert 0.75 * backoff <= delay <= 1.25 * backoff + assert delays[1] >= delays[0] assert delays[-1] <= 300.0 * 1.25 async def test_repeated_failures_do_not_spam_listeners(self):