Re-lands901b01d, which was reverted wholesale by28974d1(BR-2 committed from the shared index). The agent UI files901b01dhad rolled back are left at their AGUI-2 versions; contracts, routes, db types and the runbook part already reached main through later commits, so this carries the rest. HHClub (憨憨) was checked read-only against a real account (one detail request; list, user data and health came from the product's own fetch). Parsing fixes: - list titles carried the site's new-torrent marker "[新]" (58 of 100 rows; the detail page title has none); stripped with a replace filter - refreshing a detail overwrote seeders / leechers / snatched / category name / H&R with null because the HHClub div detail page lacks them; detail values now fall back to the list snapshot (mergeDetailOverList), and an unknown detail promotion keeps the list promotion - level "贵宾" is VIP (umt.vipLevels) - sanitized minimal fixture HHANCLUB_LIST_PAGE_2026 + tests; README marks HHClub as verified 2026-09-29 Size, counts, category ids/names, +08:00 time and subtitle were correct. Request restraint: - site definitions may carry umt.defaults; merged with settings.sites (longer intervals, fewer pages): new settings defaultSnapshotMaxPages 1, defaultHealthCheckIntervalSec 6 h, defaultDetailCacheSec 24 h; default burst 2. HHClub: snapshot 60 min page 1 only, health 6 h, search concurrency 1, detail cache 24 h, 1 token / 3 s burst 2 - periodic / manual snapshots are capped to that page count; scheduled health checks skip sites checked within the interval; detail refresh is served from cache inside the window; live searches on one site queue - every rate-limiter token is counted per site per hour (site_request_count, migration 0046); GET /sites/{id}/request-stats; site overview and resource list show requests in the last 24 hours Resource list: two-layer rows. Main line: title + spec tags (episodes, resolution, source, codec, HDR, group, 国语/中字, site tags) + promotion with time left + H&R. Second line: subtitle (one line, ellipsis) and fixed-width category / size / seed-leech-snatched / upload time (relative, absolute on hover) columns, stale badge, inventory overlay; folds into a card on phones. Also aligns identity_candidate.normalized_title order in db types with the migrations (schema drift test). Takes effect after the stack is rebuilt (migration 0046). Co-Authored-By: Claude Opus 5.5 <[email protected]>
@umt/testkit
测试夹具与 mock 服务,只供测试依赖,不进入生产构建:
| 入口 | 模拟对象 |
|---|---|
createMockQb |
qBittorrent WebUI v2(见下文"落盘") |
createMockNexusPhpSite、createMockMteamApi、createMockFetch |
观众(NexusPHP)站点页面、馒头 M-Team API(见 src/sites/README.md) |
createMockRssServer |
RSS / Atom Feed(见 src/rss/README.md) |
createMockTmdb |
TMDB v3(api_key 与 Bearer 两种鉴权,fault() 注入 429/5xx/延迟) |
createMockEmby |
Emby / Jellyfin(flavor),/Users、/Items、`/Shows/{id}/Seasons |
createMockCamofox |
camofox-browser(tabs / navigate / snapshot / screenshot,可选 accessKey;快照里故意带密码、Cookie、passkey 供脱敏测试) |
createTempTree |
临时目录树(可写库根哨兵) |
mock qB 落盘
createMockQb() 默认只改内存状态。e2e 需要"下载完成 → Copy 入库"时:
const qb = await createMockQb();
// files 的 path 相对任务 save_path(多文件种子含根目录);写随机内容,大小与清单一致,返回绝对路径
const paths = await qb.complete(hash, { files: [{ path: "Show.S01/Show.S01E01.mkv", size: 4 * 1024 * 1024 }] });
createMockQb({ materializeFiles: true })让所有complete()按现有文件清单落盘;单次总量上限maxMaterializeBytes(默认 64 MiB)。torrents/delete带deleteFiles=true时同步删除落盘文件和因此变空的目录(不越过 save_path)。- save_path 请用临时目录(
mkdtemp),不要用/downloads这类真实路径。