Files
UHD_Media_Tool/apps/server/test/lifecycle/name-recognize-hook.test.ts
T
rootandClaude Opus 5.5 079c3eafe9 feat: 业务节点发生命周期事件并调用链式钩子
- 订阅已建 / 已改 / 已完成 / 已取消,下载完成 / 失败 / 移除,整理结束(统一 organization.changed 终态),
  刮削完成 / 失败,作品进入关注,元数据已刷新(任务调用方),季集结构切换,站点不可达 / 恢复:
  都在调用方事务内写 outbox,带幂等键
- 钩子挂点:name.recognize(resolveRelease / identifyParsed)、organize.rename 与 organize.intercept(planImport)、
  resource.selection(订阅重评)、subscription.completion.check(completeSubscription)
- 测试:各节点事件 payload 与去重、整理改名 / 拦截 / 出错不影响整理、名称识别改写、资源选择、订阅完成拦截

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 19:27:14 +08:00

69 lines
2.8 KiB
TypeScript

import { parseRelease } from "@umt/domain";
import { createMockTmdb, type MockTmdb, TMDB_FIXTURES } from "@umt/testkit";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { HookRegistry } from "../../src/hooks/index.ts";
import { resolveRelease } from "../../src/modules/sources-identity/service/resolve.ts";
import { createHarness, type Harness, pgAvailable, seedSite } from "../sources-identity/harness.ts";
// 链式钩子 name.recognize(ADR-006,对照 MP ChainEventType.NameRecognize):资源标题解析之后、召回之前改写解析结果。
const up = await pgAvailable();
const GARBLED = "Zhuo.Wu.Ming.2023.2160p.UHD.BluRay.REMUX.HDR.HEVC-FGT";
describe.skipIf(!up)("name.recognize 钩子改写解析结果", () => {
let h: Harness;
let tmdb: MockTmdb;
let siteId: string;
let n = 0;
const release = async (title: string) =>
h.db
.insertInto("source_release")
.values({ site_id: siteId, site_release_id: String(910000 + ++n), title })
.returningAll()
.executeTakeFirstOrThrow();
beforeAll(async () => {
tmdb = await createMockTmdb({ fixtures: TMDB_FIXTURES });
h = await createHarness("name_hook", { tmdbUrl: tmdb.url });
siteId = (await seedSite(h, { baseUrl: "https://hook.example/" })).site.id;
await h.db
.insertInto("settings")
.values({ key: "identity", value: JSON.stringify({ eagerTmdbLookup: true }) } as never)
.execute();
});
afterEach(() => {
h.ctx.hooks = undefined;
});
afterAll(async () => {
await h?.close();
await tmdb?.close();
});
it("没有钩子:认不出的标题照常不自动识别", async () => {
const r = await release(GARBLED);
const res = await resolveRelease(h.rt, { sourceReleaseId: r.id });
expect(res.outcome).not.toBe("auto");
});
it("钩子按站点别名改写解析结果后自动识别;上下文带原标题与来源资源", async () => {
const seen: unknown[] = [];
const hooks = new HookRegistry();
hooks.forModule("test").rewrite("name.recognize", (p, c) => {
seen.push({ title: c.title, sourceReleaseId: c.sourceReleaseId, trigger: c.trigger });
return c.title.startsWith("Zhuo.Wu.Ming") ? parseRelease(c.title.replace("Zhuo.Wu.Ming", "Oppenheimer")) : p;
});
h.ctx.hooks = hooks;
const r = await release(GARBLED);
const res = await resolveRelease(h.rt, { sourceReleaseId: r.id });
expect(res.outcome).toBe("auto");
const e = await h.db
.selectFrom("external_identity")
.select("external_id")
.where("media_entity_id", "=", res.mediaEntityId!)
.where("provider", "=", "tmdb")
.executeTakeFirstOrThrow();
expect(e.external_id).toBe("872585");
expect(seen).toEqual([{ title: GARBLED, sourceReleaseId: r.id, trigger: "observed" }]);
});
});