houserules/packages/rete/tests/golden/g2-two-condition-join.test.ts
Joey Yakimowich-Payne 0401295bbc
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.
2026-04-16 14:16:19 -06:00

139 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
});
});