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.
180 lines
6.3 KiB
TypeScript
180 lines
6.3 KiB
TypeScript
/**
|
|
* G9 — Retraction removes a match (non-derived)
|
|
*
|
|
* Pararules origin: tests/test1.nim "removing facts":
|
|
* session.insert(Bob, Color, "blue")
|
|
* session.insert(Yair, LeftOf, Zach)
|
|
* session.insert(Alice, Color, "maize")
|
|
* session.insert(Yair, RightOf, Bob)
|
|
* check session.queryAll(rule1).len == 1
|
|
*
|
|
* session.retract(Yair, RightOf, Bob)
|
|
* check session.queryAll(rule1).len == 0
|
|
*
|
|
* session.retract(Bob, Color) ← value parameter not required
|
|
* check session.queryAll(rule1).len == 0
|
|
*
|
|
* We test:
|
|
* 1. Insert fact → match appears
|
|
* 2. Retract fact → match disappears
|
|
* 3. Re-insert → match reappears
|
|
* 4. Two-condition join: retracting one condition removes joint match
|
|
* 5. Session.retract only needs (id, attr) — value is not required
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
import { Session } from "../../src/session.js";
|
|
import { BetaMemory } from "../../src/beta.js";
|
|
import { Token } from "../../src/beta.js";
|
|
import { JoinNode } from "../../src/join.js";
|
|
import { ProductionNode } from "../../src/query.js";
|
|
import type { EntityId } from "../../src/schema.js";
|
|
import type { JoinTest } from "../../src/join.js";
|
|
import type { TokenFact } from "../../src/beta.js";
|
|
|
|
const ROOT_FACT: TokenFact = { id: 0 as EntityId, attr: "__root", value: null };
|
|
|
|
function buildSingleCondNet(attr: string) {
|
|
const session = new Session({ autoFire: false });
|
|
const alpha = session._getAlpha();
|
|
|
|
const rootMem = new BetaMemory();
|
|
rootMem.leftActivate(new Token(null, ROOT_FACT, {}));
|
|
|
|
const alphaNode = alpha.buildNode({ id: null, attr });
|
|
const join = new JoinNode(rootMem, alphaNode.memory, [], "val", "eid");
|
|
alphaNode.onActivate((id, a, value) => join.rightActivate(id, a, value));
|
|
alphaNode.onDeactivate((id, a, value) => join.rightDeactivate(id, a, value));
|
|
|
|
const prod = new ProductionNode("rule");
|
|
join.addDownstreamActivate(prod.leftActivate.bind(prod));
|
|
join.addDownstreamDeactivate(prod.leftDeactivate.bind(prod));
|
|
|
|
return { session, prod };
|
|
}
|
|
|
|
function buildTwoCondNet() {
|
|
const session = new Session({ autoFire: false });
|
|
const alpha = session._getAlpha();
|
|
|
|
const rootMem = new BetaMemory();
|
|
rootMem.leftActivate(new Token(null, ROOT_FACT, {}));
|
|
|
|
const alphaX = alpha.buildNode({ id: null, attr: "X" });
|
|
const alphaY = alpha.buildNode({ id: null, attr: "Y" });
|
|
const mem1 = new BetaMemory();
|
|
const idEq: JoinTest = { type: "idEquality", leftVar: "eid" };
|
|
|
|
const join1 = new JoinNode(rootMem, alphaX.memory, [], "x", "eid");
|
|
const join2 = new JoinNode(mem1, alphaY.memory, [idEq], "y", null);
|
|
|
|
join1.addDownstreamActivate(mem1.leftActivate.bind(mem1));
|
|
join1.addDownstreamDeactivate(mem1.leftDeactivate.bind(mem1));
|
|
mem1.addDownstreamActivate(join2.leftActivate.bind(join2));
|
|
mem1.addDownstreamDeactivate(join2.leftDeactivate.bind(join2));
|
|
|
|
alphaX.onActivate((id, attr, value) => join1.rightActivate(id, attr, value));
|
|
alphaX.onDeactivate((id, attr, value) => join1.rightDeactivate(id, attr, value));
|
|
alphaY.onActivate((id, attr, value) => join2.rightActivate(id, attr, value));
|
|
alphaY.onDeactivate((id, attr, value) => join2.rightDeactivate(id, attr, value));
|
|
|
|
const prod = new ProductionNode("xyRule");
|
|
join2.addDownstreamActivate(prod.leftActivate.bind(prod));
|
|
join2.addDownstreamDeactivate(prod.leftDeactivate.bind(prod));
|
|
|
|
return { session, prod };
|
|
}
|
|
|
|
describe("G9 — retraction removes match", () => {
|
|
it("insert → match appears, retract → match disappears", () => {
|
|
const { session, prod } = buildSingleCondNet("Health");
|
|
|
|
const e1 = session.nextId();
|
|
session.insert(e1, "Health", 100);
|
|
expect(prod.matches).toHaveLength(1);
|
|
|
|
session.retract(e1, "Health");
|
|
expect(prod.matches).toHaveLength(0);
|
|
});
|
|
|
|
it("retract then re-insert → match reappears", () => {
|
|
const { session, prod } = buildSingleCondNet("Health");
|
|
|
|
const e1 = session.nextId();
|
|
session.insert(e1, "Health", 100);
|
|
expect(prod.matches).toHaveLength(1);
|
|
|
|
session.retract(e1, "Health");
|
|
expect(prod.matches).toHaveLength(0);
|
|
|
|
session.insert(e1, "Health", 50);
|
|
expect(prod.matches).toHaveLength(1);
|
|
expect(prod.matches[0]?.bindings["val"]).toBe(50);
|
|
});
|
|
|
|
it("two-condition join: retracting first condition removes joint match", () => {
|
|
const { session, prod } = buildTwoCondNet();
|
|
|
|
const e1 = session.nextId();
|
|
session.insert(e1, "X", 3);
|
|
session.insert(e1, "Y", 4);
|
|
expect(prod.matches).toHaveLength(1);
|
|
|
|
session.retract(e1, "X");
|
|
expect(prod.matches).toHaveLength(0);
|
|
});
|
|
|
|
it("two-condition join: retracting second condition removes joint match", () => {
|
|
const { session, prod } = buildTwoCondNet();
|
|
|
|
const e1 = session.nextId();
|
|
session.insert(e1, "X", 1);
|
|
session.insert(e1, "Y", 2);
|
|
expect(prod.matches).toHaveLength(1);
|
|
|
|
session.retract(e1, "Y");
|
|
expect(prod.matches).toHaveLength(0);
|
|
});
|
|
|
|
it("retract one entity of many — only that entity's match disappears", () => {
|
|
const { session, prod } = buildSingleCondNet("Health");
|
|
|
|
const e1 = session.nextId();
|
|
const e2 = session.nextId();
|
|
const e3 = session.nextId();
|
|
session.insert(e1, "Health", 10);
|
|
session.insert(e2, "Health", 20);
|
|
session.insert(e3, "Health", 30);
|
|
expect(prod.matches).toHaveLength(3);
|
|
|
|
session.retract(e2, "Health");
|
|
expect(prod.matches).toHaveLength(2);
|
|
// e1 and e3 still present
|
|
const remainingEids = prod.matches.map((m) => m.bindings["eid"]);
|
|
expect(remainingEids).toContain(e1);
|
|
expect(remainingEids).toContain(e3);
|
|
expect(remainingEids).not.toContain(e2);
|
|
});
|
|
|
|
it("update (re-insert same attr) replaces old value in match", () => {
|
|
const { session, prod } = buildSingleCondNet("Health");
|
|
|
|
const e1 = session.nextId();
|
|
session.insert(e1, "Health", 100);
|
|
expect(prod.matches[0]?.bindings["val"]).toBe(100);
|
|
|
|
session.insert(e1, "Health", 50); // update
|
|
expect(prod.matches).toHaveLength(1);
|
|
expect(prod.matches[0]?.bindings["val"]).toBe(50);
|
|
});
|
|
|
|
it("WM.contains returns false after retraction", () => {
|
|
const { session } = buildSingleCondNet("Health");
|
|
|
|
const e1 = session.nextId();
|
|
session.insert(e1, "Health", 100);
|
|
expect(session.contains(e1, "Health")).toBe(true);
|
|
|
|
session.retract(e1, "Health");
|
|
expect(session.contains(e1, "Health")).toBe(false);
|
|
});
|
|
});
|