soulsync/tests/quality/test_selection.py
dev 310a5fe1bd feat(quality): quality-aware source fall-through in search_with_fallback
Add core/quality/selection.py: rank_with_targets() returns (ranked,
satisfied) where satisfied = a candidate meets a real target (strict).
load_profile_targets()/rank_for_profile() are the DB-backed wrappers.

search_with_fallback now skips a source that can deliver no target-meeting
quality and escalates to the next (source priority still wins among
satisfying sources; first source's results kept as fallback unless the
profile disables it). Returns RAW tracks — the satisfied check is a coarse
source gate; match-filtering + final ranking stay in the orchestrator so
the correct track is never pruned. Ranking is fail-open: a ranking error
never drops a source's real results.

Tested: rank_with_targets satisfied/fallback matrix + engine escalation,
stop-on-first, raw-not-pruned, fallback on/off. Amazon field test updated
for the corrected format token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:33:34 +02:00

64 lines
2.4 KiB
Python

"""core.quality.selection — quality-aware ranking + the satisfied flag that
drives the engine's source fall-through.
A source is "satisfied" when at least one of its candidates meets a real
target (strict, fallback off). The engine uses that to decide whether to
stop on the current source or escalate to the next.
"""
import pytest
from core.quality.model import AudioQuality, QualityTarget
from core.quality.selection import rank_with_targets
class _Cand:
"""Minimal candidate: filter_and_rank only needs ``.audio_quality``."""
def __init__(self, aq, name=""):
self.audio_quality = aq
self.name = name
def __repr__(self):
return f"_Cand({self.name})"
FLAC_HIRES = AudioQuality('flac', sample_rate=96000, bit_depth=24)
FLAC_CD = AudioQuality('flac', sample_rate=44100, bit_depth=16)
MP3_320 = AudioQuality('mp3', bitrate=320)
WANT_HIRES = [QualityTarget(label='FLAC 24', format='flac', bit_depth=24, min_sample_rate=96000)]
WANT_FLAC_ONLY = [QualityTarget(label='FLAC 16', format='flac', bit_depth=16)]
def test_satisfied_when_a_candidate_meets_a_target():
cands = [_Cand(MP3_320, 'mp3'), _Cand(FLAC_HIRES, 'hires')]
ranked, satisfied = rank_with_targets(cands, WANT_HIRES, fallback_enabled=True)
assert satisfied is True
assert ranked[0].name == 'hires' # the matching candidate wins
def test_unsatisfied_but_fallback_returns_sorted_when_enabled():
cands = [_Cand(MP3_320, 'mp3')]
ranked, satisfied = rank_with_targets(cands, WANT_FLAC_ONLY, fallback_enabled=True)
assert satisfied is False # no FLAC → no target met
assert [c.name for c in ranked] == ['mp3'] # but fallback keeps it
def test_unsatisfied_and_fallback_off_returns_empty():
cands = [_Cand(MP3_320, 'mp3')]
ranked, satisfied = rank_with_targets(cands, WANT_FLAC_ONLY, fallback_enabled=False)
assert satisfied is False
assert ranked == []
def test_empty_targets_accepts_everything_satisfied():
cands = [_Cand(MP3_320, 'mp3'), _Cand(FLAC_CD, 'cd')]
ranked, satisfied = rank_with_targets(cands, [], fallback_enabled=True)
assert satisfied is True # no constraint → first source wins
assert ranked[0].name == 'cd' # still quality-sorted
def test_no_candidates_is_unsatisfied():
ranked, satisfied = rank_with_targets([], WANT_FLAC_ONLY, fallback_enabled=True)
assert satisfied is False
assert ranked == []