/** * 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; expect(reparsed["name"]).toBe("plain"); expect(reparsed["salience"]).toBe(1); expect(Array.isArray(reparsed["conditions"])).toBe(true); }); });