Files
re-xxm/tests/test_repository.py
T
kairee c5b7185675 feat(growth): same-day 1-day baselines for new videos
Let 1-day view growth use a zero baseline for videos published today, and the first successful sample for older videos ingested today, without mixing those into 7/30-day rankings.
2026-11-02 20:44:17 +08:00

430 lines
17 KiB
Python

# -*- coding: utf-8 -*-
"""收藏夹/视频状态机测试(不联网): uv run python tests/test_repository.py"""
import os
import sys
import tempfile
from pathlib import Path
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
F1 = FolderConfig(year=2025, media_id=4451053851, title="2025")
F2 = FolderConfig(year=2026, media_id=3700023351, title="2026")
def resource(aid, *, attr=0, up_mid=100, title="视频", bvid=None, fav_time=None,
pages=2):
return FavResource(
aid=aid,
bvid=bvid or f"BV{aid}",
title=title,
up_mid=up_mid,
up_name="UP",
pubdate=1_700_000_000,
duration=600,
cover="http://example/cover.jpg",
attr=attr,
fav_time=fav_time,
pages=pages,
)
def setup(tmp):
db = Database(Path(tmp) / "app.db", backup_dir=Path(tmp) / "backup")
db.initialize()
repo = Repository(db, clock=lambda: 1_800_000_000)
repo.ensure_folder(F1)
repo.ensure_folder(F2)
return db, repo
def test_upsert_and_folder_resource():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
outcome = repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
assert outcome.status == "active"
assert outcome.previous_folder_id is None
video = repo.get_video(1)
assert video["bvid"] == "BV1"
assert video["folder_id"] == F1.media_id
assert video["source"] == f"fav:{F1.media_id}"
assert video["first_seen"] == 1_800_000_000
assert video["pages"] == 2
assert repo.folder_active_aids(F1.media_id) == {1}
# 幂等:重复导入不新增行、不改 first_seen
repo.upsert_from_fav(resource(1, title="新标题"), F1.media_id, was_added=False)
assert len(repo.list_videos()) == 1
video = repo.get_video(1)
assert video["title"] == "新标题"
assert video["first_seen"] == 1_800_000_000
finally:
db.close()
def test_invalid_attr_and_missing():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
out = repo.upsert_from_fav(resource(2, attr=9), F1.media_id, was_added=True)
assert out.status == "removed"
assert repo.get_video(2)["attr"] == 9
# 原本有效的视频后来失效:保留原历史标题
repo.upsert_from_fav(
resource(4, title="原始标题"), F1.media_id, was_added=True
)
repo.upsert_from_fav(
resource(4, title="已失效视频", attr=9), F1.media_id, was_added=False
)
video4 = repo.get_video(4)
assert video4["status"] == "removed"
assert video4["title"] == "原始标题"
status = repo.mark_missing_resource(
3, folder_id=F1.media_id, bvid="BV3"
)
assert status == "removed"
video = repo.get_video(3)
assert video["status"] == "removed"
assert video["bvid"] == "BV3"
assert video["title"] == "(资源已失效)"
assert video["attr"] == 9
assert 3 in repo.folder_active_aids(F1.media_id)
finally:
db.close()
def test_remove_from_folder_transitions():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
for aid in (1, 2):
repo.upsert_from_fav(resource(aid), F1.media_id, was_added=True)
# 2 同时出现在两个夹:从 F1 移除后仍 active
repo.upsert_from_fav(resource(2), F2.media_id, was_added=True)
status = repo.mark_resource_removed(F1.media_id, 2)
assert status == "active"
assert repo.get_video(2)["status"] == "active"
# 1 只在 F1:移除 -> unfaved,但数据保留
status = repo.mark_resource_removed(F1.media_id, 1)
assert status == "unfaved"
assert repo.get_video(1)["status"] == "unfaved"
assert repo.get_video(1)["bvid"] == "BV1"
assert 1 not in repo.folder_active_aids(F1.media_id)
# 再次出现在收藏夹 -> active
out = repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
assert out.status == "active"
finally:
db.close()
def test_dropped_only_revives_on_reappearing_in_folder():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
repo.set_video_status(1, "dropped")
# 全量刷新(未重新加入)保持 dropped
out = repo.upsert_from_fav(resource(1), F1.media_id, was_added=False)
assert out.status == "dropped"
# 从收藏夹移除也不改变 dropped
repo.mark_resource_removed(F1.media_id, 1)
assert repo.get_video(1)["status"] == "dropped"
# 重新出现在收藏夹 -> 自动恢复 active
out = repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
assert out.status == "active"
finally:
db.close()
def test_removed_stays_removed_when_folder_removed():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1, attr=9), F1.media_id, was_added=True)
assert repo.get_video(1)["status"] == "removed"
status = repo.mark_resource_removed(F1.media_id, 1)
assert status == "removed"
finally:
db.close()
def test_multi_folder_moves_primary_and_warns():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
out = repo.upsert_from_fav(resource(1), F2.media_id, was_added=True)
assert out.moved_folder is True
assert out.previous_folder_id == F1.media_id
assert repo.get_video(1)["folder_id"] == F2.media_id
# 两个夹的资源关系都保留
assert repo.folder_active_aids(F1.media_id) == {1}
assert repo.folder_active_aids(F2.media_id) == {1}
finally:
db.close()
def test_blacklist_seed_idempotent():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
assert repo.seed_blacklist([1, 2, 3], now=100) == 3
assert repo.seed_blacklist([2, 3, 4], now=200) == 1
rows = repo.blacklist()
assert [r["mid"] for r in rows] == [1, 2, 3, 4]
assert all(r["reason"] == "seed" for r in rows)
assert repo.is_blacklisted(2) and not repo.is_blacklisted(99)
repo.add_blacklist(9, name="切片号", reason="manual")
assert repo.is_blacklisted(9)
repo.add_blacklist(9, reason="updated")
row = next(r for r in repo.blacklist() if r["mid"] == 9)
assert row["name"] == "切片号"
assert row["reason"] == "updated"
finally:
db.close()
def test_folder_sync_metadata_and_counts():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
repo.finish_folder_sync(
F1, media_count=1, full_refresh=True, now=1_800_000_000
)
row = repo.get_folder(F1.media_id)
assert row.media_count == 1
assert row.last_ids_at == 1_800_000_000
assert row.last_full_at == 1_800_000_000
repo.finish_folder_sync(
F1, media_count=2, full_refresh=False, now=1_800_000_100
)
row = repo.get_folder(F1.media_id)
assert row.media_count == 2
assert row.last_ids_at == 1_800_000_100
assert row.last_full_at == 1_800_000_000
# 配置中移除的年度夹停用,重新加入时恢复
repo.disable_folders_except([F1.media_id])
assert repo.get_folder(F2.media_id).enabled is False
repo.ensure_folder(F2)
assert repo.get_folder(F2.media_id).enabled is True
counts = repo.counts()
assert counts["video_total"] == 1
assert counts["video_by_status"] == {"active": 1}
assert counts["folders"] == 2
finally:
db.close()
def test_primary_folder_is_order_independent():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1, fav_time=100), F1.media_id, was_added=True)
assert repo.get_video(1)["folder_id"] == F1.media_id
repo.upsert_from_fav(resource(1, fav_time=200), F2.media_id, was_added=True)
assert repo.get_video(1)["folder_id"] == F2.media_id
# 全量刷新按相反顺序遍历,主归属仍应指向 fav_time 更新的夹
repo.upsert_from_fav(resource(1, fav_time=100), F1.media_id, was_added=False)
assert repo.get_video(1)["folder_id"] == F2.media_id
repo.upsert_from_fav(resource(1, fav_time=200), F2.media_id, was_added=False)
assert repo.get_video(1)["folder_id"] == F2.media_id
# 从主夹移除、另一个夹仍持有时,主归属切换
status = repo.mark_resource_removed(F2.media_id, 1)
assert status == "active"
assert repo.get_video(1)["folder_id"] == F1.media_id
finally:
db.close()
def test_dropped_invalid_preserves_original_title():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(
resource(1, title="原始标题"), F1.media_id, was_added=True
)
repo.set_video_status(1, "dropped")
repo.upsert_from_fav(
resource(1, title="已失效视频", attr=9),
F1.media_id,
was_added=False,
)
video = repo.get_video(1)
assert video["status"] == "dropped"
assert video["title"] == "原始标题"
finally:
db.close()
def test_pending_snapshot_aids():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
repo.upsert_from_fav(resource(2), F1.media_id, was_added=True)
repo.upsert_from_fav(resource(3), F1.media_id, was_added=True)
repo.set_video_status(3, "unfaved")
repo.upsert_snapshot(
"2026-10-31", 1,
metrics={"view": 1, "like": 1, "favorite": 1, "coin": 1,
"share": 1, "reply": 1, "danmaku": 1},
ok=True, err=None, captured_at=1,
)
repo.upsert_snapshot(
"2026-10-31", 2,
metrics=None, ok=False, err="x", captured_at=1,
)
assert repo.pending_snapshot_aids("2026-10-31") == [2, 3]
finally:
db.close()
def test_upsert_snapshot_preserves_view_open():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
metrics = {
"view": 10, "like": 1, "favorite": 0, "coin": 0,
"share": 0, "reply": 0, "danmaku": 0,
}
repo.upsert_snapshot(
"2026-10-31", 1, metrics=metrics, ok=True, err=None, captured_at=10,
)
first = repo.get_snapshot("2026-10-31", 1)
assert first["view"] == 10
assert first["view_open"] == 10
assert first["open_captured_at"] == 10
metrics["view"] = 40
repo.upsert_snapshot(
"2026-10-31", 1, metrics=metrics, ok=True, err=None, captured_at=20,
)
second = repo.get_snapshot("2026-10-31", 1)
assert second["view"] == 40
assert second["view_open"] == 10
assert second["open_captured_at"] == 10
assert second["captured_at"] == 20
repo.upsert_snapshot(
"2026-10-31", 2, metrics=None, ok=False, err="x", captured_at=1,
)
failed = repo.get_snapshot("2026-10-31", 2)
assert failed["view_open"] is None
metrics["view"] = 7
repo.upsert_snapshot(
"2026-10-31", 2, metrics=metrics, ok=True, err=None, captured_at=5,
)
recovered = repo.get_snapshot("2026-10-31", 2)
assert recovered["view"] == 7
assert recovered["view_open"] == 7
assert recovered["open_captured_at"] == 5
copied = repo.copy_ok_snapshots(
"2026-10-31", "2026-11-01", captured_at=30, src="seed",
)
assert copied == 2
seeded = repo.get_snapshot("2026-11-01", 1)
assert seeded["view"] == 40
assert seeded["view_open"] == 40
assert seeded["open_captured_at"] == 30
assert seeded["src"] == "seed"
finally:
db.close()
def test_up_follower_snapshot_and_backfill():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1, up_mid=100), F1.media_id, was_added=True)
repo.upsert_from_fav(resource(2, up_mid=100), F1.media_id, was_added=True)
repo.upsert_from_fav(resource(3, up_mid=200), F1.media_id, was_added=True)
missing = {row["up_mid"] for row in repo.up_mids_missing_follower()}
assert missing == {100, 200}
now = 1_800_000_000
assert repo.apply_up_follower(
mid=100, name="UP", follower=1234, following=5, now=now
) == 2
first = repo.get_video(1)
assert first["up_follower"] == 1234
assert first["up_follower_at"] == now
assert first["up_follower_backfilled"] == 0
profile = repo.get_up_profile(100)
assert profile["follower_first"] == 1234
assert profile["follower_latest"] == 1234
assert {
row["up_mid"] for row in repo.up_mids_missing_follower()
} == {200}
# 同一 UP 后来新增的视频记录当下的粉丝快照,不覆盖旧视频
repo.upsert_from_fav(resource(4, up_mid=100), F1.media_id, was_added=True)
repo.apply_up_follower(
mid=100, name="UP", follower=2000, following=6, now=now + 60
)
assert repo.get_video(4)["up_follower"] == 2000
assert repo.get_video(1)["up_follower"] == 1234
profile = repo.get_up_profile(100)
assert profile["follower_first"] == 1234
assert profile["follower_latest"] == 2000
# 回填只补空值,并标记 backfilled
assert repo.apply_up_follower(
mid=200, name="UP2", follower=88, now=now + 120, backfilled=True
) == 1
assert repo.get_video(3)["up_follower"] == 88
assert repo.get_video(3)["up_follower_backfilled"] == 1
# live_since 之前的视频标为回填,之后的视为实时采集
repo.upsert_from_fav(resource(5, up_mid=300), F1.media_id, was_added=True)
repo.apply_up_follower(
mid=300, name="UP3", follower=50, now=now + 180,
live_since=now + 1,
)
assert repo.get_video(5)["up_follower_backfilled"] == 1
coverage = repo.up_follower_coverage()
assert coverage == {
"total": 5, "covered": 5, "missing": 0, "ups_covered": 3,
}
finally:
db.close()
def main():
test_upsert_and_folder_resource()
test_invalid_attr_and_missing()
test_remove_from_folder_transitions()
test_dropped_only_revives_on_reappearing_in_folder()
test_removed_stays_removed_when_folder_removed()
test_multi_folder_moves_primary_and_warns()
test_blacklist_seed_idempotent()
test_folder_sync_metadata_and_counts()
test_primary_folder_is_order_independent()
test_dropped_invalid_preserves_original_title()
test_pending_snapshot_aids()
test_upsert_snapshot_preserves_view_open()
test_up_follower_snapshot_and_backfill()
print("OK: 收藏夹/视频状态机与黑名单测试全部通过")
if __name__ == "__main__":
main()