feat(up): capture follower snapshots at ingest and backfill history

This commit is contained in:
kairee committed 2026-11-01 11:18:06 +08:00
1 parent 4942d1d161
commit 519fc39fca
8 files changed
+401 -14

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""UP 粉丝快照采集测试(不联网): uv run python tests/test_up_profile.py"""
import os
import sys
import tempfile
from pathlib import Path
from types import SimpleNamespace
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
from reaction_board.bilibili.models import FavResource # noqa: E402
from reaction_board.config import FolderConfig # noqa: E402
from reaction_board.database import Database # noqa: E402
from reaction_board.repository import Repository # noqa: E402
from reaction_board.services.up_profile import UpProfileService # noqa: E402
FOLDER = FolderConfig(year=2026, media_id=3700023351, title="2026")
NOW = 1_800_000_000
def resource(aid, up_mid):
return FavResource(
aid=aid,
bvid=f"BV{aid}",
title=f"视频{aid}",
up_mid=up_mid,
up_name=f"UP{up_mid}",
pubdate=NOW - 86400,
duration=7200,
cover=None,
attr=0,
fav_time=None,
pages=2,
)
class FakeClient:
def __init__(self, *, failing=()):
self.calls = []
self.failing = set(failing)
def relation_stat(self, mid):
self.calls.append(int(mid))
if int(mid) in self.failing:
raise RuntimeError("simulated network error")
return SimpleNamespace(mid=int(mid), follower=1000 + int(mid), following=5)
def setup(tmp):
db = Database(Path(tmp) / "app.db")
db.initialize()
repo = Repository(db, clock=lambda: NOW)
repo.ensure_folder(FOLDER)
for aid, mid in ((1, 100), (2, 100), (3, 200)):
repo.upsert_from_fav(resource(aid, mid), FOLDER.media_id, was_added=True)
return db, repo
def test_capture_dedupes_and_continues_after_failure():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
client = FakeClient(failing={200})
service = UpProfileService(repo, lambda: client, clock=lambda: NOW)
result = service.capture(
[100, 100, 200, None, 0], live_since=NOW - 3600
)
assert result.requested == 2
assert client.calls == [100, 200]
assert result.updated_videos == 2
assert result.failed == 1
assert "200" in result.errors[0]
assert repo.get_video(1)["up_follower"] == 1100
assert repo.get_video(2)["up_follower"] == 1100
assert repo.get_video(3)["up_follower"] is None
assert repo.get_up_profile(100)["follower_first"] == 1100
assert repo.get_up_profile(200) is None
finally:
db.close()
def test_capture_marks_old_videos_as_backfilled():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
client = FakeClient()
service = UpProfileService(repo, lambda: client, clock=lambda: NOW)
service.capture([100], live_since=NOW + 1)
assert repo.get_video(1)["up_follower_backfilled"] == 1
assert repo.get_video(2)["up_follower_backfilled"] == 1
finally:
db.close()
def test_backfill_dry_run_and_limit():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
client = FakeClient()
service = UpProfileService(repo, lambda: client, clock=lambda: NOW)
dry = service.backfill(dry_run=True)
assert dry.requested == 2
assert client.calls == []
assert repo.up_follower_coverage()["missing"] == 3
limited = service.backfill(limit=1)
assert limited.requested == 1
assert client.calls == [100]
assert repo.get_video(1)["up_follower_backfilled"] == 1
assert repo.get_video(3)["up_follower"] is None
rest = service.backfill()
assert rest.requested == 1
assert client.calls == [100, 200]
assert repo.up_follower_coverage()["missing"] == 0
finally:
db.close()
def main():
test_capture_dedupes_and_continues_after_failure()
test_capture_marks_old_videos_as_backfilled()
test_backfill_dry_run_and_limit()
print("OK: UP 粉丝快照采集与回填测试全部通过")
if __name__ == "__main__":
main()