diff --git a/homeassistant/components/sandbox/proxy_flow.py b/homeassistant/components/sandbox/proxy_flow.py index 251d14283ec5..f70e361ee3b1 100644 --- a/homeassistant/components/sandbox/proxy_flow.py +++ b/homeassistant/components/sandbox/proxy_flow.py @@ -193,7 +193,21 @@ class SandboxFlowProxy(ConfigFlow): if result_type is FlowResultType.CREATE_ENTRY: entry_data = struct_to_dict(result.data) + options = ( + struct_to_dict(result.options) if result.HasField("options") else None + ) self._terminated = True + # ``async_create_entry`` stamps the created result's + # ``version``/``minor_version`` from ``self.VERSION``/ + # ``self.MINOR_VERSION`` (read off the instance, not the class — + # see ``ConfigFlow.async_create_entry``). Override the proxy + # instance's values with the sandbox flow's so the entry carries + # the integration's real schema version; otherwise the proxy's + # default ``VERSION=1`` triggers a spurious migration on next setup. + if result.HasField("version"): + self.VERSION = result.version + if result.HasField("minor_version"): + self.MINOR_VERSION = result.minor_version create_result = self.async_create_entry( title=( result.title @@ -205,6 +219,7 @@ class SandboxFlowProxy(ConfigFlow): result.description if result.HasField("description") else None ), description_placeholders=placeholders, + options=options, ) # Tag the FlowResult so the framework's entry constructor in # ``ConfigEntriesFlowManager.async_finish_flow`` reads it into diff --git a/sandbox/hass_client/tests/test_flow_runner.py b/sandbox/hass_client/tests/test_flow_runner.py index 5109733a4558..cac00456c6ec 100644 --- a/sandbox/hass_client/tests/test_flow_runner.py +++ b/sandbox/hass_client/tests/test_flow_runner.py @@ -78,6 +78,22 @@ class _DemoFlow(ConfigFlow, domain="phase4_demo"): ) +class _VersionedFlow(ConfigFlow, domain="phase4_versioned"): + """A one-step flow with a non-default VERSION/MINOR_VERSION and options.""" + + VERSION = 2 + MINOR_VERSION = 3 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + return self.async_create_entry( + title="Versioned", + data={"host": "1.2.3.4"}, + options={"poll_interval": 30}, + ) + + @pytest.fixture(name="channels") async def _channels_fixture() -> tuple[Channel, Channel]: main, sandbox = _make_channel_pair() @@ -162,6 +178,40 @@ async def test_flow_step_creates_entry( assert struct_to_dict(step_result.data) == {"host": "1.2.3.4"} +async def test_create_entry_marshals_version_and_options( + channels: tuple[Channel, Channel], runner: FlowRunner +) -> None: + """CREATE_ENTRY carries the flow's VERSION/MINOR_VERSION/options on the wire.""" + main, sandbox = channels + runner.register(sandbox) + main.start() + sandbox.start() + + ha_config_entries.HANDLERS["phase4_versioned"] = _VersionedFlow + fake_module = ModuleType("homeassistant.components.phase4_versioned") + fake_flow_module = ModuleType("homeassistant.components.phase4_versioned.config_flow") + runner.hass.data[ha_loader.DATA_COMPONENTS]["phase4_versioned"] = fake_module + runner.hass.data[ha_loader.DATA_COMPONENTS]["phase4_versioned.config_flow"] = ( + fake_flow_module + ) + runner.hass.config.components.add("phase4_versioned") + try: + init_msg = pb.FlowInit(handler="phase4_versioned") + init_msg.context.update({"source": "user"}) + result = await main.call("sandbox/flow_init", init_msg) + finally: + ha_config_entries.HANDLERS.pop("phase4_versioned", None) + runner.hass.data[ha_loader.DATA_COMPONENTS].pop("phase4_versioned", None) + runner.hass.data[ha_loader.DATA_COMPONENTS].pop( + "phase4_versioned.config_flow", None + ) + + assert result.type == "create_entry" + assert result.version == 2 + assert result.minor_version == 3 + assert struct_to_dict(result.options) == {"poll_interval": 30} + + async def test_flow_step_validation_error_returns_form( channels: tuple[Channel, Channel], runner: FlowRunner ) -> None: diff --git a/tests/components/sandbox/test_proxy_flow.py b/tests/components/sandbox/test_proxy_flow.py index 6ae00e8a36d5..783e640f6e75 100644 --- a/tests/components/sandbox/test_proxy_flow.py +++ b/tests/components/sandbox/test_proxy_flow.py @@ -12,6 +12,7 @@ from homeassistant.components.sandbox._proto import sandbox_pb2 as pb from homeassistant.components.sandbox.channel import Channel from homeassistant.components.sandbox.manager import SandboxManager from homeassistant.components.sandbox.messages import struct_to_dict +from homeassistant.components.sandbox.proxy_flow import SandboxFlowProxy from homeassistant.components.sandbox.router import SandboxFlowRouter from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant @@ -166,6 +167,55 @@ async def test_full_flow_user_to_create_entry( assert entries[0].state is ConfigEntryState.LOADED +async def test_create_entry_carries_version_and_options( + hass: HomeAssistant, manager: FakeSandboxManager +) -> None: + """A sandbox CREATE_ENTRY with VERSION/MINOR_VERSION/options round-trips. + + Pins the framework behaviour the fix relies on: ``async_create_entry`` + reads ``self.VERSION``/``self.MINOR_VERSION`` off the proxy *instance*, + so overriding them before the call lands the integration's real schema + version on the entry (instead of the proxy class default ``VERSION=1``, + which would trigger a spurious migration on next setup). + """ + mock_integration(hass, MockModule("test_proxy_full")) + create_entry = pb.FlowResult( + type=FlowResultType.CREATE_ENTRY.value, + flow_id="sandbox-flow-ver", + handler="test_proxy_full", + title="Versioned", + version=2, + minor_version=3, + ) + create_entry.data.update({"host": "1.2.3.4"}) + create_entry.options.update({"poll_interval": 30}) + responses = [create_entry] + + with ( + _wired_sandbox(manager, group="built-in", responses=responses), + patch( + "homeassistant.components.sandbox.router.classify", + return_value=type("A", (), {"is_main": False, "group": "built-in"})(), + ), + ): + await _install_router(hass, manager) + # The proxy class default is VERSION=1 — the entry must NOT inherit it. + assert SandboxFlowProxy.VERSION == 1 + result = await hass.config_entries.flow.async_init( + "test_proxy_full", context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + entries = hass.config_entries.async_entries("test_proxy_full") + assert len(entries) == 1 + entry = entries[0] + assert entry.version == 2 + assert entry.minor_version == 3 + assert entry.options == {"poll_interval": 30} + assert entry.data == {"host": "1.2.3.4"} + + async def test_form_with_errors_reshows( hass: HomeAssistant, manager: FakeSandboxManager ) -> None: diff --git a/tests/testing_config/.storage/sandbox/built-in/demo_key b/tests/testing_config/.storage/sandbox/built-in/demo_key new file mode 100644 index 000000000000..61d0a7c58601 --- /dev/null +++ b/tests/testing_config/.storage/sandbox/built-in/demo_key @@ -0,0 +1,8 @@ +{ + "data": { + "counter": 42 + }, + "version": 2, + "minor_version": 3, + "key": "demo_key" +} \ No newline at end of file diff --git a/tests/testing_config/.storage/sandbox/built-in/persistent b/tests/testing_config/.storage/sandbox/built-in/persistent new file mode 100644 index 000000000000..d12ed650dc9e --- /dev/null +++ b/tests/testing_config/.storage/sandbox/built-in/persistent @@ -0,0 +1,8 @@ +{ + "data": { + "survives": true + }, + "version": 4, + "minor_version": 0, + "key": "persistent" +} \ No newline at end of file diff --git a/tests/testing_config/.storage/sandbox/built-in/shared_key b/tests/testing_config/.storage/sandbox/built-in/shared_key new file mode 100644 index 000000000000..d0e0b62b1bba --- /dev/null +++ b/tests/testing_config/.storage/sandbox/built-in/shared_key @@ -0,0 +1,8 @@ +{ + "data": { + "side": "built-in" + }, + "version": 1, + "minor_version": 1, + "key": "shared_key" +} \ No newline at end of file