houserules/packages/rete/tests/golden/g7-serialization.test.ts
Joey Yakimowich-Payne 0401295bbc
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.
2026-04-16 14:16:19 -06:00

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);
});
});