# -*- 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): 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, ) 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 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 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() print("OK: 收藏夹/视频状态机与黑名单测试全部通过") if __name__ == "__main__": main()