"""R14 backend coverage: the channel-detail Normal/Shorts/Live counts (moved off the manager table), and the interactive "get full history" immediate-backfill guard that keeps one burst from racing the scheduler's cursor or draining the shared backfill budget.""" from datetime import datetime, timezone from fastapi.testclient import TestClient import app.state as state from app.auth import current_user from app.db import get_db from app.main import app from app.models import Channel, User, Video from app.sync import runner def test_channel_detail_returns_normal_shorts_live_counts(db, monkeypatch): # published_at set so channel_detail skips the YouTube-enrich path (no quota / network in tests). ch = Channel( id="UC_nsl", title="N/S/L Channel", published_at=datetime(2020, 1, 1, tzinfo=timezone.utc), ) db.add(ch) me = User(email="nsl@example.com") db.add(me) db.flush() # 2 normal, 1 short, 1 live → stored 4, normal = 4 − 1 (short) − 1 (live) = 2. db.add_all( [ Video(id="v_norm1", channel_id=ch.id), Video(id="v_norm2", channel_id=ch.id), Video(id="v_short", channel_id=ch.id, is_short=True), Video(id="v_live", channel_id=ch.id, live_status="live"), ] ) db.commit() # The request gate 503s an unconfigured instance; flip the process-level flag (auto-restored). monkeypatch.setattr(state, "_configured_cache", True) app.dependency_overrides[get_db] = lambda: db app.dependency_overrides[current_user] = lambda: me try: resp = TestClient(app).get(f"/api/channels/{ch.id}") finally: app.dependency_overrides.clear() assert resp.status_code == 200, resp.text body = resp.json() assert body["stored_videos"] == 4 assert body["count_short"] == 1 assert body["count_live"] == 1 assert body["count_normal"] == 2 class TestImmediateBackfillGuard: """backfill_channel_now coordinates with the paced scheduler through a small in-flight set: a second call for a channel already in flight is a no-op, the set is capped so a burst can't drain the shared budget, and run_deep_backfill skips a channel an immediate backfill is handling.""" def setup_method(self): runner._immediate_backfill.clear() def teardown_method(self): runner._immediate_backfill.clear() @staticmethod def _boom_session(): # SessionLocal is only reached AFTER the guard admits the channel; if the guard short-circuits # (as it must here) this is never called, so raising proves the short-circuit. raise AssertionError("guard must short-circuit before opening a session") def test_a_second_call_for_an_in_flight_channel_is_a_noop(self, monkeypatch): runner._immediate_backfill.add("UC_busy") monkeypatch.setattr(runner, "SessionLocal", self._boom_session) runner.backfill_channel_now("UC_busy") # returns immediately; _boom_session never runs assert runner._immediate_backfill == {"UC_busy"} # the in-flight entry is untouched def test_over_the_concurrency_cap_falls_back_to_the_scheduler(self, monkeypatch): for i in range(runner._MAX_CONCURRENT_IMMEDIATE): runner._immediate_backfill.add(f"UC_full_{i}") monkeypatch.setattr(runner, "SessionLocal", self._boom_session) runner.backfill_channel_now("UC_new") # over the cap → no-op, scheduler will handle it assert "UC_new" not in runner._immediate_backfill def test_scheduler_deep_pass_skips_an_in_flight_channel(self, db, monkeypatch): # A channel the scheduler's query WOULD pick (deep-requested, recent-synced, not done)… ch = Channel( id="UC_inflight", title="In-flight", recent_synced_at=datetime(2026, 1, 1, tzinfo=timezone.utc), uploads_playlist_id="UU_inflight", backfill_done=False, ) db.add(ch) me = User(email="svc@example.com") db.add(me) db.flush() from app.models import Subscription db.add(Subscription(user_id=me.id, channel_id=ch.id, deep_requested=True)) db.commit() paged: list[str] = [] monkeypatch.setattr(runner, "get_service_user", lambda _db: me) monkeypatch.setattr(runner.quota, "remaining_today", lambda _db: 10_000) monkeypatch.setattr(runner, "YouTubeClient", lambda *a, **k: _NullYt()) monkeypatch.setattr( runner, "backfill_channel_deep", lambda _db, _yt, channel, _mp: paged.append(channel.id) or 0 ) monkeypatch.setattr(runner, "reconcile_full_history", lambda _db: 0) # …is skipped by run_deep_backfill while an immediate backfill owns it (no cursor race). runner._immediate_backfill.add(ch.id) runner.run_deep_backfill(db) assert paged == [], "the scheduler must not page a channel an immediate backfill is handling" # …and IS paged once the immediate backfill releases it. runner._immediate_backfill.discard(ch.id) runner.run_deep_backfill(db) assert paged == [ch.id] class _NullYt: def __enter__(self): return self def __exit__(self, *a): return False