143 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|