Harden the cloud-courses toggle against three review findings
- _on_cloud_courses_changed cleared the canonical-view cache but never
pushed state: select.py's current_option reads coordinator.data,
which only moves on async_set_updated_data, so a change here (the
new toggle, or apply_cloud_courses naming a program -- which has
called this same method since before the toggle existed) sat stale
in the UI until an unrelated poll or observe happened to run next.
Now calls _push_cache_snapshot() too.
- _refresh_cloud_course_issue now runs from __init__.py's
options-update listener on every entry save, not just a
cloud-course-specific one. Saving an unrelated option
(CONF_BYPASS_REMOTE_CONTROL, say) before this device's first poll,
or while it's rehydrated offline, read /course/vs/0 as empty --
indistinguishable from "nothing pending" -- and would delete a
Repair a real poll had every reason to raise. Now a no-op on an
empty rep, leaving whatever issue state already exists untouched
until a real poll can judge it.
- The "cloud_courses" menu's off-state note was a raw English string
built in config_flow.py and substituted via description_placeholders
into all 7 locales' descriptions -- unlike the SmartThings screen
names quoted elsewhere (deliberately English everywhere; that's a
third-party app's own label, not ours), this one named LocalThings'
own "Offer downloaded cycles"/"Device settings" labels, which are
translated per locale and should have matched. Replaced with a
permanent, state-independent sentence translated in the catalog
itself, in all 7 locales, instead of conditional Python-built text.
Also restores a word an earlier edit dropped from
async_step_cloud_courses's docstring ("can complete confidently").
Tests: two new regression tests, each confirmed to fail against the
pre-fix code before being fixed -- one drives coordinator.data through
a toggle via a fixture already sitting on a one-time cloud override, so
current_option actually depends on the cloud store instead of falling
back to the raw course code; the other simulates a second coordinator
against the same entry with an empty resource cache (a not-yet-polled
restart) and confirms an existing Repair survives an unrelated option
save. Full suite (1585 tests), ruff, and `ty check custom_components
tests` all pass.
This commit is contained in:
10 files changed
+89
-22
No files matched your search
@@ -332,6 +332,51 @@ async def test_disabling_does_not_stop_passive_observation(hass: HomeAssistant):
|
||||
assert entry.data[CONF_CLOUD_COURSES]["slots"]["55"]["blob"] == SPORTS
|
||||
|
||||
|
||||
async def test_disabling_pushes_the_new_current_option_immediately(hass: HomeAssistant):
|
||||
"""select.py's current_option reads coordinator.data, not
|
||||
canonical_resources() -- clearing only the canonical-view cache left
|
||||
it stale until whatever poll happened to run next. The fixture is
|
||||
sitting on a one-time Jeans ('6B') override, so naming it is what
|
||||
makes the live selection actually depend on the cloud store rather
|
||||
than falling back to the raw course code."""
|
||||
entry = _entry(hass)
|
||||
coordinator = await _coordinator(hass, entry)
|
||||
coordinator.apply_cloud_courses({"55": "Sports", "6B": "Jeans"}, "87")
|
||||
await _flush(hass)
|
||||
assert coordinator.data["cycle"] == "cloud:6B"
|
||||
|
||||
hass.config_entries.async_update_entry(entry, options={CONF_CLOUD_COURSES_ENABLED: False})
|
||||
coordinator._on_cloud_courses_changed()
|
||||
|
||||
assert coordinator.data["cycle"] == "87"
|
||||
|
||||
|
||||
async def test_an_unrelated_option_save_before_first_poll_does_not_clear_a_real_repair(
|
||||
hass: HomeAssistant,
|
||||
):
|
||||
"""_on_cloud_courses_changed now runs from __init__.py's options-update
|
||||
listener on *every* entry save, not just a cloud-course-specific one --
|
||||
including one made before this device's first poll (a fresh restart,
|
||||
still rehydrating). /course/vs/0 unpolled reads as an empty rep, which
|
||||
must not be treated as evidence that nothing is pending: that would
|
||||
delete a Repair a real poll had every reason to raise."""
|
||||
entry = _entry(hass)
|
||||
coordinator = await _coordinator(hass, entry)
|
||||
coordinator._refresh_cloud_course_issue()
|
||||
await _flush(hass)
|
||||
registry = ir.async_get(hass)
|
||||
issue_id = f"cloud_courses_{entry.entry_id}"
|
||||
assert registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
# A second coordinator against the same entry, standing in for a
|
||||
# restart that hasn't polled yet -- its own resource cache is empty.
|
||||
fresh = LocalThingsCoordinator(hass, entry)
|
||||
fresh._on_cloud_courses_changed()
|
||||
await _flush(hass)
|
||||
|
||||
assert registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Options flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user