feat(dot_alfred/v2): Phase 4 Step 4 — settings + anchor warning

Two small additions that close out Phase 4's loose ends.

Settings — tmdb_cache_ttl_days

    class Settings(BaseSettings):
        # --- DOT_ALFRED ---
        tmdb_cache_ttl_days: int = 14

Default 14 days, matching the dot_alfred_v2 master spec. Will drive
the Phase 5 TTL policy on TVShowLibraryIndexSidecar /
MovieLibraryIndexSidecar (decide when a TMDB-cached entry is stale
and triggers a refresh sync).

Anchor-mismatch warning

DotAlfredTVShowLibraryIndex._load_or_heal and DotAlfredMovieLibraryIndex
._load_or_heal now cross-check each indexed entry's metadata.path
against the on-disk folder layout right after a successful parse.
Drift (sidecar says folder X, X no longer exists under library_root)
is surfaced as a WARNING log — one per missing folder, with the
tmdb_id for cross-reference. No auto-heal on drift; the caller
decides (the heal path remains opt-in via index.heal()).

The warning fires only on the parsed-index path. The heal path
always synthesizes entries from real folder names, so it can never
drift — silent by construction.

Tests

* TestTVShowLibraryIndexAnchorWarning — 3 scenarios:
  warn-on-drift / no-warn-on-match / no-warn-on-heal.
* TestMovieLibraryIndexAnchorWarning — symmetric coverage.

Full suite: 1237 passed / 8 skipped / 4 xfailed.
This commit is contained in:
2026-05-25 21:14:18 +02:00
parent 86222d95d1
commit cc334a7951
3 changed files with 134 additions and 3 deletions
@@ -264,3 +264,81 @@ class TestMovieLibraryIndex:
# Placeholder until TMDB sync.
assert entry.release_year is None
assert any("healing" in r.message for r in caplog.records)
# ════════════════════════════════════════════════════════════════════════════
# Anchor-mismatch warnings (parsed-index path only — heal path is silent)
# ════════════════════════════════════════════════════════════════════════════
class TestTVShowLibraryIndexAnchorWarning:
def test_warns_when_indexed_folder_missing(
self, tv_library, foundation_release, foundation_tmdb_info, now_utc, caplog
):
# Seed the index with a path that exists ("Foundation"), then
# delete the folder so the next read detects the drift.
index = DotAlfredTVShowLibraryIndex(tv_library)
index.upsert(
foundation_tmdb_info,
foundation_release,
path="Foundation",
fetched_at=now_utc,
)
(tv_library / "Foundation").rmdir()
with caplog.at_level(logging.WARNING):
index.find_by_tmdb_id(TmdbId(84958))
assert any(
"anchor mismatch" in r.message and "Foundation" in r.message
for r in caplog.records
)
def test_no_warning_when_indexed_folder_present(
self, tv_library, foundation_release, foundation_tmdb_info, now_utc, caplog
):
index = DotAlfredTVShowLibraryIndex(tv_library)
index.upsert(
foundation_tmdb_info,
foundation_release,
path="Foundation",
fetched_at=now_utc,
)
with caplog.at_level(logging.WARNING):
index.find_by_tmdb_id(TmdbId(84958))
assert not any(
"anchor mismatch" in r.message for r in caplog.records
)
def test_no_warning_on_heal_path(
self, tv_library, foundation_release, caplog
):
# The heal path always synthesizes entries from real folder
# names, so it can never drift — no anchor warning should fire.
release_repo = DotAlfredSeriesReleaseRepository(tv_library)
release_repo.save(foundation_release, show_folder="Foundation")
index = DotAlfredTVShowLibraryIndex(tv_library, release_repo=release_repo)
with caplog.at_level(logging.WARNING):
index.find_by_tmdb_id(TmdbId(84958))
assert not any(
"anchor mismatch" in r.message for r in caplog.records
)
class TestMovieLibraryIndexAnchorWarning:
def test_warns_when_indexed_folder_missing(
self, movie_library, inception_release, now_utc, caplog
):
index = DotAlfredMovieLibraryIndex(movie_library)
index.upsert(
inception_release,
name="Inception",
release_year=2010,
path=inception_release.folder,
fetched_at=now_utc,
)
(movie_library / inception_release.folder).rmdir()
with caplog.at_level(logging.WARNING):
index.find_by_tmdb_id(TmdbId(27205))
assert any(
"anchor mismatch" in r.message and inception_release.folder in r.message
for r in caplog.records
)