/** * G9 — Retraction removes a match (non-derived) * * Pararules origin: tests/test1.nim "removing facts": * session.insert(Bob, Color, "blue") * session.insert(Yair, LeftOf, Zach) * session.insert(Alice, Color, "maize") * session.insert(Yair, RightOf, Bob) * check session.queryAll(rule1).len == 1 * * session.retract(Yair, RightOf, Bob) * check session.queryAll(rule1).len == 0 * * session.retract(Bob, Color) ← value parameter not required * check session.queryAll(rule1).len == 0 * * We test: * 1. Insert fact → match appears * 2. Retract fact → match disappears * 3. Re-insert → match reappears * 4. Two-condition join: retracting one condition removes joint match * 5. Session.retract only needs (id, attr) — value is not required */ 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 buildSingleCondNet(attr: string) { const session = new Session({ autoFire: false }); const alpha = session._getAlpha(); const rootMem = new BetaMemory(); rootMem.leftActivate(new Token(null, ROOT_FACT, {})); const alphaNode = alpha.buildNode({ id: null, attr }); const join = new JoinNode(rootMem, alphaNode.memory, [], "val", "eid"); alphaNode.onActivate((id, a, value) => join.rightActivate(id, a, value)); alphaNode.onDeactivate((id, a, value) => join.rightDeactivate(id, a, value)); const prod = new ProductionNode("rule"); join.addDownstreamActivate(prod.leftActivate.bind(prod)); join.addDownstreamDeactivate(prod.leftDeactivate.bind(prod)); return { session, prod }; } function buildTwoCondNet() { const session = new Session({ autoFire: false }); const alpha = session._getAlpha(); const rootMem = new BetaMemory(); rootMem.leftActivate(new Token(null, ROOT_FACT, {})); const alphaX = alpha.buildNode({ id: null, attr: "X" }); const alphaY = alpha.buildNode({ id: null, attr: "Y" }); const mem1 = new BetaMemory(); const idEq: JoinTest = { type: "idEquality", leftVar: "eid" }; const join1 = new JoinNode(rootMem, alphaX.memory, [], "x", "eid"); const join2 = new JoinNode(mem1, alphaY.memory, [idEq], "y", null); join1.addDownstreamActivate(mem1.leftActivate.bind(mem1)); join1.addDownstreamDeactivate(mem1.leftDeactivate.bind(mem1)); mem1.addDownstreamActivate(join2.leftActivate.bind(join2)); mem1.addDownstreamDeactivate(join2.leftDeactivate.bind(join2)); 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)); const prod = new ProductionNode("xyRule"); join2.addDownstreamActivate(prod.leftActivate.bind(prod)); join2.addDownstreamDeactivate(prod.leftDeactivate.bind(prod)); return { session, prod }; } describe("G9 — retraction removes match", () => { it("insert → match appears, retract → match disappears", () => { const { session, prod } = buildSingleCondNet("Health"); const e1 = session.nextId(); session.insert(e1, "Health", 100); expect(prod.matches).toHaveLength(1); session.retract(e1, "Health"); expect(prod.matches).toHaveLength(0); }); it("retract then re-insert → match reappears", () => { const { session, prod } = buildSingleCondNet("Health"); const e1 = session.nextId(); session.insert(e1, "Health", 100); expect(prod.matches).toHaveLength(1); session.retract(e1, "Health"); expect(prod.matches).toHaveLength(0); session.insert(e1, "Health", 50); expect(prod.matches).toHaveLength(1); expect(prod.matches[0]?.bindings["val"]).toBe(50); }); it("two-condition join: retracting first condition removes joint match", () => { const { session, prod } = buildTwoCondNet(); const e1 = session.nextId(); session.insert(e1, "X", 3); session.insert(e1, "Y", 4); expect(prod.matches).toHaveLength(1); session.retract(e1, "X"); expect(prod.matches).toHaveLength(0); }); it("two-condition join: retracting second condition removes joint match", () => { const { session, prod } = buildTwoCondNet(); const e1 = session.nextId(); session.insert(e1, "X", 1); session.insert(e1, "Y", 2); expect(prod.matches).toHaveLength(1); session.retract(e1, "Y"); expect(prod.matches).toHaveLength(0); }); it("retract one entity of many — only that entity's match disappears", () => { const { session, prod } = buildSingleCondNet("Health"); const e1 = session.nextId(); const e2 = session.nextId(); const e3 = session.nextId(); session.insert(e1, "Health", 10); session.insert(e2, "Health", 20); session.insert(e3, "Health", 30); expect(prod.matches).toHaveLength(3); session.retract(e2, "Health"); expect(prod.matches).toHaveLength(2); // e1 and e3 still present const remainingEids = prod.matches.map((m) => m.bindings["eid"]); expect(remainingEids).toContain(e1); expect(remainingEids).toContain(e3); expect(remainingEids).not.toContain(e2); }); it("update (re-insert same attr) replaces old value in match", () => { const { session, prod } = buildSingleCondNet("Health"); const e1 = session.nextId(); session.insert(e1, "Health", 100); expect(prod.matches[0]?.bindings["val"]).toBe(100); session.insert(e1, "Health", 50); // update expect(prod.matches).toHaveLength(1); expect(prod.matches[0]?.bindings["val"]).toBe(50); }); it("WM.contains returns false after retraction", () => { const { session } = buildSingleCondNet("Health"); const e1 = session.nextId(); session.insert(e1, "Health", 100); expect(session.contains(e1, "Health")).toBe(true); session.retract(e1, "Health"); expect(session.contains(e1, "Health")).toBe(false); }); });