fix: close data integrity and hot-reload review findings
Backfill same-day snapshots for newly added or restored videos, preserve successful fan snapshots on resume, persist incomplete state before forced re-runs, and split collection completion from backup completion so failed backups can retry without API calls. Reject missing data payloads instead of treating them as empty folders, guard against mass-unfav on suspicious empty remote folders, rebuild Bilibili clients when credentials change, recompute primary folder ownership from active relations with per-folder fav_time, preserve original titles for dropped invalid videos, poll config every 60 seconds, and fix favsort verify interval and mixed resource types.
This commit is contained in:
1 parent
178ab64b18
commit
6a21b5d905
21 files changed
+895
-82
No files matched your search
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""凭据变化时客户端重建测试(不联网): uv run python tests/test_provider.py"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
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.client import BilibiliClient, Pacer # noqa: E402
|
||||
from reaction_board.bilibili.provider import ClientProvider # noqa: E402
|
||||
from reaction_board.config import ConfigStore # noqa: E402
|
||||
|
||||
|
||||
def write_config(path: Path, *, cookie: str, mid: int) -> None:
|
||||
path.write_text(json.dumps({
|
||||
"bilibili": {
|
||||
"read": {"mid": mid, "csrf": "read-csrf", "cookie": cookie},
|
||||
"write": {"mid": 2, "csrf": "write-csrf", "cookie": "write-cookie"},
|
||||
},
|
||||
"folders": [],
|
||||
}, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
def bump(path: Path) -> None:
|
||||
target = max(path.stat().st_mtime_ns, time.time_ns()) + 1_000_000
|
||||
os.utime(path, ns=(target, target))
|
||||
|
||||
|
||||
def test_rebuild_only_when_credentials_change():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write_config(path, cookie="cookie-a", mid=1)
|
||||
store = ConfigStore(path, env={})
|
||||
created = []
|
||||
|
||||
def factory(credentials):
|
||||
created.append((credentials.mid, credentials.cookie))
|
||||
return BilibiliClient(
|
||||
cookie=credentials.cookie,
|
||||
csrf=credentials.csrf,
|
||||
mid=credentials.mid,
|
||||
pacer=Pacer(1.0, 1.6),
|
||||
)
|
||||
|
||||
provider = ClientProvider(store, "read", factory)
|
||||
first = provider.get()
|
||||
assert provider.get() is first
|
||||
assert created == [(1, "cookie-a")]
|
||||
|
||||
write_config(path, cookie="cookie-b", mid=1)
|
||||
bump(path)
|
||||
second = provider.get()
|
||||
assert second is not first
|
||||
assert created == [(1, "cookie-a"), (1, "cookie-b")]
|
||||
assert second.pacer.min_delay == 1.0
|
||||
assert second.pacer.max_delay == 1.6
|
||||
|
||||
# 仅 mtime 变化但凭据相同:不重建
|
||||
bump(path)
|
||||
assert provider.get() is second
|
||||
assert len(created) == 2
|
||||
|
||||
|
||||
def test_mid_change_rebuilds():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write_config(path, cookie="cookie-a", mid=1)
|
||||
store = ConfigStore(path, env={})
|
||||
provider = ClientProvider(
|
||||
store, "read",
|
||||
lambda credentials: BilibiliClient(mid=credentials.mid),
|
||||
)
|
||||
assert provider.get().mid == 1
|
||||
write_config(path, cookie="cookie-a", mid=9)
|
||||
bump(path)
|
||||
assert provider.get().mid == 9
|
||||
|
||||
|
||||
def test_config_revision_increments():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write_config(path, cookie="cookie-a", mid=1)
|
||||
store = ConfigStore(path, env={})
|
||||
assert store.revision == 0
|
||||
write_config(path, cookie="cookie-b", mid=1)
|
||||
bump(path)
|
||||
store.get()
|
||||
assert store.revision == 1
|
||||
store.get()
|
||||
assert store.revision == 1
|
||||
|
||||
|
||||
def main():
|
||||
test_rebuild_only_when_credentials_change()
|
||||
test_mid_change_rebuilds()
|
||||
test_config_revision_increments()
|
||||
print("OK: 凭据变化重建客户端与配置 revision 测试全部通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user