Files
re-xxm/tests/test_candidates.py
T
kairee 4a9c37f2f3 feat(telegram): candidate list/delete/clear commands
私聊新增 /candidate [页码](待决策列表)、/candi_del(标题按钮逐条删除,删后原地重建键盘、可翻页连续删)、/candi_clear(全部/待决策/取消)。重构 handle_callback 为前缀分派(cdel/cpage/cclr,保留 add/ign/blk/fav);repository 加 list_pending_candidates/delete_all_candidates;LibraryService 加 pending_candidates/delete_candidate/clear_candidates 并刷看板缓存。
2026-11-01 20:45:57 +08:00

217 lines
7.5 KiB
Python

# -*- 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 test_list_pending_and_delete_all():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
candidate(repo, 1, score=9)
candidate(repo, 2, score=4)
candidate(repo, 3, score=1)
repo.mark_candidate_decision(3, "rejected")
assert [c["aid"] for c in repo.list_pending_candidates()] == [1, 2]
assert repo.delete_all_candidates(pending_only=True) == 2
assert repo.get_candidate(3) is not None
assert repo.list_pending_candidates() == []
candidate(repo, 4)
assert repo.delete_all_candidates() == 2 # aid 3 + aid 4
assert repo.list_candidates() == []
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_list_pending_and_delete_all()
test_folder_for_up_uses_earliest_video()
test_fav_action_state_machine()
print("OK: 候选池、去重、通知记录与 fav_action 状态机测试全部通过")
if __name__ == "__main__":
main()