/** * G4 — Multiple entities matching a wildcard condition * * Pararules origin: tests/test1.nim "number of conditions != number of facts": * rule numCondsAndFacts(Fact): * what: * (b, Color, "blue") * (y, LeftOf, z) * (a, Color, "maize") * (y, RightOf, b) * (x, Height, h) ← wildcard on x; George/Thomas/Xavier all match * check session.queryAll(rule1).len == 3 * * Our equivalent: a single wildcard condition `(?id, Health, ?hp)` matches * all entities that have a Health fact. Inserting N entities → N matches. * * Network: * root → join(alphaHealth, no tests) → 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, queryAll } 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 buildHealthNetwork() { const session = new Session({ autoFire: false }); const alpha = session._getAlpha(); const rootMem = new BetaMemory(); rootMem.leftActivate(new Token(null, ROOT_FACT, {})); const alphaHealth = alpha.buildNode({ id: null, attr: "Health" }); const join = new JoinNode(rootMem, alphaHealth.memory, [], "hp", "eid"); alphaHealth.onActivate((id, attr, value) => join.rightActivate(id, attr, value)); alphaHealth.onDeactivate((id, attr, value) => join.rightDeactivate(id, attr, value)); const prod = new ProductionNode("healthRule"); join.addDownstreamActivate(prod.leftActivate.bind(prod)); join.addDownstreamDeactivate(prod.leftDeactivate.bind(prod)); return { session, prod }; } describe("G4 — multiple entities matching wildcard condition", () => { it("N entities each with Health fact → N matches", () => { const { session, prod } = buildHealthNetwork(); const count = 3; for (let i = 0; i < count; i++) { const e = session.nextId(); session.insert(e, "Health", (i + 1) * 10); } expect(prod.matches).toHaveLength(count); }); it("5 entities → 5 matches (pararules test1.nim '3 height entities' scaled)", () => { const { session, prod } = buildHealthNetwork(); for (let i = 0; i < 5; i++) { const e = session.nextId(); session.insert(e, "Health", 72); } expect(prod.matches).toHaveLength(5); }); it("queryAll returns one bindings object per entity", () => { const { session, prod } = buildHealthNetwork(); const ids: EntityId[] = []; for (let i = 0; i < 4; i++) { const e = session.nextId(); ids.push(e); session.insert(e, "Health", i * 100); } const all = queryAll(prod); expect(all).toHaveLength(4); // Every matched entity id appears in the results const matchedIds = all.map((b) => b["eid"]); for (const id of ids) { expect(matchedIds).toContain(id); } }); it("entity without Health fact is not counted", () => { const { session, prod } = buildHealthNetwork(); const e1 = session.nextId(); const e2 = session.nextId(); session.insert(e1, "Health", 100); session.insert(e2, "Position", "a1"); // no Health expect(prod.matches).toHaveLength(1); }); it("0 entities → 0 matches", () => { const { prod } = buildHealthNetwork(); expect(prod.matches).toHaveLength(0); }); });