"""The podcast poll loop itself: cadence, error resilience, and client lifecycle.""" from __future__ import annotations import asyncio from pathlib import Path import httpx2 import pytest import musicmouse.services.podcasts as podcasts_module from musicmouse.services.podcasts import PodcastFeedService class _FakeLibrary: def __init__(self, root: Path) -> None: self.root = root async def _run_briefly(service: PodcastFeedService) -> None: """Let `run()` complete a few fast-interval passes, then cancel it like shutdown does.""" task = asyncio.create_task(service.run()) await asyncio.sleep(0.05) task.cancel() with pytest.raises(asyncio.CancelledError): await task @pytest.mark.asyncio async def test_run_checks_immediately_and_calls_on_change_only_when_something_changed( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: calls = 0 async def fake_sync_all_shows( client: httpx2.AsyncClient, root: Path, *, keep: int | None = None ) -> bool: nonlocal calls calls += 1 return calls == 1 # only the very first pass finds something new monkeypatch.setattr(podcasts_module, "sync_all_shows", fake_sync_all_shows) changes = 0 async def on_change() -> None: nonlocal changes changes += 1 client = httpx2.AsyncClient(transport=httpx2.MockTransport(lambda _r: httpx2.Response(200))) service = PodcastFeedService( _FakeLibrary(tmp_path), client=client, on_change=on_change, interval=0.01 ) await _run_briefly(service) assert changes == 1 assert calls >= 2 assert client.is_closed @pytest.mark.asyncio async def test_run_survives_a_failing_pass( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: calls = 0 async def fake_sync_all_shows( client: httpx2.AsyncClient, root: Path, *, keep: int | None = None ) -> bool: nonlocal calls calls += 1 if calls == 1: raise RuntimeError("boom") return False monkeypatch.setattr(podcasts_module, "sync_all_shows", fake_sync_all_shows) async def on_change() -> None: pytest.fail("on_change must not run when nothing changed") client = httpx2.AsyncClient(transport=httpx2.MockTransport(lambda _r: httpx2.Response(200))) service = PodcastFeedService( _FakeLibrary(tmp_path), client=client, on_change=on_change, interval=0.01 ) await _run_briefly(service) # The RuntimeError from the first pass must not have ended the loop. assert calls >= 2 assert client.is_closed