diff --git a/hacs.json b/hacs.json index bd5be75..722e594 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "NestQuest", - "homeassistant": "2024.1.0", + "homeassistant": "2024.6.0", "render_readme": true } diff --git a/tests/test_hacs.py b/tests/test_hacs.py index 41eeb74..9d263e9 100644 --- a/tests/test_hacs.py +++ b/tests/test_hacs.py @@ -25,10 +25,16 @@ def test_hacs_json_exists_and_is_valid(hacs_config: dict) -> None: """Verify hacs.json exists and is valid JSON.""" assert isinstance(hacs_config, dict) assert hacs_config.get("name") == "NestQuest" - assert hacs_config.get("homeassistant") == "2024.1.0" + assert hacs_config.get("homeassistant") == "2024.6.0" assert hacs_config.get("render_readme") is True +def test_hacs_json_minimum_version_supports_runtime_data(hacs_config: dict) -> None: + """Advertised HA floor must support ConfigEntry.runtime_data (added in 2024.6).""" + advertised = tuple(int(part) for part in hacs_config.get("homeassistant", "").split(".")) + assert advertised >= (2024, 6, 0) + + def test_license_exists_and_non_empty() -> None: """Verify LICENSE file exists and is non-empty.""" assert LICENSE_PATH.exists(), f"LICENSE file does not exist at {LICENSE_PATH}" diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 2b8a503..5908cae 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -30,31 +30,30 @@ def _run(coro): class _ListenerRegistry: - """Models HA's update-listener bookkeeping: re-adding replaces, not accumulates.""" + """Models HA's update-listener bookkeeping: appends to a list per entry, with per-listener removers.""" def __init__(self, hass=None): self._hass = hass - self._listeners: dict[str, object] = {} + self._listeners: dict[str, list[object]] = {} self.reloaded: list[str] = [] self._loop = asyncio.new_event_loop() def add(self, entry_id: str, listener) -> object: def _remove(): - self._listeners.pop(entry_id, None) + self._listeners.get(entry_id, []).remove(listener) - self._listeners[entry_id] = listener + self._listeners.setdefault(entry_id, []).append(listener) return _remove def dispatch_options_update(self, entry) -> None: - listener = self._listeners.get(entry.entry_id) - if listener is not None: + for listener in list(self._listeners.get(entry.entry_id, [])): result = listener(self._hass, entry) if inspect.isawaitable(result): self._loop.run_until_complete(result) @property def size(self) -> int: - return len(self._listeners) + return sum(len(listeners) for listeners in self._listeners.values()) def _make_hass(): @@ -176,6 +175,31 @@ def test_three_reload_cycles_keep_single_listener() -> None: assert hass.config_entries.async_reload.call_count == 0 +def test_three_setup_unload_cycles_leave_no_listeners() -> None: + """Listeners accumulate per entry (HA semantics); unload must remove them all.""" + hass, registry = _make_hass() + entry = _wire_entry(_make_entry(), registry) + for _ in range(3): + _run(async_setup_entry(hass, entry)) + _run(async_unload_entry(hass, entry)) + assert registry._listeners.get(entry.entry_id, []) == [] + assert registry.size == 0 + + +def test_options_update_after_each_setup_invokes_exactly_one_listener() -> None: + """After each setup, an options update dispatches to exactly one listener.""" + hass, registry = _make_hass() + entry = _wire_entry(_make_entry(), registry) + for _ in range(3): + _run(async_setup_entry(hass, entry)) + registry.dispatch_options_update(entry) + assert hass.config_entries.async_reload.call_count == 1 + assert registry.reloaded == [entry.entry_id] + _run(async_unload_entry(hass, entry)) + hass.config_entries.async_reload.reset_mock() + registry.reloaded.clear() + + def test_reload_setup_cycles_dispatch_reload_each_time() -> None: hass, registry = _make_hass() entry = _wire_entry(_make_entry(), registry)