diff --git a/packages/server/src/protocol.test.ts b/packages/server/src/protocol.test.ts index 695f1d6..e372372 100644 --- a/packages/server/src/protocol.test.ts +++ b/packages/server/src/protocol.test.ts @@ -681,12 +681,31 @@ describe("ModifierProfileSchema", () => { expect(r.success).toBe(false); }); - it("rejects a profile with unknown modifier kind", () => { + it("accepts a profile with arbitrary modifier kind (T3 widening for custom ids)", () => { + // Pre-T3 the wire enum rejected unknown kinds. The widening was + // forced by user-authored CustomModifierIds (arbitrary strings like + // "custom:shield"); kind validity is now enforced by the engine's + // registry-dispatch fallback at apply time, not at the wire. const r = ModifierProfileSchema.safeParse({ ...validProfile, perType: [ { - kind: "teleport", + kind: "custom:teleport", + pieceType: "pawn", + color: "white", + value: 1, + }, + ], + }); + expect(r.success).toBe(true); + }); + + it("rejects a profile with empty-string modifier kind", () => { + const r = ModifierProfileSchema.safeParse({ + ...validProfile, + perType: [ + { + kind: "", pieceType: "pawn", color: "white", value: 1, diff --git a/packages/server/src/protocol.ts b/packages/server/src/protocol.ts index d9b97f6..9f431d0 100644 --- a/packages/server/src/protocol.ts +++ b/packages/server/src/protocol.ts @@ -195,14 +195,32 @@ export type ResolvedLayout = z.infer; // assertion below. // --------------------------------------------------------------------------- -const ModifierKindIdSchema = z.enum([ +/** + * Built-in modifier ids — documented here for reference; the actual + * parse schema below accepts ANY non-empty string. + * + * T3 widens `kind` away from a literal enum because user-authored + * custom modifier descriptors use arbitrary ids (e.g. "custom:shield"). + * Rejecting unknown kinds server-side would drop every profile that + * references a custom descriptor, breaking multiplayer end-to-end. + * + * Validity of the kind is enforced ELSEWHERE: the chess engine's + * apply pipeline looks the id up in MODIFIER_REGISTRY (built-ins) + * then engine.customModifiers (user-authored). Unknown kinds are + * warned and skipped — forwards-compat with a new built-in by + * design. + */ +const BUILTIN_MODIFIER_KINDS = [ "hp-bonus", "range-bonus", "direction-additions", "capture-flags", "promotion-override", "damage-resistance", -]); +] as const; +void BUILTIN_MODIFIER_KINDS; // documentation anchor; server parse accepts any string + +const ModifierKindIdSchema = z.string().min(1); const ModifierColorExtSchema = z.enum(["white", "black", "both"]);