Initial Commit
This commit is contained in:
270
packages/foundation/flow/rules/src/optimizer/optimizer.test.ts
Normal file
270
packages/foundation/flow/rules/src/optimizer/optimizer.test.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user