import type { EpisodeMappingPreview, EpisodeMappingRuleRow } from "@umt/contracts"; import { describe, expect, it } from "vitest"; import { EXCLUDE_REASON } from "../organize-wizard/model.ts"; import { absoluteGroups, agentRuleInfo, canonicalText, disabledReasonText, draftFromSearch, draftOfRule, EMPTY_DRAFT, groupMethodsText, METHOD_LABEL, mappingHref, methodText, parseTable, reresolveToast, ruleText, sourceText, specOfDraft, } from "./model.ts"; describe("集数映射文案", () => { it("方式名称给人话,覆盖全部方式", () => { expect(METHOD_LABEL).toEqual({ raw: "原号", rule: "按规则", absolute_in_season: "季内绝对集号", absolute_cross_season: "跨季绝对集号", air_date: "按播出日期", tmdb_season: "按 TMDB 默认季集换算", unverified: "元数据未覆盖,先按原号", unresolved: "待确认", skipped: "不适用", }); expect(methodText({ method: "rule", special: "ignore" })).toBe("按规则:忽略"); expect(methodText(null)).toBe("未解析"); }); it("季集写法:原始 / 规范 / 特别篇 / 播出日期", () => { expect(sourceText({ source: { season: 1, episodes: { start: 25, end: 25 } } })).toBe("S01E25"); expect(sourceText({ source: { episodes: { start: 13, end: 14 } } })).toBe("E13-E14"); expect(sourceText({ source: { airDate: "20250314" } })).toBe("2025-03-14"); expect( canonicalText({ method: "absolute_cross_season", canonical: { season: 2, episodes: { start: 1, end: 1 } } }), ).toBe("S02E01"); expect(canonicalText({ method: "rule", canonical: null, special: "extra" })).toBe("花絮"); expect(canonicalText({ method: "unresolved", canonical: null })).toBe("—"); }); it("整理向导的排除原因有「集数待确认」与特典花絮", () => { expect(EXCLUDE_REASON.episode_unresolved).toBe("集数待确认"); expect(EXCLUDE_REASON.special_extra).toBeTruthy(); }); it("停用原因:手动 / 规则失效", () => { const rule = { status: "disabled", disabledReason: JSON.stringify({ manual: true }) } as EpisodeMappingRuleRow; expect(disabledReasonText(rule)).toBe("手动停用"); expect( disabledReasonText({ ...rule, disabledReason: JSON.stringify({ source: { season: 2, episodes: { start: 30, end: 30 } }, evidence: [] }), }), ).toBe("规则算出的集数对不上季表(来源 S02E30),已自动停用"); expect(disabledReasonText({ ...rule, status: "active" })).toBeNull(); }); }); describe("对照表与草稿", () => { it("对照表文本解析与回写", () => { const t = parseTable("26=S02E01\n27-28 = s2e2\n"); expect(t.error).toBeNull(); expect(t.entries).toEqual([ { from: { start: 26, end: 26 }, to: { season: 2, episode: 1 } }, { from: { start: 27, end: 28 }, to: { season: 2, episode: 2 } }, ]); expect(ruleText({ kind: "table", tableEntries: t.entries })).toBe("26=S02E01 等 2 行"); expect(parseTable("26->").error).toContain("第 1 行"); expect(parseTable("28-27=S02E01").error).toContain("写反"); }); it("草稿 → 规则内容:不完整时给错误,不发预览", () => { expect(specOfDraft({ ...EMPTY_DRAFT, expression: "" }).error).toContain("偏移表达式"); expect(specOfDraft({ ...EMPTY_DRAFT, season: "x", expression: "EP-12" }).error).toContain("来源季"); expect(specOfDraft({ ...EMPTY_DRAFT, season: "2", releaseGroup: " ANi ", expression: "EP-12" }).spec).toEqual({ season: 2, releaseGroup: "ANi", kind: "expression", expression: "EP-12", targetSeason: null, }); expect(specOfDraft({ ...EMPTY_DRAFT, kind: "special", special: "extra" }).spec).toMatchObject({ kind: "special", special: "extra", season: null, releaseGroup: null, }); }); it("规则 → 草稿 → 规则内容 保持一致", () => { const row = { season: 1, releaseGroup: "Lilith-Raws", kind: "expression", expression: "EP-30", targetSeason: 2, tableEntries: null, special: null, } as EpisodeMappingRuleRow; expect(specOfDraft(draftOfRule(row)).spec).toEqual({ season: 1, releaseGroup: "Lilith-Raws", kind: "expression", expression: "EP-30", targetSeason: 2, }); }); it("待处理「修改」的跳转地址能读回预填草稿", () => { const href = mappingHref("/console/library/x", { season: 1, releaseGroup: "ANi", expression: "EP-12", targetSeason: 2, }); expect(href).toBe("/console/library/x?mapSeason=1&mapGroup=ANi&mapExpr=EP-12&mapTarget=2#episode-mapping"); const sp = new URLSearchParams(href.slice(href.indexOf("?") + 1, href.indexOf("#"))); expect(draftFromSearch(sp)).toMatchObject({ season: "1", releaseGroup: "ANi", expression: "EP-12", targetSeason: "2", }); expect(draftFromSearch(new URLSearchParams())).toBeNull(); }); }); describe("按发布组统计", () => { const preview = { groups: [ { releaseGroup: "ANi", total: 5, pending: 0, suggestion: null, methods: [ { method: "raw", count: 1 }, { method: "absolute_cross_season", count: 4 }, ], }, { releaseGroup: null, total: 1, pending: 1, suggestion: null, methods: [{ method: "unresolved", count: 1 }] }, ], } as unknown as EpisodeMappingPreview; it("一句话统计按数量排序", () => { expect(groupMethodsText(preview.groups[0]!)).toBe("ANi:跨季绝对集号 4、原号 1"); expect(groupMethodsText(preview.groups[1]!)).toBe("未知发布组:待确认 1"); }); it("找出用绝对集号的发布组", () => { expect(absoluteGroups(preview)).toEqual(["ANi"]); expect(absoluteGroups(undefined)).toEqual([]); }); }); describe("应用后的提示", () => { it("只列非零项", () => { expect(reresolveToast({ releases: 12, changed: 2, resolved: 2 })).toBe( "已应用:重算 12 条资源,2 条有变化,2 条待确认已解决", ); expect(reresolveToast({ releases: 3, changed: 0, resolved: 0 }, "已停用")).toBe("已停用:重算 3 条资源"); }); }); describe("智能体写的规则", () => { it("标出智能体来源与证据等级,带写规则时的理由;其它来源不标", () => { const rule = (over: Partial) => ({ createdByType: "agent", evidenceLevel: "multi_evidence", evidence: {}, ...over }) as EpisodeMappingRuleRow; expect(agentRuleInfo(rule({ evidence: { reasons: ["发布时间对上播出日期", 3] } }))).toEqual({ label: "智能体 · 多证据", reasons: ["发布时间对上播出日期"], }); expect(agentRuleInfo(rule({ evidenceLevel: "needs_review" }))?.label).toBe("智能体 · 待复核"); expect(agentRuleInfo(rule({ createdByType: "user", evidenceLevel: "manual" }))).toBeNull(); }); });