fix(channels): address R14 code-review findings
- list_channels no longer computes the Normal/Shorts/Live aggregation the frontend stopped reading when R14 dropped the N/S/L table column; the dead ManagedChannel.count_* fields are gone too. - Immediate "get full history" backfill now coordinates with the paced scheduler via a small in-flight set: capped at 2 concurrent + per-channel dedup, and run_deep_backfill skips a channel an immediate backfill owns — killing the cursor race and bounding the quota-drain burst, without losing the speed win. - Add test_channel_backfill.py: the channelDetail N/S/L counts + the guard's no-op / cap / scheduler-skip behaviour. - Inline the syncedChannels useMemo (its dep is a fresh array each render, so it never cached); fix the stale "~2-min ticks" comment (it's 10 minutes).
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user