feat(config)!: migrate entries to v3 — one address, no androidtv key (t41)
ENTRY_VERSION = 3, declared once in const.py and reused as the flow's VERSION so the stored shape and the migration cannot drift. v2 → v3 mirrors the two shape changes: * the duplicated address fields collapse into CONF_HOST — the protocol host wins, else the ADB host (both name the same Shield), reading options first and data second so a hand-written seed is covered too; * the androidtv-entity key is dropped from data AND options with a logged note naming the dropped entity (the old value would only mislead now that nothing reads it). A v1 entry still travels the whole way in one call (the v1→v2 protocol defaults are applied, then the v3 collapse). Downgrades (version > 3) are rejected so HA can offer a rollback, and the current version is a no-op. Existing entries carrying the entity key keep working until the migration runs: nothing reads it anymore (the c1 commit), which is exactly why the migration can be a plain removal. Test note: my first version of the version-parity pin called a bare entity_stublib.install() and leaked the shallow stub set into the entity-module loader of later tests (STATE_PAUSED/entity_available import failures). It now uses the file's scoped harness, like every other test there. Verified: 316 passed / 2 skipped, 317 / 1 with androidtvremote2, ruff clean, drift engine exit 0 on HEAD (36 call arguments — one more awaited/checked call).
This commit is contained in:
1 parent
75ebeb40b0
commit
81f70585cd
5 files changed
+187
-46
No files matched your search
@@ -9,6 +9,7 @@ migration validity for v2 entries without the field.
|
||||
|
||||
import asyncio # noqa: F401 (async steps need the event loop)
|
||||
import gc
|
||||
import logging
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
@@ -414,48 +415,73 @@ class TestAdbOnlyStandalone:
|
||||
assert result["errors"] == {"base": "no_state_source"}
|
||||
|
||||
|
||||
class TestMigrationKeepsOrAllowsAbsence:
|
||||
async def test_v1_to_v2_keeps_the_androidtv_field(self, tmp_path):
|
||||
class TestMigrationDropsTheEntityKey:
|
||||
"""t41: the androidtv key is dropped from BOTH dicts with a logged note.
|
||||
|
||||
An entry that still carries it keeps working until then (nothing reads it) —
|
||||
the migration is what removes the last trace of the entity path.
|
||||
"""
|
||||
|
||||
async def test_v1_entry_reaches_v3_without_the_entity_key(self, tmp_path, caplog):
|
||||
module = _entry_module()
|
||||
hass = FakeHass(str(tmp_path))
|
||||
entry = SimpleNamespace(
|
||||
version=1,
|
||||
data={CONF_ANDROIDTV_ENTITY: "media_player.shield"},
|
||||
options={},
|
||||
options={CONF_ANDROIDTV_ENTITY: "media_player.shield"},
|
||||
title="mdarcy",
|
||||
)
|
||||
|
||||
ok = await module.async_migrate_entry(hass, entry)
|
||||
with caplog.at_level(logging.INFO):
|
||||
ok = await module.async_migrate_entry(hass, entry)
|
||||
|
||||
assert ok is True
|
||||
assert entry.data[CONF_ANDROIDTV_ENTITY] == "media_player.shield" # kept
|
||||
# options were rewritten with the protocol defaults; the data dict —
|
||||
# including the androidtv field — was never touched
|
||||
assert hass.config_entries.updating_entry_calls == [
|
||||
(
|
||||
entry,
|
||||
{
|
||||
"options": {CONF_PROTOCOL_ENABLED: False, CONF_HOST: ""},
|
||||
"version": 2,
|
||||
},
|
||||
)
|
||||
]
|
||||
assert CONF_ANDROIDTV_ENTITY not in entry.data
|
||||
assert CONF_ANDROIDTV_ENTITY not in entry.options
|
||||
assert entry.options == {CONF_PROTOCOL_ENABLED: False, CONF_HOST: ""}
|
||||
assert entry.data == {}
|
||||
# the note names what was dropped, so the log is actionable
|
||||
notes = [record.getMessage() for record in caplog.records]
|
||||
assert any("media_player.shield" in note for note in notes), notes
|
||||
assert hass.config_entries.updating_entry_calls[0][1]["version"] == 3
|
||||
|
||||
async def test_v2_without_the_field_is_valid(self, tmp_path):
|
||||
"""Standalone entries born v2 must pass through migration untouched."""
|
||||
async def test_an_entry_without_the_key_logs_plainly(self, tmp_path, caplog):
|
||||
module = _entry_module()
|
||||
hass = FakeHass(str(tmp_path))
|
||||
entry = SimpleNamespace(version=2, data={}, options={}, title="mdarcy")
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
ok = await module.async_migrate_entry(hass, entry)
|
||||
|
||||
assert ok is True
|
||||
notes = [record.getMessage() for record in caplog.records]
|
||||
assert any("Migrated mdarcy config entry to version 3" in note for note in notes), notes
|
||||
|
||||
async def test_v2_entry_with_a_legacy_address_migrates_to_the_one_field(self, tmp_path):
|
||||
"""An entry written before the unification lands on CONF_HOST."""
|
||||
module = _entry_module()
|
||||
hass = FakeHass(str(tmp_path))
|
||||
entry = SimpleNamespace(
|
||||
version=2,
|
||||
data={},
|
||||
options={CONF_PROTOCOL_ENABLED: True, CONF_HOST: "shield.local"},
|
||||
options={CONF_PROTOCOL_ENABLED: True, "protocol_host": "shield.local"},
|
||||
title="mdarcy",
|
||||
)
|
||||
|
||||
ok = await module.async_migrate_entry(hass, entry)
|
||||
|
||||
assert ok is True
|
||||
assert hass.config_entries.updating_entry_calls == [] # no rewrite needed
|
||||
assert entry.options[CONF_HOST] == "shield.local"
|
||||
assert "protocol_host" not in entry.options
|
||||
|
||||
def test_the_stored_version_and_the_flow_cannot_drift(self, tmp_path):
|
||||
# _form_module owns the scoped stub install/pop — never a bare
|
||||
# entity_stublib.install() here: a shallow stub set leaks into the
|
||||
# entity-module loader of later tests.
|
||||
module = _form_module()
|
||||
from nsh.const import ENTRY_VERSION
|
||||
|
||||
assert module.NvidiaShieldConfigFlow.VERSION == ENTRY_VERSION == 3
|
||||
|
||||
|
||||
def _cmd_player(modules):
|
||||
|
||||
Reference in new issue
Block a user