135 lines
5.4 KiB
TypeScript
135 lines
5.4 KiB
TypeScript
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" }]);
|
|
});
|
|
});
|