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