Files
UHD_Media_Tool/packages/domain/test/identity.test.ts
T
rootandClaude Fable 5.1 304feea141 chore(deno): migrate toolchain from Node 24 + pnpm to Deno 2.9.7 + TypeScript 7.0.2 (W1.5)
- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock;
  remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports)
- tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions),
  test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types
- tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl
- server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting;
  drop @hono/node-server, tsx, esbuild build.mjs
- logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests
- relative imports get explicit extensions (sites/mp-sync paths left for follow-up)
- crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas
- single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml
- docs: README, runbook, ADR-001 superseded, plan §1 validation row

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:53:06 +08:00

159 lines
7.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
type IdentityCandidate,
normalizeTitle,
parseRelease,
queryTitles,
rankCandidates,
resolveIdentity,
scoreCandidate,
stripSeasonSuffix,
TRAD_TO_SIMP,
toSimplified,
} from "../src/index.ts";
const movie = (over: Partial<IdentityCandidate> = {}): IdentityCandidate => ({
provider: "tmdb",
entityType: "movie",
externalId: "872585",
titles: ["Oppenheimer", "奥本海默"],
year: 2023,
aliases: [],
...over,
});
describe("标题归一化", () => {
it("繁简 / 全半角 / 标点 / 大小写", () => {
expect(toSimplified("葬送的芙莉蓮")).toBe("葬送的芙莉莲");
expect(normalizeTitle("葬送的芙莉蓮")).toBe(normalizeTitle("葬送的芙莉莲"));
expect(normalizeTitle("THE LAST OF US")).toBe("thelastofus");
expect(normalizeTitle("Spider-Man: No Way Home")).toBe("spidermannowayhome");
expect(normalizeTitle("Tom & Jerry")).toBe("tomandjerry");
expect(normalizeTitle("黑暗榮耀")).toBe("黑暗荣耀");
});
it("去季号/年份后缀", () => {
expect(stripSeasonSuffix("庆余年 第二季")).toBe("庆余年");
expect(stripSeasonSuffix("The Boys Season 4")).toBe("The Boys");
expect(stripSeasonSuffix("Show S02")).toBe("Show");
expect(stripSeasonSuffix("Title (2019)")).toBe("Title");
expect(stripSeasonSuffix("第二季")).toBe("第二季");
expect(normalizeTitle("庆余年 第二季")).toBe(normalizeTitle("慶餘年"));
});
it("映射表是一一对应的字符对", () => {
expect(TRAD_TO_SIMP.size).toBeGreaterThan(500);
for (const [k, v] of TRAD_TO_SIMP) {
expect([...k].length).toBe(1);
expect([...v].length).toBe(1);
}
});
it("queryTitles 去重", () => {
const p = parseRelease("奥本海默 Oppenheimer 2023 1080p");
expect(queryTitles(p)).toEqual(["奥本海默", "oppenheimer"]);
});
});
describe("候选打分(证据等级,无概率)", () => {
const p = parseRelease("Oppenheimer.2023.2160p.UHD.BluRay.REMUX-FraMeSToR");
it("标题精确 + 年份一致 → rule_confirmed", () => {
const r = scoreCandidate(p, movie());
expect(r.level).toBe("rule_confirmed");
expect(r.titleMatch).toBe("exact");
expect(r.yearMatch).toBe("exact");
expect(Object.values(r).some((v) => typeof v === "number")).toBe(false);
});
it("年份容差 ±1 → multi_evidence;±2 → rejected", () => {
expect(scoreCandidate(p, movie({ year: 2024 })).level).toBe("multi_evidence");
expect(scoreCandidate(p, movie({ year: 2022 })).yearMatch).toBe("within_tolerance");
expect(scoreCandidate(p, movie({ year: 2025 })).level).toBe("rejected");
expect(scoreCandidate(p, movie({ year: 2025 }), { yearTolerance: 2 }).level).toBe("multi_evidence");
});
it("别名命中 → multi_evidence;部分包含 → needs_confirm;无关 → rejected", () => {
expect(scoreCandidate(p, movie({ titles: ["X"], aliases: ["Oppenheimer"] })).level).toBe("multi_evidence");
expect(scoreCandidate(p, movie({ titles: ["Oppenheimer Returns"] })).level).toBe("needs_confirm");
expect(scoreCandidate(p, movie({ titles: ["Barbie"] })).level).toBe("rejected");
});
it("年份未知:电影需确认,剧集有季号且类型一致算多证据", () => {
const noYear = parseRelease("Oppenheimer 1080p WEB-DL");
expect(scoreCandidate(noYear, movie()).level).toBe("needs_confirm");
const tv = parseRelease("奔跑吧 第十二季 20240426 第1期 1080p WEB-DL");
const show: IdentityCandidate = {
provider: "tmdb",
entityType: "tv",
externalId: "1",
titles: ["奔跑吧"],
year: 2017,
aliases: [],
};
const r = scoreCandidate(tv, show);
expect(r.yearMatch).toBe("unknown");
expect(r.typeMatch).toBe("match");
expect(r.level).toBe("multi_evidence");
expect(scoreCandidate(tv, { ...show, entityType: "movie" }).level).toBe("needs_confirm");
});
it("剧集季年份:seasonYears 命中算 season_year;后续季无季年份信息不据此拒绝", () => {
const s2 = parseRelease("The.Last.of.Us.S02.2025.1080p.WEB-DL");
const show: IdentityCandidate = {
provider: "tmdb",
entityType: "tv",
externalId: "100088",
titles: ["The Last of Us"],
year: 2023,
aliases: [],
};
expect(scoreCandidate(s2, show).yearMatch).toBe("unknown");
const withYears = scoreCandidate(s2, { ...show, seasonYears: [2023, 2025] });
expect(withYears.yearMatch).toBe("season_year");
expect(withYears.level).toBe("rule_confirmed");
expect(scoreCandidate(s2, { ...show, seasonYears: [2023] }).level).toBe("rejected");
});
it("显式 ID 提升一级;标题不符但显式 ID 命中 → needs_confirm", () => {
const e = parseRelease("[tmdbid=872585] Oppenheimer 2024 1080p");
const r = scoreCandidate(e, movie());
expect(r.evidence).toContain("explicit_id");
expect(r.level).toBe("rule_confirmed");
const e2 = parseRelease("[tmdbid=872585] Something Else 2023 1080p");
expect(scoreCandidate(e2, movie()).level).toBe("needs_confirm");
const local = movie({
provider: "local",
externalId: "uuid-1",
linkedExternalIds: [{ provider: "tmdb", id: "872585" }],
});
expect(scoreCandidate(e2, local).evidence).toContain("explicit_id");
});
});
describe("排序与判定", () => {
const p = parseRelease("Oppenheimer.2023.1080p.BluRay.x264-CHD");
it("本地实体同等级优先,等级 > 标题 > 年份", () => {
const tmdb = movie();
const local = movie({ provider: "local", externalId: "uuid-1", localEntityId: "uuid-1" });
const weak = movie({ externalId: "9", titles: ["Oppenheimer X"] });
const ranked = rankCandidates(p, [weak, tmdb, local]);
expect(ranked.map((r) => r.candidate.externalId)).toEqual(["uuid-1", "872585", "9"]);
});
it("唯一强候选 → auto", () => {
const res = resolveIdentity(p, [movie(), movie({ externalId: "2", titles: ["Barbie"] })]);
expect(res.status).toBe("auto");
expect(res.best?.candidate.externalId).toBe("872585");
});
it("同等级两个不同身份 → ambiguous;同一身份(本地映射)不算冲突", () => {
const dup = resolveIdentity(p, [movie(), movie({ externalId: "999" })]);
expect(dup.status).toBe("ambiguous");
expect(dup.reason).toBe("multiple_candidates_same_level");
const local = movie({
provider: "local",
externalId: "uuid-1",
localEntityId: "uuid-1",
linkedExternalIds: [{ provider: "tmdb", id: "872585" }],
});
expect(resolveIdentity(p, [movie(), local]).status).toBe("auto");
expect(resolveIdentity(p, [local, movie()]).status).toBe("auto");
expect(resolveIdentity(p, [local, { ...local, externalId: "x" }]).status).toBe("auto");
});
it("弱证据 → ambiguous;无候选 → no_match", () => {
expect(resolveIdentity(p, [movie({ titles: ["Oppenheimer Returns"] })]).status).toBe("ambiguous");
expect(resolveIdentity(p, []).status).toBe("no_match");
expect(resolveIdentity(p, [movie({ titles: ["Barbie"] })]).status).toBe("no_match");
});
});