diff --git a/custom_components/nestquest/__init__.py b/custom_components/nestquest/__init__.py index 2cfea93..78441f0 100644 --- a/custom_components/nestquest/__init__.py +++ b/custom_components/nestquest/__init__.py @@ -460,6 +460,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: database = await _async_open_database(hass, await async_get_db_path(hass)) remove_update_listener: Callable[[], Any] | None = None remove_time_change_listener: Callable[[], Any] | None = None + coordinator: Any = None try: # Seed the admin allowlist on first setup so the owner is never # locked out: an empty database takes the persisted admin copy @@ -528,6 +529,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await result except BaseException: pass + # A coordinator created before the failure holds entity + # listeners pointed at the database being closed below — + # shut it down FIRST so no scheduled refresh can ever run + # against a closed connection. + if coordinator is not None: + try: + await coordinator.async_shutdown() + except BaseException: + pass await database.close() raise assert remove_update_listener is not None diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 414273a..745c53a 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -3,6 +3,8 @@ from __future__ import annotations import datetime +import pytest + from conftest import wire_entry_to_registry from custom_components.nestquest import async_setup_entry, async_unload_entry @@ -242,3 +244,38 @@ async def test_coordinator_notifies_listeners_on_refresh( await coordinator.async_refresh() assert called == ["entity"] assert coordinator.last_update_success is True + + +async def test_failed_first_refresh_shuts_coordinator_down( + hass, make_entry, monkeypatch +) -> None: + """A setup failure after coordinator creation must not leak its + listeners against the database the except path closes.""" + from custom_components.nestquest import async_setup_entry + import custom_components.nestquest.coordinator as coordinator_module + + shutdown_calls = [] + + async def _record_shutdown(self): + shutdown_calls.append(self.entry_id) + + monkeypatch.setattr( + coordinator_module.NestQuestCoordinator, + "async_shutdown", + _record_shutdown, + ) + + async def _boom(self): + raise RuntimeError("refresh failed") + + monkeypatch.setattr( + coordinator_module.NestQuestCoordinator, + "_async_update_data", + _boom, + ) + + entry = wire_entry_to_registry(make_entry(), hass.registry) + with pytest.raises(RuntimeError, match="refresh failed"): + await async_setup_entry(hass, entry) + assert shutdown_calls == [entry.entry_id] + assert getattr(entry, "runtime_data", None) is None