10 integration tests in packages/rete/tests/golden/ manually wire AlphaNode → BetaMemory → JoinNode → ProductionNode chains and drive them via Session.insert/retract. Each test maps to a pararules Nim reference test (documented in GOLDEN-MAP.md). Coverage: packages/rete/src at 96.8% statements / 95.4% branch / 97.8% functions — all well above the 90% Phase 1 gate. Tests: 227 total (166 pre-existing + 61 new golden), all green.
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
/**
|
|
* G6 — Conflict resolution ordering
|
|
*
|
|
* Pararules origin: tests/test2.nim "recursion limit" (rule1/rule2/rule3 cascade)
|
|
* and SPEC.md §Conflict Resolution which defines salience → specificity → addedAt.
|
|
*
|
|
* Pararules uses salience in rule declarations to control firing order.
|
|
* Our engine implements the same three-key sort via orderActivations():
|
|
* 1. Salience descending
|
|
* 2. Specificity (# conditions) descending
|
|
* 3. Insertion order ascending
|
|
*
|
|
* This golden test verifies the pure ordering function, which is the
|
|
* conflict-resolution kernel that the future agenda will call.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
import { orderActivations } from "../../src/conflict.js";
|
|
import { Token } from "../../src/beta.js";
|
|
import { Session } from "../../src/session.js";
|
|
import { HandlerRegistry } from "../../src/registry.js";
|
|
import { defineRule, v } from "../../src/builder.js";
|
|
import type { Activation, OrderableRule } from "../../src/conflict.js";
|
|
import type { EntityId } from "../../src/schema.js";
|
|
import type { TokenFact } from "../../src/beta.js";
|
|
|
|
const ROOT_FACT: TokenFact = { id: 0 as EntityId, attr: "__root", value: null };
|
|
const dummyToken = new Token(null, ROOT_FACT, {});
|
|
|
|
function mkActivation(
|
|
name: string,
|
|
salience: number,
|
|
conditionCount: number,
|
|
addedAt: number,
|
|
): Activation {
|
|
const rule: OrderableRule = {
|
|
name,
|
|
salience,
|
|
conditions: Array.from({ length: conditionCount }, () => ({})),
|
|
handler: "noop",
|
|
addedAt,
|
|
};
|
|
return { rule, token: dummyToken };
|
|
}
|
|
|
|
describe("G6 — conflict resolution ordering", () => {
|
|
it("higher salience fires first", () => {
|
|
const activations = [
|
|
mkActivation("low", 0, 1, 0),
|
|
mkActivation("high", 10, 1, 1),
|
|
mkActivation("mid", 5, 1, 2),
|
|
];
|
|
|
|
const ordered = orderActivations(activations);
|
|
expect(ordered[0]?.rule.name).toBe("high");
|
|
expect(ordered[1]?.rule.name).toBe("mid");
|
|
expect(ordered[2]?.rule.name).toBe("low");
|
|
});
|
|
|
|
it("salience ties broken by specificity (more conditions wins)", () => {
|
|
const activations = [
|
|
mkActivation("oneCondition", 10, 1, 0),
|
|
mkActivation("twoConditions", 10, 2, 1),
|
|
mkActivation("threeConditions", 10, 3, 2),
|
|
];
|
|
|
|
const ordered = orderActivations(activations);
|
|
expect(ordered[0]?.rule.name).toBe("threeConditions");
|
|
expect(ordered[1]?.rule.name).toBe("twoConditions");
|
|
expect(ordered[2]?.rule.name).toBe("oneCondition");
|
|
});
|
|
|
|
it("salience and specificity ties broken by insertion order ascending", () => {
|
|
const activations = [
|
|
mkActivation("added2", 5, 2, 2),
|
|
mkActivation("added0", 5, 2, 0),
|
|
mkActivation("added1", 5, 2, 1),
|
|
];
|
|
|
|
const ordered = orderActivations(activations);
|
|
expect(ordered[0]?.rule.name).toBe("added0");
|
|
expect(ordered[1]?.rule.name).toBe("added1");
|
|
expect(ordered[2]?.rule.name).toBe("added2");
|
|
});
|
|
|
|
it("mixed case: salience, then specificity, then addedAt", () => {
|
|
const activations = [
|
|
mkActivation("ruleC", 5, 3, 2), // low salience, many conditions
|
|
mkActivation("ruleA", 10, 1, 0), // high salience, 1 condition
|
|
mkActivation("ruleB", 10, 2, 1), // high salience, 2 conditions
|
|
];
|
|
|
|
const ordered = orderActivations(activations);
|
|
// B > A (same salience, B has more conditions)
|
|
// A > C (higher salience)
|
|
expect(ordered[0]?.rule.name).toBe("ruleB");
|
|
expect(ordered[1]?.rule.name).toBe("ruleA");
|
|
expect(ordered[2]?.rule.name).toBe("ruleC");
|
|
});
|
|
|
|
it("empty activations list → empty result", () => {
|
|
expect(orderActivations([])).toHaveLength(0);
|
|
});
|
|
|
|
it("single activation → unchanged", () => {
|
|
const single = [mkActivation("only", 10, 2, 0)];
|
|
const ordered = orderActivations(single);
|
|
expect(ordered).toHaveLength(1);
|
|
expect(ordered[0]?.rule.name).toBe("only");
|
|
});
|
|
|
|
it("orderActivations does not mutate the input array", () => {
|
|
const activations = [
|
|
mkActivation("b", 5, 1, 0),
|
|
mkActivation("a", 10, 1, 1),
|
|
];
|
|
const original = [...activations];
|
|
orderActivations(activations);
|
|
expect(activations[0]?.rule.name).toBe(original[0]?.rule.name);
|
|
expect(activations[1]?.rule.name).toBe(original[1]?.rule.name);
|
|
});
|
|
|
|
it("Session._getOrderableRules returns rules in addedAt order with correct salience", () => {
|
|
const registry = new HandlerRegistry();
|
|
registry.register("noop", () => {});
|
|
|
|
const session = new Session({ autoFire: false });
|
|
|
|
const r1 = defineRule({
|
|
name: "rule1",
|
|
salience: 0,
|
|
what: [{ id: null, attr: "X", binding: v("x") }],
|
|
handler: "noop",
|
|
registry,
|
|
});
|
|
const r2 = defineRule({
|
|
name: "rule2",
|
|
salience: 10,
|
|
what: [{ id: null, attr: "Y", binding: v("y") }],
|
|
handler: "noop",
|
|
registry,
|
|
});
|
|
session.add(r1);
|
|
session.add(r2);
|
|
|
|
const orderable = session._getOrderableRules();
|
|
expect(orderable).toHaveLength(2);
|
|
expect(orderable[0]?.name).toBe("rule1");
|
|
expect(orderable[0]?.salience).toBe(0);
|
|
expect(orderable[0]?.addedAt).toBe(0);
|
|
expect(orderable[1]?.name).toBe("rule2");
|
|
expect(orderable[1]?.salience).toBe(10);
|
|
expect(orderable[1]?.addedAt).toBe(1);
|
|
});
|
|
});
|