- server 的 src/modules/*/__tests__ 挪到 test/<模块>/;e2e 去掉 eN- 前缀, w5a2 / v2 / search2 / *-dh1 / *-md1 / *-md2、web 的 agui3 / search2、domain 的 id2 / id3 改为按行为命名 - vitest 分三组:unit、pg(真实 PG + 假引擎,文件并行,最多 6 个)、serial(真实引擎 / worker / e2e,串行); pg 组用例超时放宽到 60s(迁移类用例并行时排队等 schema 锁)。server 测试 615s → 约 350s - CLAUDE.md 补测试约定;待办补 harness 去重与 continuous-series 偶发超时(疑似 Slot 评估竞争) - ADR-004 补充:维持 Deno,记录实际只用到 --allow-run 白名单 Co-Authored-By: Claude Opus 5.5 <[email protected]>
159 lines
5.9 KiB
TypeScript
159 lines
5.9 KiB
TypeScript
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<ReturnType<typeof seedBase>>;
|
|
|
|
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);
|
|
});
|