feat(web): rules page with layer cards, rule-string sync, preview and preferences as tags (FE-3)
- /console/rules tabs: rule groups, custom conditions, defaults, acquisition policies, suppressions - editor: rule string parsed live via /rules/parse (position + suggestions) and synced both ways with layer cards; hard excludes; expectedRevision conflicts keep local edits - preview against recent releases or pasted titles with pri / level / reasons / trace - single-rule groups shown as preference tags; disable/delete writes suppression records - /console/preferences redirects to /console/rules (origin passed through) Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
81bf010e3e
commit
b908b4a0c9
12 files changed
+2657
-427
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import type { RuleExpr, RuleLayer } from "@umt/contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { cardsOf, cardToLayer, exprToString, layersToString } from "./expr.ts";
|
||||
|
||||
const a = (id: string): RuleExpr => ({ op: "atom", id });
|
||||
const not = (arg: RuleExpr): RuleExpr => ({ op: "not", arg });
|
||||
const and = (...args: RuleExpr[]): RuleExpr => ({ op: "and", args });
|
||||
const or = (...args: RuleExpr[]): RuleExpr => ({ op: "or", args });
|
||||
|
||||
describe("规则串序列化", () => {
|
||||
it("只加必要括号,原子大写", () => {
|
||||
expect(exprToString(and(a("specsub"), a("CNSUB")))).toBe("SPECSUB & CNSUB");
|
||||
expect(exprToString(or(and(a("4K"), a("HDR")), a("1080P")))).toBe("4K & HDR | 1080P");
|
||||
expect(exprToString(and(or(a("4K"), a("1080P")), not(a("BLU"))))).toBe("(4K | 1080P) & !BLU");
|
||||
expect(exprToString(not(or(a("REMUX"), a("BLU"))))).toBe("!(REMUX | BLU)");
|
||||
});
|
||||
|
||||
it("多层用 > 连接", () => {
|
||||
const layers: RuleLayer[] = [{ expr: and(a("SPECSUB"), a("CNSUB")) }, { expr: a("BLU") }];
|
||||
expect(layersToString(layers)).toBe("SPECSUB & CNSUB > BLU");
|
||||
});
|
||||
});
|
||||
|
||||
describe("层卡片往返", () => {
|
||||
it("两层以内的表达式可转卡片再转回", () => {
|
||||
const layers: RuleLayer[] = [
|
||||
{ expr: and(or(a("4K"), a("1080P")), not(a("BLU"))) },
|
||||
{ expr: not(a("REMUX")) },
|
||||
{ expr: or(a("CNSUB"), not(and(a("HDR"), a("DOLBY")))) },
|
||||
];
|
||||
const cards = cardsOf(layers);
|
||||
expect(cards).not.toBeNull();
|
||||
expect(cards?.map((c) => cardToLayer(c))).toEqual(layers);
|
||||
});
|
||||
|
||||
it("嵌套太深时返回 null,只能在规则串里改", () => {
|
||||
expect(cardsOf([{ expr: and(a("A"), or(a("B"), and(a("C"), a("D")))) }])).toBeNull();
|
||||
});
|
||||
|
||||
it("空层转不出表达式", () => {
|
||||
expect(cardToLayer({ op: "and", items: [] })).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user