test(rete): port pararules golden tests; tag Phase 1 parity (P1.14)
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.
This commit is contained in:
parent
0259268e34
commit
0401295bbc
12 changed files with 1510 additions and 2 deletions
174
packages/rete/tests/golden/g8-variable-binding-chain.test.ts
Normal file
174
packages/rete/tests/golden/g8-variable-binding-chain.test.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* G8 — Variable binding propagation across a 3-condition join chain
|
||||
*
|
||||
* Pararules origin: tests/test1.nim "number of conditions != number of facts"
|
||||
* and tests/test2.nim "join value with id" / "multiple joins":
|
||||
* rule rule1(Fact):
|
||||
* what:
|
||||
* (Bob, LeftOf, id) ← id bound from value
|
||||
* (id, Color, color) ← id used in entity position (value join)
|
||||
* (id, Height, height) ← id equality
|
||||
*
|
||||
* Our equivalent: 3-condition chain where eid is bound in condition 1,
|
||||
* then used as idEquality in conditions 2 and 3.
|
||||
*
|
||||
* (?eid, "Type", ?type) ∧ (?eid, "Health", ?hp) ∧ (?eid, "Position", ?pos)
|
||||
*
|
||||
* Network:
|
||||
* root → join1(alphaType) → mem1 → join2(alphaHealth, idEq:eid) →
|
||||
* mem2 → join3(alphaPos, idEq:eid) → prod
|
||||
*/
|
||||
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 };
|
||||
const idEq: JoinTest = { type: "idEquality", leftVar: "eid" };
|
||||
|
||||
function buildThreeConditionNetwork() {
|
||||
const session = new Session({ autoFire: false });
|
||||
const alpha = session._getAlpha();
|
||||
|
||||
// Root
|
||||
const rootMem = new BetaMemory();
|
||||
rootMem.leftActivate(new Token(null, ROOT_FACT, {}));
|
||||
|
||||
// Alpha nodes for each condition
|
||||
const alphaType = alpha.buildNode({ id: null, attr: "Type" });
|
||||
const alphaHealth = alpha.buildNode({ id: null, attr: "Health" });
|
||||
const alphaPos = alpha.buildNode({ id: null, attr: "Position" });
|
||||
|
||||
// Intermediate beta memories
|
||||
const mem1 = new BetaMemory();
|
||||
const mem2 = new BetaMemory();
|
||||
|
||||
// join1: root × Type → binds type and eid
|
||||
const join1 = new JoinNode(rootMem, alphaType.memory, [], "type", "eid");
|
||||
|
||||
// join2: mem1 × Health with idEquality → binds hp
|
||||
const join2 = new JoinNode(mem1, alphaHealth.memory, [idEq], "hp", null);
|
||||
|
||||
// join3: mem2 × Position with idEquality → binds pos
|
||||
const join3 = new JoinNode(mem2, alphaPos.memory, [idEq], "pos", null);
|
||||
|
||||
// Wire: join1 → mem1 → join2 → mem2 → join3
|
||||
join1.addDownstreamActivate(mem1.leftActivate.bind(mem1));
|
||||
join1.addDownstreamDeactivate(mem1.leftDeactivate.bind(mem1));
|
||||
mem1.addDownstreamActivate(join2.leftActivate.bind(join2));
|
||||
mem1.addDownstreamDeactivate(join2.leftDeactivate.bind(join2));
|
||||
|
||||
join2.addDownstreamActivate(mem2.leftActivate.bind(mem2));
|
||||
join2.addDownstreamDeactivate(mem2.leftDeactivate.bind(mem2));
|
||||
mem2.addDownstreamActivate(join3.leftActivate.bind(join3));
|
||||
mem2.addDownstreamDeactivate(join3.leftDeactivate.bind(join3));
|
||||
|
||||
// Wire alpha events → join right sides
|
||||
alphaType.onActivate((id, attr, value) => join1.rightActivate(id, attr, value));
|
||||
alphaType.onDeactivate((id, attr, value) => join1.rightDeactivate(id, attr, value));
|
||||
alphaHealth.onActivate((id, attr, value) => join2.rightActivate(id, attr, value));
|
||||
alphaHealth.onDeactivate((id, attr, value) => join2.rightDeactivate(id, attr, value));
|
||||
alphaPos.onActivate((id, attr, value) => join3.rightActivate(id, attr, value));
|
||||
alphaPos.onDeactivate((id, attr, value) => join3.rightDeactivate(id, attr, value));
|
||||
|
||||
// ProductionNode
|
||||
const prod = new ProductionNode("threeCondRule");
|
||||
join3.addDownstreamActivate(prod.leftActivate.bind(prod));
|
||||
join3.addDownstreamDeactivate(prod.leftDeactivate.bind(prod));
|
||||
|
||||
return { session, prod };
|
||||
}
|
||||
|
||||
describe("G8 — variable binding propagation across 3-condition chain", () => {
|
||||
it("entity with all 3 facts produces 1 match with all bindings correct", () => {
|
||||
const { session, prod } = buildThreeConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
session.insert(e1, "Type", "knight");
|
||||
session.insert(e1, "Health", 100);
|
||||
session.insert(e1, "Position", "e4");
|
||||
|
||||
expect(prod.matches).toHaveLength(1);
|
||||
const b = prod.matches[0]?.bindings;
|
||||
expect(b?.["type"]).toBe("knight");
|
||||
expect(b?.["hp"]).toBe(100);
|
||||
expect(b?.["pos"]).toBe("e4");
|
||||
expect(b?.["eid"]).toBe(e1);
|
||||
});
|
||||
|
||||
it("entity missing one fact (Position) does not produce a match", () => {
|
||||
const { session, prod } = buildThreeConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
session.insert(e1, "Type", "pawn");
|
||||
session.insert(e1, "Health", 50);
|
||||
// no Position
|
||||
|
||||
expect(prod.matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("2 complete entities → 2 matches; 1 partial → still 2 matches", () => {
|
||||
const { session, prod } = buildThreeConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
const e2 = session.nextId();
|
||||
const e3 = session.nextId();
|
||||
|
||||
session.insert(e1, "Type", "knight");
|
||||
session.insert(e1, "Health", 100);
|
||||
session.insert(e1, "Position", "e4");
|
||||
|
||||
session.insert(e2, "Type", "rook");
|
||||
session.insert(e2, "Health", 80);
|
||||
session.insert(e2, "Position", "a1");
|
||||
|
||||
session.insert(e3, "Type", "pawn");
|
||||
// e3 missing Health and Position
|
||||
|
||||
expect(prod.matches).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("idEquality prevents cross-entity joins (different entities cannot merge bindings)", () => {
|
||||
const { session, prod } = buildThreeConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
const e2 = session.nextId();
|
||||
|
||||
session.insert(e1, "Type", "knight");
|
||||
session.insert(e1, "Health", 100);
|
||||
// e1 has no Position
|
||||
|
||||
session.insert(e2, "Type", "rook");
|
||||
session.insert(e2, "Position", "a1");
|
||||
// e2 has no Health
|
||||
|
||||
// Neither entity has all 3 facts, so no complete matches
|
||||
expect(prod.matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("bindings from all 3 conditions available in the final token", () => {
|
||||
const { session, prod } = buildThreeConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
session.insert(e1, "Type", "bishop");
|
||||
session.insert(e1, "Health", 75);
|
||||
session.insert(e1, "Position", "c3");
|
||||
|
||||
const token = prod.matches[0]?.token;
|
||||
expect(token).toBeDefined();
|
||||
// Walk the token chain: 3 facts deep
|
||||
let depth = 0;
|
||||
let curr = token ?? null;
|
||||
while (curr !== null) {
|
||||
depth++;
|
||||
curr = curr.parent;
|
||||
}
|
||||
// Root token (depth=1) + 3 condition facts = depth 4
|
||||
expect(depth).toBe(4);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue