soulsync/tests/test_canonical_resolver.py
BoulderBadgeDad 57e039e34d Canonical: make source selection a job setting (default active-preferred)
Feedback from the live dry-run: the job was pinning whichever source best fit
the files regardless of which source it was, which was surprising — users
expect it to respect their active metadata source. Made it a per-job setting
instead of a baked-in policy.

source_selection (default 'active_preferred'):
- active_preferred — use the active/primary metadata source's release when the
  album has an ID for it AND it clears the score floor; otherwise fall back to
  the best-fit among the other sources. Respects the configured source but
  self-heals when that link is clearly broken (below floor / no ID).
- active_only — only ever the active source; never considers others.
- best_fit — previous behavior: whichever source matches the files best.

resolve_canonical_for_album gains mode + primary_source; the orchestration
threads the primary source through; the job reads source_selection from its
settings. Note: active_preferred respects the active source as long as it clears
the floor, so it will NOT override a deluxe-vs-standard mismatch on the primary
(#767-Bug2) — that's what best_fit is for; the choice is now the user's.

Tests: per-mode coverage in test_canonical_resolver.py (active_preferred uses
primary when it fits, falls back when primary is below floor, keeps primary even
when another fits better; active_only pins primary / never falls back; best_fit
unchanged), orchestration default-mode test, and the setting default. 39
canonical tests pass.
2026-06-02 12:58:59 -07:00

177 lines
6.3 KiB
Python

"""Tests for resolve_canonical_for_album (#765 Stage 2 — injectable core)."""
from __future__ import annotations
from core.metadata.canonical_resolver import resolve_canonical_for_album
STD = [{"duration_ms": 180_000 + i * 10_000, "title": f"Song {i+1}"} for i in range(11)]
DLX = STD + [{"duration_ms": 300_000 + i * 10_000, "title": f"Bonus {i+1}"} for i in range(6)]
PRIORITY = ["spotify", "itunes", "deezer", "musicbrainz"]
def _fetcher(table):
"""fetch_tracklist backed by a {(source, album_id): tracks} table."""
def fetch(source, album_id):
return table.get((source, album_id))
return fetch
def test_best_fit_mode_picks_best_regardless_of_priority():
files = list(STD) # user owns the 11-track standard
table = {
("spotify", "sp_deluxe"): DLX, # spotify (primary) linked to deluxe (17)
("musicbrainz", "mb_std"): STD, # musicbrainz has standard (11)
}
out = resolve_canonical_for_album(
album_source_ids={"spotify": "sp_deluxe", "musicbrainz": "mb_std"},
file_tracks=files,
fetch_tracklist=_fetcher(table),
source_priority=PRIORITY,
mode="best_fit",
)
# best_fit: standard matches the files, deluxe doesn't — fit beats priority.
assert out["source"] == "musicbrainz" and out["album_id"] == "mb_std"
assert out["score"] > 0.9
# ── source-selection modes ────────────────────────────────────────────────
def test_active_preferred_uses_primary_when_it_fits():
files = list(STD)
table = {("spotify", "sp1"): STD, ("musicbrainz", "mb1"): STD} # both fit
out = resolve_canonical_for_album(
album_source_ids={"spotify": "sp1", "musicbrainz": "mb1"},
file_tracks=files, fetch_tracklist=_fetcher(table),
source_priority=PRIORITY, # primary = spotify
) # default mode = active_preferred
assert out["source"] == "spotify"
def test_active_preferred_falls_back_when_primary_clearly_misfits():
files = list(STD) # 11 tracks
table = {
("spotify", "sp_bad"): [{"duration_ms": 60_000, "title": "X"}] * 3, # 3-track, <floor
("musicbrainz", "mb_std"): STD,
}
out = resolve_canonical_for_album(
album_source_ids={"spotify": "sp_bad", "musicbrainz": "mb_std"},
file_tracks=files, fetch_tracklist=_fetcher(table),
source_priority=PRIORITY, mode="active_preferred",
)
# primary spotify scores below floor -> fall back to the fitting source.
assert out["source"] == "musicbrainz"
def test_active_preferred_keeps_primary_even_if_another_fits_better():
files = list(STD)
# primary spotify is a deluxe (decent fit, above floor); musicbrainz is exact.
table = {("spotify", "sp_dlx"): DLX, ("musicbrainz", "mb_std"): STD}
out = resolve_canonical_for_album(
album_source_ids={"spotify": "sp_dlx", "musicbrainz": "mb_std"},
file_tracks=files, fetch_tracklist=_fetcher(table),
source_priority=PRIORITY, mode="active_preferred",
)
# active_preferred respects the active source as long as it clears the floor,
# even though musicbrainz would fit better (use best_fit for that).
assert out["source"] == "spotify"
def test_active_only_pins_primary_and_never_falls_back():
files = list(STD)
# primary spotify is below floor; a perfect musicbrainz exists but is ignored.
table = {
("spotify", "sp_bad"): [{"duration_ms": 60_000, "title": "X"}] * 3,
("musicbrainz", "mb_std"): STD,
}
out = resolve_canonical_for_album(
album_source_ids={"spotify": "sp_bad", "musicbrainz": "mb_std"},
file_tracks=files, fetch_tracklist=_fetcher(table),
source_priority=PRIORITY, mode="active_only",
)
assert out is None # primary didn't fit, and active_only won't consider others
def test_active_only_pins_primary_when_it_fits():
files = list(STD)
table = {("spotify", "sp1"): STD, ("musicbrainz", "mb1"): STD}
out = resolve_canonical_for_album(
album_source_ids={"spotify": "sp1", "musicbrainz": "mb1"},
file_tracks=files, fetch_tracklist=_fetcher(table),
source_priority=PRIORITY, mode="active_only",
)
assert out["source"] == "spotify"
def test_priority_breaks_tie_between_equal_fits():
files = list(STD)
table = {("spotify", "a"): STD, ("itunes", "b"): STD} # identical fit
out = resolve_canonical_for_album(
album_source_ids={"itunes": "b", "spotify": "a"},
file_tracks=files,
fetch_tracklist=_fetcher(table),
source_priority=PRIORITY, # spotify before itunes
)
assert out["source"] == "spotify"
def test_skips_sources_without_ids_or_failed_fetch():
files = list(STD)
def fetch(source, album_id):
if source == "spotify":
raise RuntimeError("API down")
if source == "deezer":
return STD
return None
out = resolve_canonical_for_album(
album_source_ids={"spotify": "x", "deezer": "y"}, # no itunes id
file_tracks=files,
fetch_tracklist=fetch,
source_priority=PRIORITY,
)
assert out["source"] == "deezer"
def test_none_when_no_candidates():
out = resolve_canonical_for_album(
album_source_ids={},
file_tracks=list(STD),
fetch_tracklist=_fetcher({}),
source_priority=PRIORITY,
)
assert out is None
def test_none_when_no_files():
out = resolve_canonical_for_album(
album_source_ids={"spotify": "a"},
file_tracks=[],
fetch_tracklist=_fetcher({("spotify", "a"): STD}),
source_priority=PRIORITY,
)
assert out is None
def test_none_when_below_floor():
files = list(STD) # 11 tracks
# Only candidate is a wildly-wrong 3-track release.
table = {("spotify", "a"): [{"duration_ms": 60_000, "title": "X"}] * 3}
out = resolve_canonical_for_album(
album_source_ids={"spotify": "a"},
file_tracks=files,
fetch_tracklist=_fetcher(table),
source_priority=PRIORITY,
)
assert out is None
def test_score_is_rounded():
out = resolve_canonical_for_album(
album_source_ids={"spotify": "a"},
file_tracks=list(STD),
fetch_tracklist=_fetcher({("spotify", "a"): STD}),
source_priority=PRIORITY,
)
assert out["score"] == round(out["score"], 4)