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
139
packages/rete/tests/golden/g2-two-condition-join.test.ts
Normal file
139
packages/rete/tests/golden/g2-two-condition-join.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* G2 — Two-condition join (same entity)
|
||||
*
|
||||
* Pararules origin: tests/test1.nim "duplicate facts" / "removing facts",
|
||||
* tests/test2.nim "queries" rule getCharacter:
|
||||
* rule getCharacter(Fact):
|
||||
* what:
|
||||
* (id, X, x)
|
||||
* (id, Y, y)
|
||||
*
|
||||
* Demonstrates idEquality join: a variable bound as the entity id in
|
||||
* condition 1 constrains which facts match in condition 2 (same entity).
|
||||
*
|
||||
* Network wired manually:
|
||||
* root → join1(alphaX) → betaMem1 → join2(alphaY, idEquality:"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 };
|
||||
|
||||
function buildTwoConditionNetwork() {
|
||||
const session = new Session({ autoFire: false });
|
||||
const alpha = session._getAlpha();
|
||||
|
||||
// Root memory with dummy token
|
||||
const rootMem = new BetaMemory();
|
||||
rootMem.leftActivate(new Token(null, ROOT_FACT, {}));
|
||||
|
||||
// Alpha nodes for X and Y
|
||||
const alphaX = alpha.buildNode({ id: null, attr: "X" });
|
||||
const alphaY = alpha.buildNode({ id: null, attr: "Y" });
|
||||
|
||||
// Intermediate beta memory between join1 and join2
|
||||
const betaMem1 = new BetaMemory();
|
||||
|
||||
// join1: root × alphaX — binds x and eid
|
||||
const join1 = new JoinNode(rootMem, alphaX.memory, [], "x", "eid");
|
||||
|
||||
// join2: betaMem1 × alphaY — idEquality on "eid", binds y
|
||||
const idEqTest: JoinTest = { type: "idEquality", leftVar: "eid" };
|
||||
const join2 = new JoinNode(betaMem1, alphaY.memory, [idEqTest], "y", null);
|
||||
|
||||
// Wire join1 → betaMem1
|
||||
join1.addDownstreamActivate(betaMem1.leftActivate.bind(betaMem1));
|
||||
join1.addDownstreamDeactivate(betaMem1.leftDeactivate.bind(betaMem1));
|
||||
|
||||
// Wire betaMem1 → join2 (propagate new partial matches)
|
||||
betaMem1.addDownstreamActivate(join2.leftActivate.bind(join2));
|
||||
betaMem1.addDownstreamDeactivate(join2.leftDeactivate.bind(join2));
|
||||
|
||||
// Wire alpha events → join right sides
|
||||
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));
|
||||
|
||||
// ProductionNode
|
||||
const prod = new ProductionNode("xyRule");
|
||||
join2.addDownstreamActivate(prod.leftActivate.bind(prod));
|
||||
join2.addDownstreamDeactivate(prod.leftDeactivate.bind(prod));
|
||||
|
||||
return { session, prod };
|
||||
}
|
||||
|
||||
describe("G2 — two-condition join (same entity)", () => {
|
||||
it("entity with both X and Y produces exactly 1 match", () => {
|
||||
const { session, prod } = buildTwoConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
session.insert(e1, "X", 3);
|
||||
session.insert(e1, "Y", 4);
|
||||
|
||||
expect(prod.matches).toHaveLength(1);
|
||||
expect(prod.matches[0]?.bindings["x"]).toBe(3);
|
||||
expect(prod.matches[0]?.bindings["y"]).toBe(4);
|
||||
expect(prod.matches[0]?.bindings["eid"]).toBe(e1);
|
||||
});
|
||||
|
||||
it("entity with only X does not match (missing Y)", () => {
|
||||
const { session, prod } = buildTwoConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
session.insert(e1, "X", 10);
|
||||
// no Y fact for e1
|
||||
|
||||
expect(prod.matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("two entities: only the complete one matches", () => {
|
||||
const { session, prod } = buildTwoConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
const e2 = session.nextId();
|
||||
|
||||
// e1 has both X and Y
|
||||
session.insert(e1, "X", 1);
|
||||
session.insert(e1, "Y", 2);
|
||||
|
||||
// e2 has only X
|
||||
session.insert(e2, "X", 99);
|
||||
|
||||
expect(prod.matches).toHaveLength(1);
|
||||
expect(prod.matches[0]?.bindings["eid"]).toBe(e1);
|
||||
});
|
||||
|
||||
it("two complete entities produce 2 matches", () => {
|
||||
const { session, prod } = buildTwoConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
const e2 = session.nextId();
|
||||
session.insert(e1, "X", 1);
|
||||
session.insert(e1, "Y", 2);
|
||||
session.insert(e2, "X", 10);
|
||||
session.insert(e2, "Y", 20);
|
||||
|
||||
expect(prod.matches).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("order of insert (X then Y, or Y then X) does not matter", () => {
|
||||
const { session, prod } = buildTwoConditionNetwork();
|
||||
|
||||
const e1 = session.nextId();
|
||||
// Insert Y before X
|
||||
session.insert(e1, "Y", 99);
|
||||
session.insert(e1, "X", 77);
|
||||
|
||||
expect(prod.matches).toHaveLength(1);
|
||||
expect(prod.matches[0]?.bindings["x"]).toBe(77);
|
||||
expect(prod.matches[0]?.bindings["y"]).toBe(99);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue