soulsync/tests/downloads/test_orchestrator_search_mode.py
dev d484046523 feat(downloads): best-quality search mode + clearer fallback UI
Adds an opt-in search strategy toggle in the Quality Profile:

- priority (default): unchanged — first source in the hybrid chain that
  meets a quality target wins.
- best_quality: pool candidates from EVERY source per query and download
  them best→worst by actual audio quality; source order only breaks ties.

Implementation reuses existing plumbing so the retry system is untouched:
- engine.search_all_sources pools raw tracks across all configured,
  non-exhausted sources (no first-source short-circuit).
- candidates.order_candidates: new quality_first sort path — profile
  quality rank dominates, confidence/peer signals break ties. Priority
  path is byte-for-byte unchanged (regression-locked by tests).
- task_worker passes quality_first + targets through; skips the redundant
  hybrid-fallback block in best-quality mode (pool already covered it).
- Per-source retry budgets unchanged: a source that spends its budget is
  added to exhausted_download_sources and thus dropped from the whole
  pool. Independent of post_processing.retry_exhaustive.
- Query generator NOT touched.

Also clarifies the "Allow fallback" setting wording: it accepts OFF-LIST
quality as a last resort (not "walk down my list"), and notes that
lossy_copy.downsample_hires also bypasses the quality gate — the cause of
16-bit/MP3 files slipping through a 24-bit-only profile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:46:01 +02:00

66 lines
2.1 KiB
Python

"""orchestrator.search routes to the right engine method per search_mode:
- priority (default) → engine.search_with_fallback (first source wins)
- best_quality + hybrid → engine.search_all_sources (pool every source)
- single-source mode → the single client's search (toggle is a no-op)
"""
import asyncio
import core.download_orchestrator as orch_mod
from core.download_orchestrator import DownloadOrchestrator
def _run(coro):
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
class _SpyEngine:
def __init__(self):
self.calls = []
async def search_with_fallback(self, query, chain, timeout=None, progress_callback=None):
self.calls.append(('fallback', tuple(chain)))
return (['fb'], [])
async def search_all_sources(self, query, chain, timeout=None,
progress_callback=None, exclude_sources=None):
self.calls.append(('all', tuple(chain)))
return (['pool'], [])
def _hybrid_orch():
orch = DownloadOrchestrator.__new__(DownloadOrchestrator)
orch.mode = 'hybrid'
orch.hybrid_order = ['soulseek', 'hifi']
orch.hybrid_primary = 'soulseek'
orch.hybrid_secondary = 'hifi'
orch.engine = _SpyEngine()
# _resolve_source_chain normalizes names through the registry; stub it so the
# test doesn't need a full registry.
orch._resolve_source_chain = lambda: ['soulseek', 'hifi']
return orch
def test_priority_mode_uses_search_with_fallback(monkeypatch):
monkeypatch.setattr(orch_mod, 'load_search_mode', lambda: 'priority')
orch = _hybrid_orch()
tracks, _ = _run(orch.search('q'))
assert orch.engine.calls == [('fallback', ('soulseek', 'hifi'))]
assert tracks == ['fb']
def test_best_quality_mode_uses_search_all_sources(monkeypatch):
monkeypatch.setattr(orch_mod, 'load_search_mode', lambda: 'best_quality')
orch = _hybrid_orch()
tracks, _ = _run(orch.search('q'))
assert orch.engine.calls == [('all', ('soulseek', 'hifi'))]
assert tracks == ['pool']