From 117780dd685a2aa88e73784505c927eabd6bfc17 Mon Sep 17 00:00:00 2001 From: Adam Smith <2105363+adamjs83@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:56:56 -0400 Subject: [PATCH] fix: backfill base_url on migration so pre-base_url entries still load async_setup_entry does entry.data[CONF_BASE_URL] as a hard index, but base_url is this fork's own addition: an entry created by a build from before the base_url commits (48195c9 era, version 2 minor 7) has no such key. On restart, setup raises KeyError: 'base_url' and the integration fails to load -- reproduced in test first, where async_setup returns False with that KeyError in the log. Fixed with a migration step rather than a defensive .get(), matching the pattern the rest of async_migrate_entry already uses, so the invariant "a loaded entry has base_url" holds for every caller instead of each call site guarding itself: - new step 2.7 -> 2.8 backfills DEFAULT_BASE_URL via dict.setdefault, so an entry that already has a base_url keeps its own endpoint and only a missing one is filled. An install from before this feature was implicitly using api.openai.com, which is exactly DEFAULT_BASE_URL, so the backfill preserves existing behavior rather than repointing anyone. - config_flow MINOR_VERSION 7 -> 8, otherwise the step never runs. test_client_built_with_base_url now also covers the preserve path, since its entry starts at minor 7 with a base_url set and must still reach the client unchanged after migrating. Also updated test_form's `minor_version == 7` assertion to 8; it pins the version new entries are created at, so the bump is expected. The reauth test's MockConfigEntry stays at minor 7 on purpose -- it stands in for an already-existing entry. Local: 138 passed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki --- .../openai_compatible/__init__.py | 11 ++++++ .../openai_compatible/config_flow.py | 2 +- tests/test_base_url.py | 34 ++++++++++++++++++- tests/test_config_flow.py | 2 +- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/custom_components/openai_compatible/__init__.py b/custom_components/openai_compatible/__init__.py index 459338a..f7de9bc 100644 --- a/custom_components/openai_compatible/__init__.py +++ b/custom_components/openai_compatible/__init__.py @@ -49,6 +49,7 @@ from .const import ( CONF_TEMPERATURE, CONF_TOP_P, DEFAULT_AI_TASK_NAME, + DEFAULT_BASE_URL, DEFAULT_NAME, DEFAULT_STT_NAME, DEFAULT_TTS_NAME, @@ -489,6 +490,16 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> ) hass.config_entries.async_update_entry(entry, minor_version=7) + if entry.version == 2 and entry.minor_version == 7: + # base_url is this fork's addition, so an entry written by a build + # from before it existed has no such key, and async_setup_entry's + # lookup raises KeyError. Such an install was implicitly talking to + # OpenAI, which is what DEFAULT_BASE_URL is, so backfilling it keeps + # the endpoint it was already using. + data = dict(entry.data) + data.setdefault(CONF_BASE_URL, DEFAULT_BASE_URL) + hass.config_entries.async_update_entry(entry, data=data, minor_version=8) + LOGGER.debug( "Migration to version %s:%s successful", entry.version, entry.minor_version ) diff --git a/custom_components/openai_compatible/config_flow.py b/custom_components/openai_compatible/config_flow.py index 06575ab..4f0df15 100644 --- a/custom_components/openai_compatible/config_flow.py +++ b/custom_components/openai_compatible/config_flow.py @@ -134,7 +134,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for OpenAI Conversation.""" VERSION = 2 - MINOR_VERSION = 7 + MINOR_VERSION = 8 @override async def async_step_user( diff --git a/tests/test_base_url.py b/tests/test_base_url.py index 2da5d6a..6824f89 100644 --- a/tests/test_base_url.py +++ b/tests/test_base_url.py @@ -5,7 +5,11 @@ from unittest.mock import AsyncMock, patch from homeassistant.core import HomeAssistant from pytest_homeassistant_custom_component.common import MockConfigEntry -from custom_components.openai_compatible.const import CONF_BASE_URL, DOMAIN +from custom_components.openai_compatible.const import ( + CONF_BASE_URL, + DEFAULT_BASE_URL, + DOMAIN, +) async def test_client_built_with_base_url(hass: HomeAssistant) -> None: @@ -48,3 +52,31 @@ async def test_api_key_optional(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert mock_client.call_args.kwargs["api_key"] == "not-required" + + +async def test_migration_backfills_missing_base_url(hass: HomeAssistant) -> None: + """An entry stored before base_url existed must still set up. + + Entries written by an earlier build of this fork have no base_url key at + all, so anything that indexes entry.data[CONF_BASE_URL] raises KeyError + and the integration fails to load on restart. + """ + entry = MockConfigEntry( + domain=DOMAIN, + data={"api_key": "sk-test"}, + version=2, + minor_version=7, + ) + entry.add_to_hass(hass) + + with patch( + "custom_components.openai_compatible.openai.AsyncOpenAI" + ) as mock_client: + mock_client.return_value.platform_headers = lambda: {} + mock_client.return_value.with_options.return_value.models.list = AsyncMock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.data[CONF_BASE_URL] == DEFAULT_BASE_URL + assert entry.minor_version == 8 + assert mock_client.call_args.kwargs["base_url"] == DEFAULT_BASE_URL diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 9585815..37cc3ee 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -122,7 +122,7 @@ async def test_form(hass: HomeAssistant) -> None: }, ] assert result2["version"] == 2 - assert result2["minor_version"] == 7 + assert result2["minor_version"] == 8 assert len(mock_setup_entry.mock_calls) == 1