"""Tests for the Ollama integration.""" from unittest.mock import patch from httpx import ConnectError import pytest from homeassistant.components import ollama from homeassistant.components.ollama.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import TEST_OPTIONS, TEST_USER_DATA from tests.common import MockConfigEntry @pytest.mark.parametrize( ("side_effect", "error"), [ (ConnectError(message="Connect error"), "Connect error"), (RuntimeError("Runtime error"), "Runtime error"), ], ) async def test_init_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, side_effect, error, ) -> None: """Test initialization errors.""" with patch( "ollama.AsyncClient.list", side_effect=side_effect, ): assert await async_setup_component(hass, ollama.DOMAIN, {}) await hass.async_block_till_done() assert error in caplog.text async def test_migration_from_v1_to_v2( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test migration from version 1 to version 2.""" # Create a v1 config entry with conversation options and an entity mock_config_entry = MockConfigEntry( domain=DOMAIN, data=TEST_USER_DATA, options=TEST_OPTIONS, version=1, title="llama-3.2-8b", ) mock_config_entry.add_to_hass(hass) device = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={(DOMAIN, mock_config_entry.entry_id)}, name=mock_config_entry.title, manufacturer="Ollama", model="Ollama", entry_type=dr.DeviceEntryType.SERVICE, ) entity = entity_registry.async_get_or_create( "conversation", DOMAIN, mock_config_entry.entry_id, config_entry=mock_config_entry, device_id=device.id, suggested_object_id="llama_3_2_8b", ) # Run migration with patch( "homeassistant.components.ollama.async_setup_entry", return_value=True, ): await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.version == 2 assert mock_config_entry.data == TEST_USER_DATA assert mock_config_entry.options == {} assert len(mock_config_entry.subentries) == 1 subentry = next(iter(mock_config_entry.subentries.values())) assert subentry.unique_id is None assert subentry.title == "llama-3.2-8b" assert subentry.subentry_type == "conversation" assert subentry.data == TEST_OPTIONS migrated_entity = entity_registry.async_get(entity.entity_id) assert migrated_entity is not None assert migrated_entity.config_entry_id == mock_config_entry.entry_id assert migrated_entity.config_subentry_id == subentry.subentry_id assert migrated_entity.unique_id == subentry.subentry_id # Check device migration assert not device_registry.async_get_device( identifiers={(DOMAIN, mock_config_entry.entry_id)} ) assert ( migrated_device := device_registry.async_get_device( identifiers={(DOMAIN, subentry.subentry_id)} ) ) assert migrated_device.identifiers == {(DOMAIN, subentry.subentry_id)} assert migrated_device.id == device.id async def test_migration_from_v1_to_v2_with_multiple_urls( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test migration from version 1 to version 2 with different URLs.""" # Create two v1 config entries with different URLs mock_config_entry = MockConfigEntry( domain=DOMAIN, data={"url": "http://localhost:11434", "model": "llama3.2:latest"}, options=TEST_OPTIONS, version=1, title="Ollama 1", ) mock_config_entry.add_to_hass(hass) mock_config_entry_2 = MockConfigEntry( domain=DOMAIN, data={"url": "http://localhost:11435", "model": "llama3.2:latest"}, options=TEST_OPTIONS, version=1, title="Ollama 2", ) mock_config_entry_2.add_to_hass(hass) device = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={(DOMAIN, mock_config_entry.entry_id)}, name=mock_config_entry.title, manufacturer="Ollama", model="Ollama 1", entry_type=dr.DeviceEntryType.SERVICE, ) entity_registry.async_get_or_create( "conversation", DOMAIN, mock_config_entry.entry_id, config_entry=mock_config_entry, device_id=device.id, suggested_object_id="ollama_1", ) device_2 = device_registry.async_get_or_create( config_entry_id=mock_config_entry_2.entry_id, identifiers={(DOMAIN, mock_config_entry_2.entry_id)}, name=mock_config_entry_2.title, manufacturer="Ollama", model="Ollama 2", entry_type=dr.DeviceEntryType.SERVICE, ) entity_registry.async_get_or_create( "conversation", DOMAIN, mock_config_entry_2.entry_id, config_entry=mock_config_entry_2, device_id=device_2.id, suggested_object_id="ollama_2", ) # Run migration with patch( "homeassistant.components.ollama.async_setup_entry", return_value=True, ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 2 for idx, entry in enumerate(entries): assert entry.version == 2 assert not entry.options assert len(entry.subentries) == 1 subentry = list(entry.subentries.values())[0] assert subentry.subentry_type == "conversation" assert subentry.data == TEST_OPTIONS assert subentry.title == f"Ollama {idx + 1}" dev = device_registry.async_get_device( identifiers={(DOMAIN, list(entry.subentries.values())[0].subentry_id)} ) assert dev is not None async def test_migration_from_v1_to_v2_with_same_urls( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test migration from version 1 to version 2 with same URLs consolidates entries.""" # Create two v1 config entries with the same URL mock_config_entry = MockConfigEntry( domain=DOMAIN, data={"url": "http://localhost:11434", "model": "llama3.2:latest"}, options=TEST_OPTIONS, version=1, title="Ollama", ) mock_config_entry.add_to_hass(hass) mock_config_entry_2 = MockConfigEntry( domain=DOMAIN, data={"url": "http://localhost:11434", "model": "llama3.2:latest"}, # Same URL options=TEST_OPTIONS, version=1, title="Ollama 2", ) mock_config_entry_2.add_to_hass(hass) device = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={(DOMAIN, mock_config_entry.entry_id)}, name=mock_config_entry.title, manufacturer="Ollama", model="Ollama", entry_type=dr.DeviceEntryType.SERVICE, ) entity_registry.async_get_or_create( "conversation", DOMAIN, mock_config_entry.entry_id, config_entry=mock_config_entry, device_id=device.id, suggested_object_id="ollama", ) device_2 = device_registry.async_get_or_create( config_entry_id=mock_config_entry_2.entry_id, identifiers={(DOMAIN, mock_config_entry_2.entry_id)}, name=mock_config_entry_2.title, manufacturer="Ollama", model="Ollama", entry_type=dr.DeviceEntryType.SERVICE, ) entity_registry.async_get_or_create( "conversation", DOMAIN, mock_config_entry_2.entry_id, config_entry=mock_config_entry_2, device_id=device_2.id, suggested_object_id="ollama_2", ) # Run migration with patch( "homeassistant.components.ollama.async_setup_entry", return_value=True, ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() # Should have only one entry left (consolidated) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 entry = entries[0] assert entry.version == 2 assert not entry.options assert len(entry.subentries) == 2 # Two subentries from the two original entries # Check both subentries exist with correct data subentries = list(entry.subentries.values()) titles = [sub.title for sub in subentries] assert "Ollama" in titles assert "Ollama 2" in titles for subentry in subentries: assert subentry.subentry_type == "conversation" assert subentry.data == TEST_OPTIONS # Check devices were migrated correctly dev = device_registry.async_get_device( identifiers={(DOMAIN, subentry.subentry_id)} ) assert dev is not None