import { createMockQb, createTempTree, type MockQb, makeTorrent, type TempTree } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts"; import { submitDownload, syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts"; import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts"; import { applyEpisodeTotal } from "../../src/modules/acquisition-organize/services/subscriptions.ts"; import { createHarness, fakeDisk, type Harness, pgAvailable, seedBase, seedEntity, seedRelease } from "./harness.ts"; // 洗版完成口径(H2):收录版本有升级目标(规则组阶梯按 pri 洗版)时,覆盖齐还不够,要到最高档才完成订阅。 // 电影洗版、按集洗版、全集洗版各一例;下载器接收即计入(afterDownloadAccepted)与入库后(afterSlotVerified)同口径。 const hasPg = await pgAvailable(); const SIZE = 300_000; describe.skipIf(!hasPg)("洗版到最高档才完成(真实 PG + mock qB + 临时目录)", () => { let h: Harness; let qb: MockQb; let lib: TempTree; let cache: TempTree; const disk = fakeDisk(10_000_000_000); const torrents = new Map(); let base: Awaited>; let groupId: string; beforeAll(async () => { qb = await createMockQb({ addVisibleAfterPolls: 0 }); lib = await createTempTree({}, { sentinel: true, prefix: "umt-lib-" }); cache = await createTempTree({}, { prefix: "umt-cache-" }); disk.state.watch = [cache.root]; disk.setPct(10); h = await createHarness("upgradedone", { diskUsage: disk.fn, torrentSource: async ({ release }) => ({ kind: "file", bytes: torrents.get(release.id)! }), }); base = await seedBase(h.db, { libraryPath: lib.root, cachePath: cache.root, qbUrl: qb.url }); const g = await h.api("POST", "/rules", { name: "洗版到 REMUX", ruleString: "REMUX > WEBDL" }); expect(g.status, JSON.stringify(g.body)).toBe(201); groupId = g.body.data.id; }, 60_000); afterAll(async () => { await h?.close(); await qb?.close(); await lib?.cleanup(); await cache?.cleanup(); }); const db = () => h.db; async function washSub(kind: "movie" | "series", title: string, mode: "movie" | "per_unit" | "full_pack") { const entityId = await seedEntity(db(), { kind, title, year: 2024 }); const sub = await db() .insertInto("subscription") .values({ media_entity_id: entityId, media_kind: kind, mode: "once", season_scope: "current_season" }) .returning("id") .executeTakeFirstOrThrow(); await db() .insertInto("acquisition_slot") .values({ media_entity_id: entityId, subscription_id: sub.id, name: "洗版", upgrade_threshold: JSON.stringify({ enabled: true, mode }), rule_group_ids: JSON.stringify([groupId]), }) .execute(); return { entityId, subId: sub.id }; } async function release(entityId: string, title: string, files?: { path: string; length: number }[]) { const t = files ? makeTorrent({ name: title, files, pieceSeed: Math.floor(Math.random() * 1e6) }) : makeTorrent({ name: `${title}.mkv`, length: SIZE, pieceSeed: Math.floor(Math.random() * 1e6) }); const id = await seedRelease(db(), { siteId: base.siteId, entityId, title, size: SIZE, hash: t.hash }); torrents.set(id, t.bytes); return { id, hash: t.hash }; } const subRow = (id: string) => db().selectFrom("subscription").selectAll().where("id", "=", id).executeTakeFirstOrThrow(); const planRow = (id: string) => db().selectFrom("acquisition_plan").selectAll().where("id", "=", id).executeTakeFirstOrThrow(); async function planFor(relId: string) { const ev = await evaluateAcquisition(h.d, { sourceReleaseId: relId }); expect(ev.plans, JSON.stringify(ev)).toHaveLength(1); return ev.plans[0]!; } async function submit(planId: string) { const s = await submitDownload(h.d, planId); expect(s.status).toBe("submitted"); return s.downloadTaskId!; } async function completeAndImport(hash: string, dtId: string) { await qb.complete(hash, { writeFiles: true }); const sync = await syncDownloads(h.d); expect(sync.completed).toContain(dtId); const p = await planImport(h.d, { downloadTaskId: dtId, trigger: "auto", actorType: "system" }); expect(p.status, JSON.stringify(p)).toBe("running"); const ex = await executeRecord(h.d, p.recordId!); expect(ex.status).toBe("succeeded"); } it("电影洗版:WEB-DL(pri 99)接收、入库都不完成;REMUX(pri 100)接收即完成", async () => { const { entityId, subId } = await washSub("movie", "Wash Film", "movie"); const web = await release(entityId, "Wash.Film.2024.1080p.WEB-DL.x264-GRP"); const webTask = await submit(await planFor(web.id)); expect(await subRow(subId)).toMatchObject({ status: "active" }); await completeAndImport(web.hash, webTask); expect(await subRow(subId)).toMatchObject({ status: "active" }); const remux = await release(entityId, "Wash.Film.2024.1080p.BluRay.REMUX.AVC-GRP"); const up = await planFor(remux.id); expect(await planRow(up)).toMatchObject({ purpose: "upgrade" }); await submit(up); expect(await subRow(subId)).toMatchObject({ status: "completed", status_reason: "download_accepted" }); }, 60_000); it("按集洗版:全集已覆盖但不是最高档时不完成;逐集洗到 REMUX 后完成", async () => { const { entityId, subId } = await washSub("series", "Wash Episodes", "per_unit"); await db() .transaction() .execute((trx) => applyEpisodeTotal(trx, { entityId, season: 1, fresh: 1, now: new Date() })); const web = await release(entityId, "Wash.Episodes.S01E01.1080p.WEB-DL.x264-GRP"); const webTask = await submit(await planFor(web.id)); expect(await subRow(subId)).toMatchObject({ status: "active" }); await completeAndImport(web.hash, webTask); expect(await subRow(subId)).toMatchObject({ status: "active" }); const remux = await release(entityId, "Wash.Episodes.S01E01.1080p.BluRay.REMUX.AVC-GRP"); const up = await planFor(remux.id); expect(await planRow(up)).toMatchObject({ purpose: "upgrade", unit_keys: ["s1e1"] }); await submit(up); expect(await subRow(subId)).toMatchObject({ status: "completed", status_reason: "download_accepted" }); }, 60_000); it("全集洗版:WEB-DL 季包覆盖全季不完成;单集 REMUX 不接;REMUX 季包后完成", async () => { const { entityId, subId } = await washSub("series", "Wash Pack", "full_pack"); await db() .transaction() .execute((trx) => applyEpisodeTotal(trx, { entityId, season: 1, fresh: 2, now: new Date() })); const files = (tag: string) => [1, 2].map((e) => ({ path: `Wash.Pack.S01E0${e}.1080p.${tag}.mkv`, length: SIZE })); const web = await release(entityId, "Wash.Pack.S01.1080p.WEB-DL.x264-GRP", files("WEB-DL.x264-GRP")); const webPlan = await planFor(web.id); expect((await planRow(webPlan)).unit_keys).toEqual(["s1e1", "s1e2"]); const webTask = await submit(webPlan); expect(await subRow(subId)).toMatchObject({ status: "active" }); await completeAndImport(web.hash, webTask); expect(await subRow(subId)).toMatchObject({ status: "active" }); const single = await release(entityId, "Wash.Pack.S01E01.1080p.BluRay.REMUX.AVC-GRP"); expect((await evaluateAcquisition(h.d, { sourceReleaseId: single.id })).plans).toHaveLength(0); const remux = await release(entityId, "Wash.Pack.S01.1080p.BluRay.REMUX.AVC-GRP", files("BluRay.REMUX.AVC-GRP")); const up = await planFor(remux.id); expect(await planRow(up)).toMatchObject({ purpose: "upgrade" }); await submit(up); expect(await subRow(subId)).toMatchObject({ status: "completed", status_reason: "download_accepted" }); }, 60_000); });