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.
146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
/**
|
|
* G7 — Serialization round-trip of a complete rule
|
|
*
|
|
* Pararules origin: pararules uses staticRuleset (tests/test3.nim) for
|
|
* compile-time rule serialization. Our engine equivalent is the
|
|
* serialize/deserialize JSON round-trip per SPEC §JSON Rule Schema:
|
|
* - Rules are pure data (no closures, no eval)
|
|
* - Handlers are referenced by name and resolved against a HandlerRegistry
|
|
* - serialize() → JSON-safe object → JSON.stringify → JSON.parse → deserialize()
|
|
* produces a structurally identical RuleDefinition
|
|
*
|
|
* This tests the full pipeline including Zod schema validation.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
import { serialize, deserialize, RULE_SCHEMA_V1 } from "../../src/serialize.js";
|
|
import { defineRule, v } from "../../src/builder.js";
|
|
import { HandlerRegistry, UnknownHandlerError } from "../../src/registry.js";
|
|
import type { EntityId } from "../../src/schema.js";
|
|
|
|
function makeRegistry(...names: string[]): HandlerRegistry {
|
|
const reg = new HandlerRegistry();
|
|
for (const name of names) {
|
|
reg.register(name, () => {});
|
|
}
|
|
return reg;
|
|
}
|
|
|
|
describe("G7 — serialization round-trip", () => {
|
|
it("serialize + JSON round-trip + deserialize produces equivalent RuleDefinition", () => {
|
|
const registry = makeRegistry("myHandler");
|
|
|
|
const rule = defineRule({
|
|
name: "myRule",
|
|
salience: 5,
|
|
what: [
|
|
{ id: null, attr: "Health", binding: v("hp"), idBinding: v("eid") },
|
|
{ id: null, attr: "Position", binding: v("pos") },
|
|
],
|
|
handler: "myHandler",
|
|
registry,
|
|
});
|
|
|
|
// Serialize → JSON → deserialize
|
|
const serialized = serialize(rule);
|
|
const json = JSON.stringify(serialized);
|
|
const parsed = JSON.parse(json) as unknown;
|
|
const restored = deserialize(parsed, registry);
|
|
|
|
// Core identity preserved
|
|
expect(restored.name).toBe(rule.name);
|
|
expect(restored.salience).toBe(rule.salience);
|
|
expect(restored.handler).toBe(rule.handler);
|
|
expect(restored.conditions).toHaveLength(rule.conditions.length);
|
|
|
|
// Condition details preserved
|
|
const c0 = restored.conditions[0];
|
|
expect(c0?.id).toBe(null);
|
|
expect(c0?.attr).toBe("Health");
|
|
expect(c0?.binding?.name).toBe("hp");
|
|
expect(c0?.idBinding?.name).toBe("eid");
|
|
|
|
const c1 = restored.conditions[1];
|
|
expect(c1?.attr).toBe("Position");
|
|
expect(c1?.binding?.name).toBe("pos");
|
|
expect(c1?.idBinding).toBeUndefined();
|
|
});
|
|
|
|
it("rule with specific entity id survives round-trip", () => {
|
|
const registry = makeRegistry("handler");
|
|
const specificId = 42 as EntityId;
|
|
|
|
const rule = defineRule({
|
|
name: "specificRule",
|
|
salience: 0,
|
|
what: [{ id: specificId, attr: "Active", binding: v("active") }],
|
|
handler: "handler",
|
|
registry,
|
|
});
|
|
|
|
const serialized = serialize(rule);
|
|
const restored = deserialize(JSON.parse(JSON.stringify(serialized)), registry);
|
|
|
|
expect(restored.conditions[0]?.id).toBe(42);
|
|
});
|
|
|
|
it("rule with handlerArgs survives round-trip", () => {
|
|
const registry = makeRegistry("handler");
|
|
|
|
const rule = defineRule({
|
|
name: "argsRule",
|
|
salience: 0,
|
|
what: [{ id: null, attr: "X", binding: v("x") }],
|
|
handler: "handler",
|
|
handlerArgs: ["threshold", 50, true],
|
|
registry,
|
|
});
|
|
|
|
const serialized = serialize(rule);
|
|
const restored = deserialize(JSON.parse(JSON.stringify(serialized)), registry);
|
|
|
|
expect(restored.handlerArgs).toEqual(["threshold", 50, true]);
|
|
});
|
|
|
|
it("salience defaults to 0 when omitted in serialized form", () => {
|
|
const registry = makeRegistry("h");
|
|
const raw = { name: "noSalience", handler: "h", conditions: [] };
|
|
const restored = deserialize(raw, registry);
|
|
expect(restored.salience).toBe(0);
|
|
});
|
|
|
|
it("deserialize throws UnknownHandlerError for unregistered handler", () => {
|
|
const emptyRegistry = new HandlerRegistry();
|
|
const json = { name: "r", handler: "missing", conditions: [] };
|
|
expect(() => deserialize(json, emptyRegistry)).toThrow(UnknownHandlerError);
|
|
});
|
|
|
|
it("RULE_SCHEMA_V1 validates structure with Zod", () => {
|
|
const valid = { name: "r", handler: "h", conditions: [], salience: 0 };
|
|
const parsed = RULE_SCHEMA_V1.safeParse(valid);
|
|
expect(parsed.success).toBe(true);
|
|
|
|
const invalid = { name: "", handler: "h" }; // empty name fails min(1)
|
|
const badParsed = RULE_SCHEMA_V1.safeParse(invalid);
|
|
expect(badParsed.success).toBe(false);
|
|
});
|
|
|
|
it("serialize produces plain JSON-safe values (no functions, no undefined)", () => {
|
|
const registry = makeRegistry("h");
|
|
const rule = defineRule({
|
|
name: "plain",
|
|
salience: 1,
|
|
what: [{ id: null, attr: "X", binding: v("x") }],
|
|
handler: "h",
|
|
registry,
|
|
});
|
|
|
|
const serialized = serialize(rule);
|
|
// Must round-trip cleanly through JSON without loss
|
|
const jsonStr = JSON.stringify(serialized);
|
|
expect(typeof jsonStr).toBe("string");
|
|
const reparsed = JSON.parse(jsonStr) as Record<string, unknown>;
|
|
expect(reparsed["name"]).toBe("plain");
|
|
expect(reparsed["salience"]).toBe(1);
|
|
expect(Array.isArray(reparsed["conditions"])).toBe(true);
|
|
});
|
|
});
|