import { describe, expect, it } from "vitest"; import { parseMatchOutcome, rankCandidates } from "./match.js"; describe("parseMatchOutcome", () => { it("maps matches and drops bad episodeId", () => { const r = parseMatchOutcome({ isMatched: true, matches: [{ episodeId: 12, animeTitle: "A", episodeTitle: "01" }, { episodeId: "x" }], }); expect(r.isMatched).toBe(true); expect(r.candidates).toHaveLength(1); expect(r.candidates[0]?.episodeId).toBe(12); }); }); describe("rankCandidates", () => { it("single candidate is high confidence", () => { const r = rankCandidates([ { episodeId: 1, animeId: 1, animeTitle: "A", episodeTitle: "01", imageUrl: null }, ]); expect(r.highConfidence).toBe(true); expect(r.candidates).toHaveLength(1); }); it("close scores are not high confidence", () => { const list = [ { episodeId: 1, animeId: 1, animeTitle: "Show", episodeTitle: "01", imageUrl: null }, { episodeId: 2, animeId: 1, animeTitle: "Show", episodeTitle: "01v2", imageUrl: null }, ]; const r = rankCandidates(list); expect(r.highConfidence).toBe(false); }); it("empty candidates are not high confidence", () => { const r = rankCandidates([]); expect(r.highConfidence).toBe(false); expect(r.candidates).toEqual([]); }); it("score gap >= default lead (0.15) is high confidence", () => { const list = [ { episodeId: 2, animeId: 1, animeTitle: "Show", episodeTitle: null, imageUrl: "img" }, { episodeId: 1, animeId: 1, animeTitle: "Show", episodeTitle: "01", imageUrl: null }, ]; const r = rankCandidates(list); expect(r.highConfidence).toBe(true); expect(r.candidates[0]?.episodeId).toBe(1); }); it("custom lead is honored", () => { const list = [ { episodeId: 2, animeId: 1, animeTitle: "Show", episodeTitle: null, imageUrl: "img" }, { episodeId: 1, animeId: 1, animeTitle: "Show", episodeTitle: "01", imageUrl: null }, ]; expect(rankCandidates(list, { lead: 0.5 }).highConfidence).toBe(false); expect(rankCandidates(list, { lead: 0.3 }).highConfidence).toBe(true); }); it("returns candidates sorted by score desc", () => { const hi = { episodeId: 1, animeId: 1, animeTitle: "A", episodeTitle: "01", imageUrl: "img", }; const mid = { episodeId: 2, animeId: 1, animeTitle: "B", episodeTitle: "02", imageUrl: null, }; const low = { episodeId: 3, animeId: null, animeTitle: null, episodeTitle: null, imageUrl: "x", }; const r = rankCandidates([low, mid, hi]); expect(r.candidates.map((c) => c.episodeId)).toEqual([1, 2, 3]); }); it("本地标题包含命中加分:多候选时命中者高置信", () => { const list = [ { episodeId: 1, animeId: 1, animeTitle: "迷宫饭", episodeTitle: "01", imageUrl: null }, { episodeId: 2, animeId: 2, animeTitle: "芙莉莲", episodeTitle: "01", imageUrl: null }, ]; const r = rankCandidates(list, { hint: { title: "葬送的芙莉莲" } }); expect(r.highConfidence).toBe(true); expect(r.candidates[0]?.episodeId).toBe(2); }); it("本地标题完全相等加分高于部分包含", () => { const list = [ { episodeId: 1, animeId: 1, animeTitle: "命运石之门", episodeTitle: "01", imageUrl: null, }, { episodeId: 2, animeId: 2, animeTitle: "命运石之门 负荷领域", episodeTitle: "01", imageUrl: null, }, ]; // 本地标题「命运石之门」:候选 1 完全相等(+1),候选 2 为包含(+0.6) const r = rankCandidates(list, { hint: { title: "命运石之门" } }); expect(r.highConfidence).toBe(true); expect(r.candidates[0]?.episodeId).toBe(1); }); it("rawName 包含候选标题也加分", () => { const list = [ { episodeId: 1, animeId: 1, animeTitle: "迷宫饭", episodeTitle: "01", imageUrl: null }, { episodeId: 2, animeId: 2, animeTitle: "芙莉莲", episodeTitle: "01", imageUrl: null }, ]; const r = rankCandidates(list, { hint: { title: "本地标题", rawName: "[Sub] Sousou no Frieren - 01.mkv" }, }); // rawName 不含任一候选,无加分 → 同分不高置信 expect(r.highConfidence).toBe(false); const r2 = rankCandidates(list, { hint: { title: "x", rawName: "[Sub] 芙莉莲 - 01.mkv" }, }); expect(r2.highConfidence).toBe(true); expect(r2.candidates[0]?.episodeId).toBe(2); }); it("单字符候选名不参与包含命中,避免噪声加分", () => { const list = [ { episodeId: 1, animeId: 1, animeTitle: "T", episodeTitle: "E", imageUrl: null }, { episodeId: 2, animeId: 2, animeTitle: "T2", episodeTitle: "E2", imageUrl: null }, ]; const r = rankCandidates(list, { hint: { title: "Title", rawName: "Title - 01.mkv", epNumber: 1 }, }); expect(r.highConfidence).toBe(false); }); it("集号相等加分拉开同标题候选差距", () => { const list = [ { episodeId: 1, animeId: 1, animeTitle: "Show", episodeTitle: "02", imageUrl: null }, { episodeId: 2, animeId: 1, animeTitle: "Show", episodeTitle: "01", imageUrl: null }, ]; const r = rankCandidates(list, { hint: { title: "Show", epNumber: 1 } }); expect(r.highConfidence).toBe(true); expect(r.candidates[0]?.episodeId).toBe(2); }); it("无 hint 时保持字段存在性打分(默认 lead 0.15)", () => { const list = [ { episodeId: 1, animeId: 1, animeTitle: "迷宫饭", episodeTitle: "01", imageUrl: null }, { episodeId: 2, animeId: 2, animeTitle: "芙莉莲", episodeTitle: "01", imageUrl: null }, ]; expect(rankCandidates(list).highConfidence).toBe(false); }); });