import { createMockRssServer, type MockRssServer } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { completeDownload, createE2E, type E2E, pgAvailable, seedBase, waitFor } from "./harness.ts"; // E1 RSS → 入库(全部经真实任务引擎 + outbox 中继): // mock RSS(观众风格条目)→ ingestFeed → release.observed → resolveObservation(mock TMDB,密钥来自设置) // → release.resolved → evaluateAcquisition(已发布策略:新作品 1080p Slot,发布即生成持续授权)→ 计划 + 提交 mock qB // → 完成(落盘)→ download.status_changed(completed)→ verifyAndImport(Copy)→ /organization-records/changes 文件级记录 // → 媒体库详情 Slot=satisfied。 const hasPg = await pgAvailable(); const BASE = "https://audiences.example"; const TITLE = "Dune.Part.Two.2024.1080p.BluRay.x264-GRP"; describe.skipIf(!hasPg)("E1 RSS → 入库", () => { let e: E2E; let rss: MockRssServer; let base: Awaited>; beforeAll(async () => { e = await createE2E("e1"); base = await seedBase(e, { baseUrl: `${BASE}/` }); rss = await createMockRssServer({ passkey: "E2EPASSKEY", items: [ { guid: "aud-812345", title: TITLE, link: `${BASE}/details.php?id=812345&hit=1`, enclosureUrl: `${BASE}/download.php?id=812345&passkey=E2EPASSKEY`, size: 300_000, pubDate: new Date(), categories: ["电影"], }, { guid: "aud-812346", title: "Dune.Part.Two.2024.720p.WEB-DL.x264-AAA", link: `${BASE}/details.php?id=812346&hit=1`, enclosureUrl: `${BASE}/download.php?id=812346&passkey=E2EPASSKEY`, size: 300_000, pubDate: new Date(), categories: ["电影"], }, ], }); }, 120_000); afterAll(async () => { await rss?.close(); await e?.close(); }, 60_000); it("发布 1080p 收录策略 → 持续授权", async () => { const created = await e.api("POST", "/policy-revisions", { policyKey: "default", kind: "acquisition", title: "新片 1080p", scope: { mediaKinds: ["movie"] }, rules: { newTitle: { enabled: true, windowDays: 3650, mediaKinds: ["movie"], slots: [{ name: "1080p", hardConditions: { resolutions: ["1080p"] } }], }, }, }); expect(created.status).toBe(201); const pub = await e.api("POST", `/policy-revisions/${created.body.data.id}/publish`, { expectedRevision: created.body.data.revision, }); expect(pub.status).toBe(200); expect(pub.body.data.status).toBe("published"); const grants = await e.api("GET", "/automation-grants"); expect(grants.body.data.items.map((g: { action: string }) => g.action)).toContain("policy.acquisition"); }); it("RSS 抓取 → 识别 → 计划 → 提交 → 完成 → Copy 入库 → 整理记录增量可读 → Slot satisfied", async () => { const urlRef = await e.booted.ctx.secrets.create({ purpose: "feed_url", plaintext: rss.feedUrl }); const feed = await e.db .insertInto("feed_source") .values({ site_id: base.siteId, name: "观众-电影", purpose: "movie", url_secret_ref: urlRef, url_display: rss.feedUrl.replace(/passkey=[^&]+/, "passkey=[REDACTED]"), poll_interval_sec: 3600, }) .returning("id") .executeTakeFirstOrThrow(); const fetched = await e.api("POST", `/feed-sources/${feed.id}/fetch`); expect(fetched.status).toBe(200); // 识别:TMDB 客户端来自设置(secret_ref),建本地作品 const rel = await waitFor( async () => e.db .selectFrom("source_release") .selectAll() .where("title", "=", TITLE) .where("resolution_status", "=", "resolved") .executeTakeFirst(), 60_000, "1080p 资源识别完成", ); const entity = await e.db .selectFrom("media_entity") .selectAll() .where("id", "=", rel.media_entity_id!) .executeTakeFirstOrThrow(); expect(entity).toMatchObject({ kind: "movie", year: 2024 }); // 计划与提交:只有 1080p 被选中 await completeDownload(e, TITLE); expect(e.qb.callsTo("torrents/add")).toHaveLength(1); const rec = await waitFor( async () => e.db .selectFrom("organization_record") .selectAll() .where("media_entity_id", "=", entity.id) .where("status", "=", "succeeded") .executeTakeFirst(), 90_000, "整理记录 succeeded", ); expect(rec).toMatchObject({ action: "copy", trigger: "auto" }); // 公开整理记录增量 API:文件级结果(只返回早于所有活跃事务的变更,共享库上其它测试的长事务会让它稍晚可见) const item = await waitFor( async () => { const changes = await e.api("GET", "/organization-records/changes?after=0&includeFiles=true"); expect(changes.status).toBe(200); return changes.body.data.items.find( (x: { id: string; status: string }) => x.id === rec.id && x.status === "succeeded", ); }, 60_000, "整理记录出现在 changes 增量里", 500, ); expect(item.files[0]).toMatchObject({ role: "main", status: "succeeded" }); expect(item.files[0].target.path).toMatch(/1080p\.mkv$/); // 媒体库详情:Slot satisfied,资产登记 const lib = await e.api("GET", `/library/${entity.id}`); expect(lib.status).toBe(200); expect(lib.body.data.slots.map((s: { name: string; status: string }) => `${s.name}:${s.status}`)).toEqual([ "1080p:satisfied", ]); expect(lib.body.data.assets.length).toBeGreaterThanOrEqual(1); // 凭证不落业务表明文 const dump = JSON.stringify(await e.db.selectFrom("observation").selectAll().execute()); expect(dump).not.toContain("E2EPASSKEY"); }, 180_000); });