fix: cancel both listeners independently on unload (NQ-80-001)

This commit is contained in:
2026-09-17 15:09:17 -04:00
parent 8bee1bbb41
commit 3a15d8077b
2 changed files with 39 additions and 5 deletions
+12 -5
View File
@@ -423,11 +423,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
):
if remove_listener is None:
continue
result = remove_listener()
if inspect.isawaitable(result):
await result
except BaseException as err:
unload_error = err
try:
result = remove_listener()
if inspect.isawaitable(result):
await result
except BaseException as err:
# Cancel EVERY listener even when an earlier remover
# raises: a skipped remover would leave a daily
# callback firing against a closed database after
# unload. The first error is retained and re-raised
# only after both removers (and the DB close) ran.
if unload_error is None:
unload_error = err
finally:
database = getattr(runtime_data, "database", None)
if database is not None:
+27
View File
@@ -241,6 +241,33 @@ async def test_unload_closes_database_even_when_listener_removal_raises() -> Non
assert database.connected is False, "database leaked despite remover error"
assert entry.runtime_data is None
assert DOMAIN not in hass.data
assert hass.time_change.size == 0, "time-change listener leaked"
async def test_unload_cancels_time_listener_even_when_update_remover_raises() -> None:
"""A raising update-listener remover must not skip the time-change cancel.
Both removers are independent: the update-listener remover raising must
still cancel the daily time-change callback, whose action would otherwise
fire after unload against a closed database.
"""
class _RemoveBoom(Exception):
pass
def _remover():
raise _RemoveBoom()
entry = make_config_entry(entry_id="update-boom-time-cancel")
entry.add_update_listener = lambda listener: _remover
hass, _registry = make_hass()
await async_setup_entry(hass, entry)
assert hass.time_change.size == 1
with pytest.raises(_RemoveBoom):
await async_unload_entry(hass, entry)
assert hass.time_change.size == 0, "time-change listener not cancelled"
assert entry.runtime_data is None
assert DOMAIN not in hass.data
async def test_setup_registers_day_rollover_listener(hass, make_entry) -> None: