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.
This commit is contained in:
parent
0259268e34
commit
0401295bbc
12 changed files with 1510 additions and 2 deletions
121
packages/rete/tests/golden/g1-single-condition.test.ts
Normal file
121
packages/rete/tests/golden/g1-single-condition.test.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue