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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
This commit is contained in:
Adam SmithandClaude Opus 5 committed 2026-09-17 21:56:56 -04:00
1 parent 99003c0cca
commit 117780dd68
4 files changed
+46 -3

No files matched your search

@@ -49,6 +49,7 @@ from .const import (
CONF_TEMPERATURE, CONF_TEMPERATURE,
CONF_TOP_P, CONF_TOP_P,
DEFAULT_AI_TASK_NAME, DEFAULT_AI_TASK_NAME,
DEFAULT_BASE_URL,
DEFAULT_NAME, DEFAULT_NAME,
DEFAULT_STT_NAME, DEFAULT_STT_NAME,
DEFAULT_TTS_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) 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( LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version "Migration to version %s:%s successful", entry.version, entry.minor_version
) )
@@ -134,7 +134,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OpenAI Conversation.""" """Handle a config flow for OpenAI Conversation."""
VERSION = 2 VERSION = 2
MINOR_VERSION = 7 MINOR_VERSION = 8
@override @override
async def async_step_user( async def async_step_user(
+33 -1
View File
@@ -5,7 +5,11 @@ from unittest.mock import AsyncMock, patch
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry 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: 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() await hass.async_block_till_done()
assert mock_client.call_args.kwargs["api_key"] == "not-required" 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
+1 -1
View File
@@ -122,7 +122,7 @@ async def test_form(hass: HomeAssistant) -> None:
}, },
] ]
assert result2["version"] == 2 assert result2["version"] == 2
assert result2["minor_version"] == 7 assert result2["minor_version"] == 8
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1