backend/tests/test_inheritance_resolver.py
Rama master · réplica de lectura del repositorio original.
"""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