houserules/packages/rete/src/serialize.test.ts

143 lines
5.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { serialize, deserialize, RULE_SCHEMA_V1 } from "./serialize.js";
import { HandlerRegistry } from "./registry.js";
import { v } from "./builder.js";
import type { EntityId } from "./schema.js";
const mkId = (n: number) => n as EntityId;
function makeRegistry(...names: string[]): HandlerRegistry {
const r = new HandlerRegistry();
for (const name of names) r.register(name, () => {});
return r;
}
describe("serialize()", () => {
it("serializes a simple rule to JSON-compatible object", () => {
const rule = {
name: "move-player",
salience: 0,
conditions: [{ id: mkId(1), attr: "X", binding: v("x"), idBinding: undefined }],
handler: "moveHandler",
handlerArgs: undefined,
};
const json = serialize(rule);
expect(json.name).toBe("move-player");
expect(json.salience).toBe(0);
expect(json.handler).toBe("moveHandler");
expect(json.conditions).toHaveLength(1);
expect(JSON.stringify(json)).not.toContain("function"); // no function refs
});
it("serializes wildcard id as null", () => {
const rule = {
name: "wildcard",
salience: 5,
conditions: [{ id: null, attr: "Health", binding: v("hp"), idBinding: v("eid") }],
handler: "h",
handlerArgs: ["arg1"],
};
const json = serialize(rule);
expect(json.conditions[0]?.id).toBeNull();
expect(json.conditions[0]?.idBinding).toBe("eid");
expect(json.handlerArgs).toEqual(["arg1"]);
expect(json.salience).toBe(5);
});
it("serializes rule with no binding (binding: null)", () => {
const rule = {
name: "no-bind",
salience: 0,
conditions: [{ id: mkId(42), attr: "Flag", binding: null, idBinding: undefined }],
handler: "flagHandler",
};
const json = serialize(rule);
expect(json.conditions[0]?.binding).toBeNull();
});
});
describe("deserialize()", () => {
it("deserializes a serialized rule back to RuleDefinition", () => {
const registry = makeRegistry("moveHandler");
const rule = {
name: "round-trip-1",
salience: 0,
conditions: [{ id: mkId(1), attr: "X", binding: v("x") }],
handler: "moveHandler",
};
const json = serialize(rule);
const restored = deserialize(json, registry);
expect(restored.name).toBe("round-trip-1");
expect(restored.handler).toBe("moveHandler");
expect(restored.conditions).toHaveLength(1);
expect(restored.conditions[0]?.binding?.name).toBe("x");
});
it("throws UnknownHandlerError for unregistered handler", () => {
const registry = new HandlerRegistry();
const json = {
name: "bad",
salience: 0,
conditions: [],
handler: "missingHandler",
};
expect(() => deserialize(json, registry)).toThrow("UnknownHandlerError");
});
it("throws on malformed JSON (missing name)", () => {
const registry = makeRegistry("h");
const bad = { salience: 0, conditions: [], handler: "h" };
expect(() => deserialize(bad as never, registry)).toThrow();
});
});
describe("round-trip equivalence (10 shapes)", () => {
const registry = makeRegistry("h1", "h2", "h3");
const shapes = [
// shape 1: minimal
{ name: "s1", salience: 0, conditions: [], handler: "h1" },
// shape 2: single condition, exact id
{ name: "s2", salience: 0, conditions: [{ id: mkId(1), attr: "A", binding: v("a") }], handler: "h1" },
// shape 3: wildcard id
{ name: "s3", salience: 0, conditions: [{ id: null, attr: "B", binding: v("b") }], handler: "h1" },
// shape 4: multiple conditions
{ name: "s4", salience: 0, conditions: [{ id: mkId(1), attr: "X", binding: v("x") }, { id: mkId(1), attr: "Y", binding: v("y") }], handler: "h2" },
// shape 5: with salience
{ name: "s5", salience: 10, conditions: [{ id: null, attr: "C", binding: null }], handler: "h2" },
// shape 6: with idBinding
{ name: "s6", salience: 0, conditions: [{ id: null, attr: "D", binding: v("d"), idBinding: v("eid") }], handler: "h3" },
// shape 7: with handlerArgs
{ name: "s7", salience: 0, conditions: [], handler: "h1", handlerArgs: [1, "two", true] },
// shape 8: null binding
{ name: "s8", salience: 0, conditions: [{ id: mkId(5), attr: "Flag", binding: null }], handler: "h2" },
// shape 9: high salience + multiple conditions
{ name: "s9", salience: 100, conditions: [{ id: mkId(1), attr: "A", binding: v("a") }, { id: null, attr: "B", binding: v("b") }], handler: "h3" },
// shape 10: empty handlerArgs
{ name: "s10", salience: -5, conditions: [], handler: "h3", handlerArgs: [] },
] as const;
for (const shape of shapes) {
it(`round-trip shape: ${shape.name}`, () => {
const json = serialize(shape);
const restored = deserialize(json, registry);
// Re-serialize restored and compare JSON strings
const json2 = serialize(restored);
expect(JSON.stringify(json2)).toBe(JSON.stringify(json));
});
}
});
describe("RULE_SCHEMA_V1", () => {
it("validates a valid rule JSON", () => {
const valid = { name: "x", salience: 0, conditions: [], handler: "h" };
const result = RULE_SCHEMA_V1.safeParse(valid);
expect(result.success).toBe(true);
});
it("rejects a rule with missing name", () => {
const bad = { salience: 0, conditions: [], handler: "h" };
const result = RULE_SCHEMA_V1.safeParse(bad);
expect(result.success).toBe(false);
});
});