Files
UHD_Media_Tool/packages/domain/test/rules-slot.test.ts
T
rootandClaude Opus 5.5 e994a7bcfa feat(domain): rule groups — MP rule-string parser/serializer, builtin keywords, chain evaluation wired into slot
- 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]>
2026-09-28 14:50:43 +08:00

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));
});
});