- rules/: RuleExpr tree, '>' layers with ! & | and parens, position-aware errors (unknown atom with suggestions, unsupported music atoms, parens, empty layer) - builtin keyword table aligned with MP BUILTIN_RULE_SET; field-based judgement where parsed fields exist, MP regex otherwise; custom atoms (MP 7 fields) - chain evaluation: hard excludes, ladder pri 100-k, groups AND-ed, pri from last laddered group, readable per-atom trace - slot: SlotSpec.rules, rule.hard_exclude / rule.no_layer_matched violations, pri basis switches to priority when a rule ladder is present - differential: 66 rule chains x 32 torrents vs MP python path (parse 64/64, pri 2090/2112, 22 registered mp_bug) Co-Authored-By: Claude Opus 5.5 <[email protected]>
95 lines
4.0 KiB
TypeScript
95 lines
4.0 KiB
TypeScript
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> = {}): RuleGroupSpec => ({
|
|
name,
|
|
layers: s ? parseRuleString(s).layers : [],
|
|
...extra,
|
|
});
|
|
const base = (groups: RuleGroupSpec[], extra: Partial<SlotSpec> = {}): 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));
|
|
});
|
|
});
|