import { describe, expect, it } from "vitest"; import { currentPri, evaluateCandidate, parseRelease, parseRuleString, type RuleGroupSpec, rankForSlot, type SlotSpec, } from "../src/index.ts"; const group = (name: string, s: string, extra: Partial = {}): RuleGroupSpec => ({ name, layers: s ? parseRuleString(s).layers : [], ...extra, }); const base = (groups: RuleGroupSpec[], extra: Partial = {}): SlotSpec => ({ name: "main", hard: {}, rules: { groups }, ...extra, }); const rel = (t: string, subtitle?: string) => parseRelease(t, subtitle ? { subtitle } : {}); describe("版本目标接入规则组", () => { it("硬排除命中 → reject(rule.hard_exclude),trace 带组名与修订号", () => { const g = group("不要原盘", "", { id: "g1", revision: 3, hardExcludes: parseRuleString("BLU | REMUX").layers.map((l) => l.expr), }); const ev = evaluateCandidate(base([g]), rel("Movie.2023.1080p.BluRay.REMUX.AVC-EPSiLON")); expect(ev.decision).toBe("reject"); if (ev.decision !== "reject") return; expect(ev.violations).toEqual([ { field: "rule", code: "hard_exclude", expected: "不要原盘", actual: ["BLU | REMUX"] }, ]); expect(ev.trace?.join("\n")).toContain("规则组「不要原盘」@r3 硬排除 BLU | REMUX 命中"); }); it("多组串联取交集,pri 取最后一个带层的组", () => { const spec = base([group("分辨率", "4K > 1080P"), group("字幕", "CNSUB > !CNSUB"), group("空组", "")]); const a = evaluateCandidate(spec, rel("Dune.2024.1080p.WEB-DL.H264-HHWEB", "中字")); expect(a).toMatchObject({ decision: "accept", pri: 100 }); const b = evaluateCandidate(spec, rel("Dune.2024.2160p.WEB-DL.H265-FLUX")); expect(b).toMatchObject({ decision: "accept", pri: 99 }); const c = evaluateCandidate(spec, rel("Dune.2024.720p.WEBRip.x264-GRP")); expect(c.decision).toBe("reject"); if (c.decision === "reject") expect(c.violations[0]).toMatchObject({ code: "no_layer_matched", expected: "分辨率" }); }); it("ladderRequired=false 的软偏好组不中只降 pri,不拒绝", () => { const spec = base([group("偏好", "SPECSUB", { ladderRequired: false })]); const r = rel("X.2024.1080p.WEB-DL.H264-G", "特效中字"); expect(evaluateCandidate(spec, r, null, { subtitle: "特效中字" })).toMatchObject({ pri: 100 }); expect(evaluateCandidate(spec, rel("X.2024.1080p.WEB-DL.H264-G"))).toMatchObject({ decision: "accept", pri: 0 }); }); it("有规则组阶梯时升级按 pri 严格提升(洗版)", () => { const spec = base([group("洗版", "REMUX > BLURAY > WEBDL")], { upgrade: { enabled: true } }); const up = evaluateCandidate(spec, rel("M.2023.1080p.BluRay.REMUX.AVC-E"), { source: "WEB-DL", resolution: "1080p", pri: 98, }); expect(up).toMatchObject({ decision: "upgrade", pri: 100, fromPri: 98 }); const same = evaluateCandidate(spec, rel("M.2023.1080p.WEB-DL.H264-G"), { source: "WEB-DL", pri: 98 }); expect(same.decision).toBe("reject"); expect(currentPri(spec, { source: "REMUX", resolution: "1080p" })).toBe(100); }); it("作品原语言中文时 CNSUB 放行(MP tmdb 条件)", () => { const spec = base([group("中字", "CNSUB")]); const r = rel("Movie.2023.1080p.WEB-DL.H264-G"); expect(evaluateCandidate(spec, r).decision).toBe("reject"); expect(evaluateCandidate(spec, r, null, { originalLanguage: "zh" })).toMatchObject({ decision: "accept" }); }); it("rankForSlot 按规则组 pri 排序", () => { const spec = base([group("阶梯", "4K > 1080P > 720P")]); const ranked = rankForSlot(spec, [ rel("A.720p.WEB-DL.x264-G"), rel("A.2160p.WEB-DL.x265-G"), rel("A.1080p.WEB-DL.x264-G"), ]); expect(ranked.map((x) => x.evaluation.decision === "accept" && x.evaluation.pri)).toEqual([100, 99, 98]); }); it("没有规则组时行为不变", () => { const r = rel("Movie.2023.1080p.WEB-DL.H264-G"); expect(evaluateCandidate(base([]), r)).toEqual(evaluateCandidate({ name: "main", hard: {} }, r)); }); });