/** * G1 — Single-condition match * * Pararules origin: tests/test1.nim "removing facts" (single-condition pattern) * and tests/test2.nim "queries" rule getPerson with one what-condition. * * Nim equivalent: * rule getHealth(Fact): * what: * (id, Health, hp) * session.insert(player1, Health, 42) * check session.query(rules.getHealth).hp == 42 * * We manually wire: * root BetaMemory → JoinNode(alphaHealth) → ProductionNode * * The Session's WM→AlphaNetwork is already wired internally; we attach * additional listeners to route alpha activations into our JoinNode. */ 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, query, queryAll, NoMatchError } from "../../src/query.js"; import type { EntityId } from "../../src/schema.js"; import type { TokenFact } from "../../src/beta.js"; const ROOT_FACT: TokenFact = { id: 0 as EntityId, attr: "__root", value: null }; function buildSingleConditionNetwork(attrKey: string) { const session = new Session({ autoFire: false }); const alpha = session._getAlpha(); // Root BetaMemory seeded with a dummy token (seeds the first join) const rootMem = new BetaMemory(); const rootToken = new Token(null, ROOT_FACT, {}); rootMem.leftActivate(rootToken); // Alpha node for our attribute (wildcard entity id) const alphaNode = alpha.buildNode({ id: null, attr: attrKey }); // JoinNode: binds value → "val", entity id → "eid" const join = new JoinNode(rootMem, alphaNode.memory, [], "val", "eid"); // Wire alpha → join (right side) alphaNode.onActivate((id, attr, value) => join.rightActivate(id, attr, value)); alphaNode.onDeactivate((id, attr, value) => join.rightDeactivate(id, attr, value)); // ProductionNode collects full matches const prod = new ProductionNode("singleCondRule"); join.addDownstreamActivate(prod.leftActivate.bind(prod)); join.addDownstreamDeactivate(prod.leftDeactivate.bind(prod)); return { session, prod }; } describe("G1 — single-condition match", () => { it("inserts one fact → production has exactly 1 match with correct binding", () => { const { session, prod } = buildSingleConditionNetwork("Health"); const e1 = session.nextId(); session.insert(e1, "Health", 42); expect(prod.matches).toHaveLength(1); expect(prod.matches[0]?.bindings["val"]).toBe(42); expect(prod.matches[0]?.bindings["eid"]).toBe(e1); }); it("query() returns the single match bindings", () => { const { session, prod } = buildSingleConditionNetwork("Health"); const e1 = session.nextId(); session.insert(e1, "Health", 100); const bindings = query(prod); expect(bindings["val"]).toBe(100); }); it("queryAll() returns all matches", () => { const { session, prod } = buildSingleConditionNetwork("Health"); const e1 = session.nextId(); const e2 = session.nextId(); session.insert(e1, "Health", 10); session.insert(e2, "Health", 20); const all = queryAll(prod); expect(all).toHaveLength(2); }); it("query() throws NoMatchError when no facts present", () => { const { prod } = buildSingleConditionNetwork("Health"); expect(() => query(prod)).toThrow(NoMatchError); }); it("two attributes produce independent matches (no cross-join)", () => { const { session, prod: prodHealth } = buildSingleConditionNetwork("Health"); const { prod: prodPos } = (() => { const alpha = session._getAlpha(); const rootMem = new BetaMemory(); const rootToken = new Token(null, ROOT_FACT, {}); rootMem.leftActivate(rootToken); const alphaNode = alpha.buildNode({ id: null, attr: "Position" }); const join = new JoinNode(rootMem, alphaNode.memory, [], "val", "eid"); alphaNode.onActivate((id, attr, value) => join.rightActivate(id, attr, value)); alphaNode.onDeactivate((id, attr, value) => join.rightDeactivate(id, attr, value)); const prod = new ProductionNode("posRule"); join.addDownstreamActivate(prod.leftActivate.bind(prod)); join.addDownstreamDeactivate(prod.leftDeactivate.bind(prod)); return { prod }; })(); const e1 = session.nextId(); session.insert(e1, "Health", 50); session.insert(e1, "Position", "e4"); expect(prodHealth.matches).toHaveLength(1); expect(prodPos.matches).toHaveLength(1); }); });