# -*- coding: utf-8 -*- """候选池、去重、通知记录与 fav_action 状态机测试(不联网): uv run python tests/test_candidates.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 FOLDER = FolderConfig(year=2026, media_id=3700023351, title="2026") 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(FOLDER) return db, repo def candidate(repo, aid=1, *, score=3, now=1_800_000_000): return repo.upsert_candidate( aid=aid, bvid=f"BV{aid}", title=f"咻咻满 reaction {aid}", up_mid=100, up_name="UP", pubdate=1_799_000_000, cover="https://example/cover.jpg", score=score, channel="search:test#p1", now=now, ) def test_seen_and_known_aid(): with tempfile.TemporaryDirectory() as tmp: db, repo = setup(tmp) try: assert repo.known_aid(1) is False repo.record_seen(1, source="search:test#p1") repo.record_seen(1, source="search:test#p2") # 幂等 assert repo.known_aid(1) is True repo.upsert_from_fav( FavResource( aid=2, bvid="BV2", title="视频", up_mid=100, up_name="UP", pubdate=1, duration=60, cover=None, attr=0, fav_time=None, ), FOLDER.media_id, was_added=True, ) assert repo.known_aid(2) is True finally: db.close() def test_candidate_upsert_preserves_decision_and_notified(): with tempfile.TemporaryDirectory() as tmp: db, repo = setup(tmp) try: first = candidate(repo, 1, score=2) assert first["decision"] is None and first["notified_at"] is None repo.mark_notified( 1, channel="telegram", batch_id="b1", ok=True, now=1_800_000_100 ) repo.mark_candidate_decision(1, "rejected", now=1_800_000_200) second = candidate(repo, 1, score=4, now=1_800_000_300) assert second["score"] == 4 assert second["decision"] == "rejected" assert second["notified_at"] == 1_800_000_100 assert second["first_seen"] == 1_800_000_000 assert len(repo.list_candidates()) == 1 assert len(repo.list_candidates("rejected")) == 1 assert repo.list_candidates("accepted") == [] finally: db.close() def test_notify_and_reject_for_up(): with tempfile.TemporaryDirectory() as tmp: db, repo = setup(tmp) try: candidate(repo, 1) candidate(repo, 2) repo.mark_notified( 1, channel="telegram", batch_id="b1", ok=False, err="boom", now=1_800_000_100, ) assert repo.get_candidate(1)["notified_at"] is None repo.mark_notified( 1, channel="telegram", batch_id="b2", ok=True, now=1_800_000_200 ) assert repo.get_candidate(1)["notified_at"] == 1_800_000_200 assert repo.reject_pending_for_up(100, now=1_800_000_300) == 2 assert repo.get_candidate(2)["decision"] == "rejected" finally: db.close() def test_delete_expired_candidates_only_pending(): with tempfile.TemporaryDirectory() as tmp: db, repo = setup(tmp) try: candidate(repo, 1, now=1_800_000_000) candidate(repo, 2, now=1_800_000_000) candidate(repo, 3, now=1_800_000_000) repo.mark_candidate_decision(3, "rejected") deleted = repo.delete_expired_candidates( ttl_days=7, now=1_800_000_000 + 8 * 86400 ) assert deleted == 2 assert repo.get_candidate(1) is None assert repo.get_candidate(3) is not None finally: db.close() def test_folder_for_up_uses_earliest_video(): with tempfile.TemporaryDirectory() as tmp: db, repo = setup(tmp) try: for aid, pubdate in ((1, 1_700_000_500), (2, 1_700_000_100)): repo.upsert_from_fav( FavResource( aid=aid, bvid=f"BV{aid}", title="视频", up_mid=100, up_name="UP", pubdate=pubdate, duration=60, cover=None, attr=0, fav_time=None, ), FOLDER.media_id, was_added=True, ) assert repo.folder_for_up(100) == FOLDER.media_id assert repo.folder_for_up(999) is None finally: db.close() def test_fav_action_state_machine(): with tempfile.TemporaryDirectory() as tmp: db, repo = setup(tmp) try: token, state = repo.begin_fav_action(1, FOLDER.media_id) assert token == "fav:1" assert state == "processing" token2, state2 = repo.begin_fav_action(1, FOLDER.media_id) assert token2 == token and state2 == "busy" repo.finish_fav_action(token, position=3) assert repo.get_fav_action(token)["state"] == "done" assert repo.get_fav_action(token)["position"] == 3 assert repo.begin_fav_action(1, FOLDER.media_id)[1] == "done" token3, state3 = repo.begin_fav_action(2, FOLDER.media_id) assert state3 == "processing" repo.fail_fav_action(token3, error="夹满") assert repo.get_fav_action(token3)["state"] == "failed" assert repo.get_fav_action(token3)["err"] == "夹满" # failed 可重试 _, retry = repo.begin_fav_action(2, FOLDER.media_id) assert retry == "processing" # 过期的 processing 自动失败 assert repo.expire_fav_actions(now=1_800_000_000 + 2 * 86400) >= 1 assert repo.get_fav_action("fav:2")["state"] == "failed" finally: db.close() def main(): test_seen_and_known_aid() test_candidate_upsert_preserves_decision_and_notified() test_notify_and_reject_for_up() test_delete_expired_candidates_only_pending() test_folder_for_up_uses_earliest_video() test_fav_action_state_machine() print("OK: 候选池、去重、通知记录与 fav_action 状态机测试全部通过") if __name__ == "__main__": main()