import { describe, expect, it } from "@suaveplan/testing/runner"; import { and, condition, or } from "../builder/index.js"; import type { Rule } from "../types/index.js"; import { analyzeRule, countRules, estimateCost, findFields, hasAsyncComponents, isOptimisationStable, optimizeRule, } from "./optimizer.js"; describe("optimizeRule — flatten", () => { it("flattens nested AND of AND", { timeout: 5000 }, () => { const r = and( and(condition("a", "equals", 1), condition("b", "equals", 2)), condition("c", "equals", 3) ); const opt = optimizeRule(r); const rules = (opt.rule as { rules: readonly Rule[] }).rules; expect(rules.length).toBe(3); }); it("flattens nested OR of OR", { timeout: 5000 }, () => { const r = or( or(condition("a", "equals", 1), condition("b", "equals", 2)), condition("c", "equals", 3) ); const opt = optimizeRule(r); expect((opt.rule as { rules: readonly Rule[] }).rules.length).toBe(3); }); it("does not flatten AND of OR", { timeout: 5000 }, () => { const r = and( or(condition("a", "equals", 1), condition("b", "equals", 2)), condition("c", "equals", 3) ); const opt = optimizeRule(r, { dedupe: false, simplify: false, sort: false, }); expect((opt.rule as { rules: readonly Rule[] }).rules.length).toBe(2); }); it("flatten can be disabled", { timeout: 5000 }, () => { const r = and( and(condition("a", "equals", 1), condition("b", "equals", 2)), condition("c", "equals", 3) ); const opt = optimizeRule(r, { flatten: false, dedupe: false, simplify: false, sort: false, }); expect((opt.rule as { rules: readonly Rule[] }).rules.length).toBe(2); }); }); describe("optimizeRule — dedupe", () => { it("removes structural duplicates", { timeout: 5000 }, () => { const c = condition("a", "equals", 1); const r = and(c, c, condition("b", "equals", 2)); const opt = optimizeRule(r, { sort: false, simplify: false }); expect((opt.rule as { rules: readonly Rule[] }).rules.length).toBe(2); }); it("dedupe recurses into groups", { timeout: 5000 }, () => { const group: Rule = { type: "group", name: "g", rules: [condition("a", "equals", 1), condition("a", "equals", 1)], }; const opt = optimizeRule(group); expect((opt.rule as { rules: readonly Rule[] }).rules.length).toBe(1); }); it("dedupe can be disabled", { timeout: 5000 }, () => { const c = condition("a", "equals", 1); const r = and(c, c); const opt = optimizeRule(r, { dedupe: false, simplify: false }); expect((opt.rule as { rules: readonly Rule[] }).rules.length).toBe(2); }); }); describe("optimizeRule — simplify", () => { it("unwraps single-child AND/OR", { timeout: 5000 }, () => { const r: Rule = { type: "logical", operator: "and", rules: [condition("a", "equals", 1)], }; const opt = optimizeRule(r, { flatten: false, dedupe: false, sort: false, }); expect(opt.rule.type).toBe("condition"); }); it("preserves NOT (single-child by design)", { timeout: 5000 }, () => { const r: Rule = { type: "logical", operator: "not", rules: [condition("a", "equals", 1)], }; const opt = optimizeRule(r); expect(opt.rule.type).toBe("logical"); expect((opt.rule as { operator: string }).operator).toBe("not"); }); it("simplify can be disabled", { timeout: 5000 }, () => { const r: Rule = { type: "logical", operator: "and", rules: [condition("a", "equals", 1)], }; const opt = optimizeRule(r, { simplify: false, flatten: false, dedupe: false, sort: false, }); expect(opt.rule.type).toBe("logical"); }); }); describe("optimizeRule — sort", () => { it("sorts AND children by ascending cost", { timeout: 5000 }, () => { const r = and( condition("a", "matches", "^x$"), condition("b", "equals", 1) ); const opt = optimizeRule(r, { flatten: false, dedupe: false, simplify: false, }); const rules = (opt.rule as { rules: readonly Rule[] }).rules; expect((rules[0] as { operator: string }).operator).toBe("equals"); }); it("does not sort NOT or group implicitly", { timeout: 5000 }, () => { const r: Rule = { type: "logical", operator: "not", rules: [condition("a", "equals", 1)], }; const opt = optimizeRule(r); expect(opt.rule.type).toBe("logical"); }); it("sort can be disabled", { timeout: 5000 }, () => { const r = and( condition("a", "matches", "^x$"), condition("b", "equals", 1) ); const opt = optimizeRule(r, { sort: false, dedupe: false, simplify: false, flatten: false, }); expect((opt.rule as { rules: readonly Rule[] }).rules[0]?.type).toBe( "condition" ); }); it("group children pass through sort", { timeout: 5000 }, () => { const r: Rule = { type: "group", name: "g", rules: [ condition("a", "matches", "^x$"), condition("b", "equals", 1), ], }; const opt = optimizeRule(r); expect(opt.rule.type).toBe("group"); }); }); describe("analyzeRule", () => { it("counts total + depth + fields", { timeout: 5000 }, () => { const r = and( condition("a", "equals", 1), or(condition("b", "equals", 2), condition("c", "equals", 3)) ); const stats = analyzeRule(r); expect(stats.totalRules).toBe(5); expect(stats.maxDepth).toBe(2); expect(stats.fields).toEqual(expect.arrayContaining(["a", "b", "c"])); }); it("detects async via custom/computed", { timeout: 5000 }, () => { const custom: Rule = { type: "custom", name: "c", validate: async () => true, }; expect(analyzeRule(custom).hasAsync).toBe(true); const computed: Rule = { type: "computed", field: "x", operator: "equals", compute: () => 1, }; expect(analyzeRule(computed).hasAsync).toBe(true); }); it("hasAsyncComponents alias", { timeout: 5000 }, () => { expect(hasAsyncComponents(condition("a", "equals", 1))).toBe(false); expect( hasAsyncComponents({ type: "custom", name: "c", validate: () => true, }) ).toBe(true); }); it("countRules / findFields / estimateCost helpers", { timeout: 5000, }, () => { const r = and( condition("a", "equals", 1), condition("b", "matches", "^x$") ); expect(countRules(r)).toBe(3); expect(findFields(r)).toEqual(expect.arrayContaining(["a", "b"])); expect(estimateCost(r)).toBeGreaterThan(0); }); it("isOptimisationStable returns true when no changes", { timeout: 5000, }, () => { const r = condition("a", "equals", 1); expect(isOptimisationStable(r)).toBe(true); }); it("isOptimisationStable returns false when optimisations apply", { timeout: 5000, }, () => { const r = and( and(condition("a", "equals", 1), condition("b", "equals", 2)) ); expect(isOptimisationStable(r)).toBe(false); }); it("walks group children", { timeout: 5000 }, () => { const r: Rule = { type: "group", name: "g", rules: [condition("a", "equals", 1), condition("b", "equals", 2)], }; expect(analyzeRule(r).totalRules).toBe(3); }); }); describe("optimizeRule — result", () => { it("reports before + after stats", { timeout: 5000 }, () => { const r = and( and(condition("a", "equals", 1), condition("b", "equals", 2)) ); const opt = optimizeRule(r); expect(opt.before.totalRules).toBeGreaterThan(opt.after.totalRules); }); });