"""Integration tests for the library-root index repositories. Cover upsert / delete / find_by_* and the auto-heal behavior on missing / corrupt index files. Auto-heal must produce a valid sidecar with TMDB-cached fields left as documented placeholders (``status="unknown"``, ``seasons=()``). """ from __future__ import annotations import logging from alfred.domain.shared.value_objects import ImdbId, TmdbId from alfred.infrastructure.persistence.dot_alfred.v2.repository import ( DotAlfredMovieLibraryIndex, DotAlfredMovieReleaseRepository, DotAlfredSeriesReleaseRepository, DotAlfredTVShowLibraryIndex, ) # ════════════════════════════════════════════════════════════════════════════ # TV — upsert / find / delete # ════════════════════════════════════════════════════════════════════════════ class TestTVShowLibraryIndexUpsert: def test_upsert_creates_index_file( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) assert (tv_library / ".alfred.index").is_file() def test_upsert_then_find_by_tmdb_id_returns_entry( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) entry = index.find_by_tmdb_id(TmdbId(84958)) assert entry is not None assert entry.name == "Foundation" assert entry.status == "Returning Series" assert entry.metadata.path == "Foundation" assert entry.metadata.fetched_at == now_utc def test_upsert_flattens_multi_episode_file_across_slots( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) entry = index.find_by_tmdb_id(TmdbId(84958)) s02 = next(s for s in entry.seasons if s.number == 2) # E02 and E03 must point to the SAME multi-episode file. assert s02.episodes["E02"] == s02.episodes["E03"] assert "E02-E03" in s02.episodes["E02"] def test_upsert_twice_replaces_entry_does_not_duplicate( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) all_entries = index.find_all() assert len(all_entries) == 1 def test_find_by_imdb_id_returns_entry( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) entry = index.find_by_imdb_id(ImdbId("tt0804484")) assert entry is not None assert entry.tmdb_id == 84958 def test_find_by_path_returns_entry( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) entry = index.find_by_path("Foundation") assert entry is not None def test_delete_removes_entry( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) assert index.delete(TmdbId(84958)) is True assert index.find_by_tmdb_id(TmdbId(84958)) is None def test_delete_unknown_id_returns_false(self, tv_library): index = DotAlfredTVShowLibraryIndex(tv_library) assert index.delete(TmdbId(999)) is False # ════════════════════════════════════════════════════════════════════════════ # TV — auto-heal # ════════════════════════════════════════════════════════════════════════════ class TestTVShowLibraryIndexAutoHeal: def test_missing_index_is_silently_healed_from_per_show_sidecars( self, tv_library, foundation_release, caplog ): # Write a per-show sidecar but no index. release_repo = DotAlfredSeriesReleaseRepository(tv_library) release_repo.save(foundation_release, show_folder="Foundation") assert not (tv_library / ".alfred.index").exists() index = DotAlfredTVShowLibraryIndex(tv_library, release_repo=release_repo) with caplog.at_level(logging.INFO): entry = index.find_by_tmdb_id(TmdbId(84958)) assert entry is not None assert entry.tmdb_id == 84958 # Healed entries carry placeholders (no TMDB sync yet). assert entry.status == "unknown" assert entry.seasons == () assert (tv_library / ".alfred.index").is_file() assert any("healing" in r.message for r in caplog.records) def test_corrupt_index_is_healed( self, tv_library, foundation_release, caplog ): release_repo = DotAlfredSeriesReleaseRepository(tv_library) release_repo.save(foundation_release, show_folder="Foundation") # Plant a corrupt index. (tv_library / ".alfred.index").write_text("not: [valid yaml") index = DotAlfredTVShowLibraryIndex(tv_library, release_repo=release_repo) with caplog.at_level(logging.WARNING): entries = index.find_all() assert len(entries) == 1 assert any("corrupt" in r.message for r in caplog.records) def test_schema_version_mismatch_in_index_is_healed( self, tv_library, foundation_release ): release_repo = DotAlfredSeriesReleaseRepository(tv_library) release_repo.save(foundation_release, show_folder="Foundation") (tv_library / ".alfred.index").write_text( "schema_version: 999\nshows: []\n" ) index = DotAlfredTVShowLibraryIndex(tv_library, release_repo=release_repo) entries = index.find_all() # After heal, only Foundation (the only valid per-show sidecar) appears. assert len(entries) == 1 assert entries[0].tmdb_id == 84958 def test_heal_is_idempotent( self, tv_library, foundation_release ): release_repo = DotAlfredSeriesReleaseRepository(tv_library) release_repo.save(foundation_release, show_folder="Foundation") index = DotAlfredTVShowLibraryIndex(tv_library, release_repo=release_repo) first = index.heal() second = index.heal() # Compare model state minus the ``fetched_at`` (timestamps differ). assert len(first.shows) == len(second.shows) == 1 assert first.shows[0].tmdb_id == second.shows[0].tmdb_id def test_heal_with_empty_library_writes_empty_index(self, tv_library): index = DotAlfredTVShowLibraryIndex(tv_library) index.heal() assert (tv_library / ".alfred.index").is_file() assert index.find_all() == () # ════════════════════════════════════════════════════════════════════════════ # TV — atomicity # ════════════════════════════════════════════════════════════════════════════ class TestTVShowLibraryIndexAtomicity: def test_upsert_leaves_no_tmp_file( self, tv_library, foundation_release, foundation_tmdb_info, now_utc ): index = DotAlfredTVShowLibraryIndex(tv_library) index.upsert( foundation_tmdb_info, foundation_release, path="Foundation", fetched_at=now_utc, ) tmps = list(tv_library.glob("*.tmp")) assert tmps == [] # ════════════════════════════════════════════════════════════════════════════ # Movies # ════════════════════════════════════════════════════════════════════════════ class TestMovieLibraryIndex: def test_upsert_and_find( self, movie_library, inception_release, now_utc ): index = DotAlfredMovieLibraryIndex(movie_library) index.upsert( inception_release, name="Inception", release_year=2010, path=inception_release.folder, fetched_at=now_utc, ) entry = index.find_by_tmdb_id(TmdbId(27205)) assert entry is not None assert entry.name == "Inception" assert entry.release_year == 2010 def test_missing_index_heals_from_movie_sidecars( self, movie_library, inception_release, caplog ): release_repo = DotAlfredMovieReleaseRepository(movie_library) release_repo.save(inception_release) index = DotAlfredMovieLibraryIndex(movie_library, release_repo=release_repo) with caplog.at_level(logging.INFO): entry = index.find_by_tmdb_id(TmdbId(27205)) assert entry is not None assert entry.tmdb_id == 27205 # Placeholder until TMDB sync. assert entry.release_year is None assert any("healing" in r.message for r in caplog.records)