"""Phase 9 observability contract tests. Verifies SSE / cache-stats wiring propagates AudioDB data end-to-end. """ from unittest.mock import AsyncMock, MagicMock, patch import msgspec import pytest from repositories.audiodb_models import ( AudioDBArtistImages, AudioDBArtistResponse, AudioDBAlbumImages, AudioDBAlbumResponse, ) from services.audiodb_image_service import AudioDBImageService TEST_MBID = "cc197bad-dc9c-440d-a5b5-d52ba2e14234" TEST_ALBUM_MBID = "1dc4c347-a1db-32aa-b14f-bc9cc507b843" SAMPLE_ARTIST_RESP = AudioDBArtistResponse( idArtist="111239", strArtist="Coldplay", strMusicBrainzID=TEST_MBID, strArtistThumb="https://example.com/thumb.jpg", strArtistFanart="https://example.com/fanart.jpg", ) SAMPLE_ALBUM_RESP = AudioDBAlbumResponse( idAlbum="2115888", strAlbum="Parachutes", strMusicBrainzID=TEST_ALBUM_MBID, strAlbumThumb="https://example.com/album_thumb.jpg", strAlbumBack="https://example.com/album_back.jpg", ) def _make_settings(enabled=True, name_search_fallback=False): s = MagicMock() s.audiodb_enabled = enabled s.audiodb_name_search_fallback = name_search_fallback s.cache_ttl_audiodb_found = 604800 s.cache_ttl_audiodb_not_found = 86400 s.cache_ttl_audiodb_library = 1209600 s.audiodb_prewarm_concurrency = 4 s.audiodb_prewarm_delay = 0.0 return s def _make_image_service(settings=None, disk_cache=None, repo=None): if settings is None: settings = _make_settings() prefs = MagicMock() prefs.get_advanced_settings.return_value = settings if disk_cache is None: disk_cache = AsyncMock() disk_cache.get_audiodb_artist = AsyncMock(return_value=None) disk_cache.get_audiodb_album = AsyncMock(return_value=None) disk_cache.set_audiodb_artist = AsyncMock() disk_cache.set_audiodb_album = AsyncMock() if repo is None: repo = AsyncMock() return AudioDBImageService( audiodb_repo=repo, disk_cache=disk_cache, preferences_service=prefs, ) class TestPrewarmLogContract: def _make_status_service(self): status = MagicMock() status.update_phase = AsyncMock() status.update_progress = AsyncMock() status.persist_progress = AsyncMock() status.is_cancelled.return_value = False return status def _make_precache_service(self, audiodb_svc=None, prefs=None): from services.library_precache_service import LibraryPrecacheService if audiodb_svc is None: audiodb_svc = AsyncMock() audiodb_svc.get_cached_artist_images = AsyncMock(return_value=None) audiodb_svc.get_cached_album_images = AsyncMock(return_value=None) audiodb_svc.fetch_and_cache_artist_images = AsyncMock(return_value=None) audiodb_svc.fetch_and_cache_album_images = AsyncMock(return_value=None) if prefs is None: settings = MagicMock() settings.audiodb_enabled = True settings.audiodb_name_search_fallback = False settings.audiodb_prewarm_concurrency = 4 settings.audiodb_prewarm_delay = 0.0 prefs = MagicMock() prefs.get_advanced_settings.return_value = settings return LibraryPrecacheService( lidarr_repo=AsyncMock(), cover_repo=AsyncMock(), preferences_service=prefs, sync_state_store=AsyncMock(), genre_index=AsyncMock(), library_db=AsyncMock(), audiodb_image_service=audiodb_svc, ) @pytest.mark.asyncio async def test_prewarm_calls_update_phase_audiodb(self): svc = AsyncMock() svc.get_cached_artist_images = AsyncMock(return_value=None) svc.get_cached_album_images = AsyncMock(return_value=None) svc.fetch_and_cache_artist_images = AsyncMock(return_value=None) precache = self._make_precache_service(audiodb_svc=svc) status = self._make_status_service() artists = [{"mbid": TEST_MBID, "name": "Coldplay"}] await precache._precache_audiodb_data(artists, [], status) phase_calls = [ c for c in status.update_phase.call_args_list if c.args[0] == "audiodb_prewarm" ] assert len(phase_calls) >= 1, "Expected update_phase('audiodb_prewarm', ...) call" class TestCacheStatsAudioDBWiring: @pytest.mark.asyncio async def test_get_stats_includes_audiodb_counts(self): from services.cache_service import CacheService disk_cache = MagicMock() disk_cache.get_stats.return_value = { "total_count": 100, "album_count": 60, "artist_count": 40, "audiodb_artist_count": 15, "audiodb_album_count": 25, } mem_cache = MagicMock() mem_cache.size.return_value = 10 mem_cache.estimate_memory_bytes.return_value = 2048 library_db = AsyncMock() library_db.get_stats = AsyncMock(return_value={ "artist_count": 5, "album_count": 8, "db_size_bytes": 4096, }) svc = CacheService( cache=mem_cache, library_db=library_db, disk_cache=disk_cache, ) svc._stats_cache_ttl = 0 with patch("services.cache_service.get_covers_cache_dir") as mock_get_dir: mock_dir = MagicMock() mock_dir.exists.return_value = False mock_get_dir.return_value = mock_dir stats = await svc.get_stats() assert stats.disk_audiodb_artist_count == 15 assert stats.disk_audiodb_album_count == 25