import { normalizeTitle } from "@umt/domain"; import { createMockTmdb, type MockTmdb, type TmdbFixture } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { siteAliasGuard } from "../../src/modules/sources-identity/service/alias-guard.ts"; import { resolveRelease } from "../../src/modules/sources-identity/service/resolve.ts"; import { createHarness, type Harness, pgAvailable, seedSite } from "./harness.ts"; // FIX-5 别名守卫:站点标题里另一部作品的英文名不能挂到按中文名命中的作品上。 const up = await pgAvailable(); const cnTv = (id: number, zh: string, en: string | null, date: string): TmdbFixture => ({ type: "tv", id, original_title: zh, original_language: "zh", origin_country: ["CN"], date, titles: en ? { "zh-CN": zh, "en-US": en } : { "zh-CN": zh }, popularity: 5, alternative_titles: [], external_ids: {}, }); describe("siteAliasGuard(纯函数)", () => { const base = { level: "rule_confirmed", yearMatch: "exact", parsedYear: 2026, entityYear: 2026, normalized: "spark of the long march", script: "latin" as const, externalNames: null, ownedByOther: false, }; it("证据等级非最高、年份不相容、与外部标题冲突、已属于别的作品都不登记", () => { expect(siteAliasGuard(base)).toBeNull(); expect(siteAliasGuard({ ...base, level: "multi_evidence" })).toBe("low_evidence"); expect(siteAliasGuard({ ...base, parsedYear: 2019 })).toBe("year_conflict"); expect(siteAliasGuard({ ...base, parsedYear: 2019, seasonYears: [2019] })).toBeNull(); expect( siteAliasGuard({ ...base, externalNames: { han: new Set(["东北有个周东北"]), latin: new Set(["zhou dongbei"]) }, }), ).toBe("conflicts_external_title"); // 外部标题只有中文时,英文名不按外部标题判冲突 expect(siteAliasGuard({ ...base, externalNames: { han: new Set(["长河"]), latin: new Set() } })).toBeNull(); expect(siteAliasGuard({ ...base, ownedByOther: true })).toBe("owned_by_other"); expect(siteAliasGuard({ ...base, normalized: "电视剧" })).toBe("generic"); }); }); describe.skipIf(!up)("别名守卫(resolveObservation)", () => { let h: Harness; let tmdb: MockTmdb; let siteId: string; let n = 0; beforeAll(async () => { tmdb = await createMockTmdb({ fixtures: [ cnTv(990001, "东北有个周东北", "Zhou Dongbei in the Northeast", "2026-01-10"), cnTv(990003, "长河", null, "2026-02-01"), ], }); h = await createHarness("aliasguard", { tmdbUrl: tmdb.url }); siteId = (await seedSite(h, { baseUrl: "https://guard.example/" })).site.id; await h.db .insertInto("settings") .values({ key: "identity", value: JSON.stringify({ eagerTmdbLookup: true }) } as never) .execute(); }); afterAll(async () => { await h?.close(); await tmdb?.close(); }); const release = (title: string) => h.db .insertInto("source_release") .values({ site_id: siteId, site_release_id: String(880000 + ++n), title }) .returningAll() .executeTakeFirstOrThrow(); const aliasesOf = async (entityId: string) => ( await h.db.selectFrom("title_alias").select(["alias", "source"]).where("media_entity_id", "=", entityId).execute() ).map((a) => a.alias); const lastAttempt = (releaseId: string) => h.db .selectFrom("resolution_attempt") .select(["best"]) .where("source_release_id", "=", releaseId) .orderBy("created_at", "desc") .executeTakeFirstOrThrow(); it("与作品已确认的英文名对不上的站点英文名不登记,记入 resolution_attempt", async () => { const r = await release("东北有个周东北.Spark.of.the.Long.March.2026.S01E38.1080p.WEB-DL.H264-GRP"); const res = await resolveRelease(h.rt, { sourceReleaseId: r.id }); expect(res.outcome).toBe("auto"); const aliases = await aliasesOf(res.mediaEntityId!); expect(aliases).toContain("东北有个周东北"); expect(aliases).not.toContain("Spark of the Long March"); const best = (await lastAttempt(r.id)).best as { aliases?: { skipped: { alias: string; reason: string }[] } }; expect(best.aliases?.skipped).toEqual([{ alias: "Spark of the Long March", reason: "conflicts_external_title" }]); }); it("已属于别的作品的站点名称不登记", async () => { const other = await h.db .insertInto("media_entity") .values({ kind: "series", title: "逆流", status: "active" } as never) .returning("id") .executeTakeFirstOrThrow(); await h.db .insertInto("title_alias") .values({ media_entity_id: other.id, alias: "Against the Current", normalized: normalizeTitle("Against the Current"), kind: "other", source: "user", } as never) .execute(); const r = await release("长河.Against.the.Current.2026.S01E01.1080p.WEB-DL.H264-GRP"); const res = await resolveRelease(h.rt, { sourceReleaseId: r.id }); expect(res.outcome).toBe("auto"); expect(res.mediaEntityId).not.toBe(other.id); expect(await aliasesOf(res.mediaEntityId!)).not.toContain("Against the Current"); const best = (await lastAttempt(r.id)).best as { aliases?: { skipped: { alias: string; reason: string }[] } }; expect(best.aliases?.skipped).toEqual([{ alias: "Against the Current", reason: "owned_by_other" }]); }); });