"""Phase A pinning tests for SoundcloudClient — UPDATED for Phase C7. SoundCloud's quirk: 3-part filename `track_id||permalink_url||display_name` because yt-dlp consumes the URL, not the track_id, to actually download. The engine record holds both fields so the worker can call `_download_sync(download_id, permalink_url, display_name)` correctly. """ from __future__ import annotations import asyncio import threading from pathlib import Path from unittest.mock import patch import pytest from core.download_engine import DownloadEngine from core.soundcloud_client import SoundcloudClient def _run_async(coro): loop = asyncio.new_event_loop() try: return loop.run_until_complete(coro) finally: loop.close() @pytest.fixture def sc_client_with_engine(): client = SoundcloudClient.__new__(SoundcloudClient) client.download_path = Path('./test_sc_downloads') client.shutdown_check = None client._engine = None engine = DownloadEngine() client.set_engine(engine) return client, engine def test_download_returns_none_for_filename_with_too_few_parts(sc_client_with_engine): client, _ = sc_client_with_engine result = _run_async(client.download('soundcloud', 'just-id-no-url', 0)) assert result is None def test_download_returns_none_for_empty_track_id_or_url(sc_client_with_engine): client, _ = sc_client_with_engine assert _run_async(client.download('soundcloud', '||https://x.com/y', 0)) is None assert _run_async(client.download('soundcloud', 'track123||', 0)) is None def test_download_raises_when_engine_not_wired(): """Defensive: client without engine reference must raise so the orchestrator's download_with_fallback surfaces the error and moves on to the next source. Returning None silently would drop the download with no user feedback (per JohnBaumb).""" import pytest client = SoundcloudClient.__new__(SoundcloudClient) client._engine = None with pytest.raises(RuntimeError, match="engine reference"): _run_async(client.download('soundcloud', 'v||t', 0)) def test_download_accepts_three_part_filename_with_display(sc_client_with_engine): """Pinning: 3-part filename `track_id||permalink_url||display` is the canonical form. All three fields go into the engine record.""" client, engine = sc_client_with_engine started = threading.Event() release = threading.Event() def slow_impl(*args, **kwargs): started.set() release.wait(timeout=1.0) return '/tmp/done.mp3' with patch.object(client, '_download_sync', side_effect=slow_impl): download_id = _run_async(client.download( 'soundcloud', 'sc-12345||https://soundcloud.com/artist/song||Some Display Title', 0, )) started.wait(timeout=1.0) record = engine.get_record('soundcloud', download_id) assert record['track_id'] == 'sc-12345' assert record['permalink_url'] == 'https://soundcloud.com/artist/song' assert record['display_name'] == 'Some Display Title' release.set() def test_download_falls_back_display_name_to_track_id_when_two_part(sc_client_with_engine): """Pinning: 2-part filename → display name = track_id.""" client, engine = sc_client_with_engine started = threading.Event() release = threading.Event() def slow_impl(*args, **kwargs): started.set() release.wait(timeout=1.0) return '/tmp/x.mp3' with patch.object(client, '_download_sync', side_effect=slow_impl): download_id = _run_async(client.download( 'soundcloud', 'sc-99||https://soundcloud.com/x/y', 0, )) started.wait(timeout=1.0) assert engine.get_record('soundcloud', download_id)['display_name'] == 'sc-99' release.set() def test_download_populates_engine_record_with_initial_state(sc_client_with_engine): client, engine = sc_client_with_engine started = threading.Event() release = threading.Event() def slow_impl(*args, **kwargs): started.set() release.wait(timeout=1.0) return '/tmp/x.mp3' with patch.object(client, '_download_sync', side_effect=slow_impl): download_id = _run_async(client.download( 'soundcloud', 'sc-1||https://soundcloud.com/x||Title', 0, )) started.wait(timeout=1.0) record = engine.get_record('soundcloud', download_id) assert record['username'] == 'soundcloud' assert record['state'] in ('Initializing', 'InProgress, Downloading') assert 'permalink_url' in record release.set()