"""Tests for the inheritance resolver service.""" from __future__ import annotations import pytest from services.inheritance_resolver import ( INHERITABLE_FIELDS, InheritableField, ResolvedSetting, ) def test_registry_has_sixteen_fields(): # 1.15.0 added the four track variant policy fields (12 -> 16). assert len(INHERITABLE_FIELDS) == 16 def test_registry_field_names_unique(): names = [f.display_name for f in INHERITABLE_FIELDS] assert len(names) == len(set(names)) def test_registry_includes_known_fields(): names = {f.display_name for f in INHERITABLE_FIELDS} assert names == { "cleanup_foreign_tracks", "forced_preference", "hi_preference", "forced_scoring", "target_languages", "cutoff_language", "must_contain", "must_not_contain", "audio_exclude_languages", "preferred_audio_track_index", "priority_override", "min_attempts_per_day", "cleanup_track_variant_mode", "cleanup_keep_forced", "cleanup_keep_sdh", "cleanup_sidecar_policy", } def test_resolved_setting_typed_dict_shape(): r: ResolvedSetting = { "effective": True, "source": "series", "chain": [ {"scope": "global", "value": False, "label": "Global default"}, {"scope": "series", "value": True, "label": "Frieren"}, ], } assert r["source"] == "series" # --------------------------------------------------------------------------- # resolve_for_series tests (Task 5) # --------------------------------------------------------------------------- from datetime import UTC, datetime, timezone from unittest.mock import MagicMock from services.inheritance_resolver import resolve_for_series def _mk_global(**overrides): defaults = { "cleanup_foreign_tracks_default": False, "forced_preference": "include", "hi_preference": "include", "provider_priorities": "animetosho,jimaku", } defaults.update(overrides) mock = MagicMock() for k, v in defaults.items(): setattr(mock, k, v) return mock def _mk_series(**overrides): from db.models.core import SeriesSettings ss = SeriesSettings( sonarr_series_id=1, updated_at=datetime.now(UTC), ) for k, v in overrides.items(): setattr(ss, k, v) return ss def _mk_profile(**overrides): from db.models.core import LanguageProfile lp = LanguageProfile( id=10, name="Anime DE", source_language="en", source_language_name="English", target_languages_json='["de"]', target_language_names_json='["German"]', is_default=0, created_at=datetime.now(UTC), updated_at=datetime.now(UTC), ) for k, v in overrides.items(): setattr(lp, k, v) return lp def test_resolve_series_uses_series_value_when_set(): result = resolve_for_series( series=_mk_series(cleanup_foreign_tracks=True), profile=None, global_cfg=_mk_global(cleanup_foreign_tracks_default=False), ) r = result["cleanup_foreign_tracks"] assert r["effective"] is True assert r["source"] == "series" def test_resolve_series_falls_through_to_profile_when_series_null(): result = resolve_for_series( series=_mk_series(), # all NULL profile=_mk_profile(forced_preference="prefer"), global_cfg=_mk_global(forced_preference="include"), ) r = result["forced_preference"] assert r["effective"] == "prefer" assert r["source"] == "profile" def test_resolve_series_falls_through_to_global_when_profile_null(): result = resolve_for_series( series=_mk_series(), profile=_mk_profile(forced_preference=None), global_cfg=_mk_global(forced_preference="exclude"), ) r = result["forced_preference"] assert r["effective"] == "exclude" assert r["source"] == "global" def test_resolve_series_skips_profile_step_for_series_only_fields(): """min_attempts_per_day has no profile_attr — chain length 2 (Series, no global).""" result = resolve_for_series( series=_mk_series(min_attempts_per_day=3), profile=_mk_profile(), global_cfg=_mk_global(), ) r = result["min_attempts_per_day"] assert r["effective"] == 3 assert r["source"] == "series" # Profile step omitted from chain scopes = [step["scope"] for step in r["chain"]] assert "profile" not in scopes def test_resolve_series_with_no_profile_assigned(): """Series without a LanguageProfile gets a chain that skips profile entirely.""" result = resolve_for_series( series=_mk_series(), profile=None, global_cfg=_mk_global(forced_preference="include"), ) r = result["forced_preference"] assert r["effective"] == "include" assert r["source"] == "global" scopes = [step["scope"] for step in r["chain"]] assert "profile" not in scopes def test_resolve_series_json_array_decoded(): result = resolve_for_series( series=_mk_series(target_languages_override='["de","fr"]'), profile=_mk_profile(target_languages_json='["de"]'), global_cfg=_mk_global(), ) r = result["target_languages"] assert r["effective"] == ["de", "fr"] assert r["source"] == "series" def test_resolve_returns_all_sixteen_fields(): result = resolve_for_series(series=_mk_series(), profile=None, global_cfg=_mk_global()) assert len(result) == 16 # --------------------------------------------------------------------------- # resolve_for_movie tests (Task 6) # --------------------------------------------------------------------------- from services.inheritance_resolver import resolve_for_movie def _mk_movie(**overrides): from db.models.core import MovieSettings ms = MovieSettings(radarr_movie_id=99, updated_at=datetime.now(UTC)) for k, v in overrides.items(): setattr(ms, k, v) return ms def test_resolve_movie_uses_movie_value(): result = resolve_for_movie( movie=_mk_movie(cleanup_foreign_tracks=True), profile=None, global_cfg=_mk_global(cleanup_foreign_tracks_default=False), ) r = result["cleanup_foreign_tracks"] assert r["effective"] is True assert r["source"] == "movie" def test_resolve_movie_chain_uses_movie_label(): result = resolve_for_movie( movie=_mk_movie(forced_preference_override="exclude"), profile=_mk_profile(), global_cfg=_mk_global(), ) scopes = [step["scope"] for step in result["forced_preference"]["chain"]] assert "movie" in scopes assert "series" not in scopes