soulsync/tests/downloads/test_download_orchestrator.py
Broque Thomas 3634dca83f B3: Orchestrator delegates query methods to engine
`get_all_downloads`, `get_download_status`, `cancel_download`, and
`clear_all_completed_downloads` on the orchestrator are now thin
pass-throughs to the engine. The plugin-iteration logic lives in
one place (the engine) instead of duplicated across orchestrator
methods.

Source-hint routing semantics preserved verbatim — engine.cancel
treats streaming-source names as direct routes and unknown names
as Soulseek peer usernames, exactly like the legacy orchestrator
did. Per-plugin exceptions still get swallowed defensively.

Test fixture `_build_orchestrator` now constructs an engine and
registers every mock plugin so the helper-built orchestrators
have the same wiring as production.

Suite still green (2012 passed). Zero behavior change for users.
2026-05-04 12:59:04 -07:00

102 lines
3.5 KiB
Python

from core.download_engine import DownloadEngine
from core.download_orchestrator import DownloadOrchestrator
from core.download_plugins.registry import DownloadPluginRegistry, PluginSpec
class _FakeClient:
def __init__(self, configured=True, clear_result=True):
self.configured = configured
self.clear_result = clear_result
self.clear_calls = 0
def is_configured(self):
return self.configured
async def clear_all_completed_downloads(self):
self.clear_calls += 1
return self.clear_result
def _build_orchestrator(**clients):
"""Build an orchestrator with mock clients via the registry.
The orchestrator iterates `self.registry.all_plugins()` to drive
every per-source operation, so the test must set up a real
registry with mock plugins (not just stuff attributes on the
orchestrator). Source slots not provided in `clients` are
skipped — registry only holds the ones the test cares about.
"""
registry = DownloadPluginRegistry()
name_to_display = {
'soulseek': 'Soulseek', 'youtube': 'YouTube', 'tidal': 'Tidal',
'qobuz': 'Qobuz', 'hifi': 'HiFi', 'deezer_dl': 'Deezer',
'lidarr': 'Lidarr', 'soundcloud': 'SoundCloud',
}
# 'deezer_dl' is the legacy attr name; canonical registry name is 'deezer'.
aliases_for = {'deezer_dl': ('deezer_dl',)}
canonical_for = {'deezer_dl': 'deezer'}
for slot, client in clients.items():
if client is None:
continue
canonical_name = canonical_for.get(slot, slot)
registry.register(PluginSpec(
name=canonical_name,
factory=lambda c=client: c,
display_name=name_to_display.get(slot, slot),
aliases=aliases_for.get(slot, ()),
))
registry.initialize()
orch = DownloadOrchestrator.__new__(DownloadOrchestrator)
orch.registry = registry
orch._init_failures = registry.init_failures
orch.soulseek = registry.get('soulseek')
orch.youtube = registry.get('youtube')
orch.tidal = registry.get('tidal')
orch.qobuz = registry.get('qobuz')
orch.hifi = registry.get('hifi')
orch.deezer_dl = registry.get('deezer')
orch.lidarr = registry.get('lidarr')
orch.soundcloud = registry.get('soundcloud')
# Engine — orchestrator delegates per-source query/cancel
# methods to it, so the test fixture must build one and
# register every mock plugin under its canonical name.
orch.engine = DownloadEngine()
for source_name, plugin in registry.all_plugins():
orch.engine.register_plugin(source_name, plugin)
return orch
def _run_async(coro):
import asyncio
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
def test_clear_all_completed_downloads_ignores_unconfigured_clients():
orch = _build_orchestrator(
soulseek=_FakeClient(configured=True, clear_result=True),
youtube=_FakeClient(configured=False, clear_result=False),
)
result = _run_async(orch.clear_all_completed_downloads())
assert result is True
assert orch.soulseek.clear_calls == 1
assert orch.youtube.clear_calls == 0
def test_clear_all_completed_downloads_propagates_configured_failures():
orch = _build_orchestrator(
soulseek=_FakeClient(configured=True, clear_result=False),
)
result = _run_async(orch.clear_all_completed_downloads())
assert result is False
assert orch.soulseek.clear_calls == 1