From 161bc0e78ac91f1caad734fc7d34cb12b092a099 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 21 Apr 2026 16:58:17 -0600 Subject: [PATCH] test(server): add wire-parity fixtures for 7 new trigger primitive kinds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server's EffectPrimitiveNodeWireSchema already uses kind: z.string().min(1) by design (see comment block in protocol.ts:525-538), so new primitive kinds do NOT require server schema changes. This commit adds parity-test fixtures proving that holds for each of the incoming trigger kinds. Adds 10 positive fixtures exercising on-move, on-turn-end, on-promotion, on-check-received, on-check-delivered, both filter variants of on-moved-onto-square (squares list + file/rank predicate), both target variants of on-captured ({relation:'ally'} + 'attacker'), and a nested on-capture→on-move composition. Plus one 51-primitive negative fixture using new kinds, confirming the .max(50) cap applies uniformly. Squares throughout use the numeric 0..63 convention (e4 = 28, d5 = 35), matching the engine's Square type. Documented a spec vs implementation divergence: wire schemas accept depth-4 because params is z.unknown() — depth enforcement lives in the client-side validator (validate.ts MAX_RECURSION_DEPTH = 3). A positive depth-4 fixture now pins this contract explicitly. --- .../src/custom-modifier-wire-parity.test.ts | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) diff --git a/packages/server/src/custom-modifier-wire-parity.test.ts b/packages/server/src/custom-modifier-wire-parity.test.ts index 4a94f89..e4a5be5 100644 --- a/packages/server/src/custom-modifier-wire-parity.test.ts +++ b/packages/server/src/custom-modifier-wire-parity.test.ts @@ -70,6 +70,171 @@ describe("wire-shape parity: client v4 ↔ server v3 (Q4.2)", () => { createdAt: 1700000000000, }; + // --------------------------------------------------------------------------- + // T13 — Fixtures exercising the 7 new trigger primitive kinds. + // + // Both schemas declare `kind: z.string().min(1)` + `params: z.unknown()` + // (protocol.ts:540-548, chess custom/schema.ts:34-39) — deliberately + // structural so new primitive kinds shipped from the client don't + // require mirroring the catalog on the server (see protocol.ts:525-538 + // comment). These fixtures pin that contract: the wire layer stays + // permissive of all 7 new kinds while preserving agreement with + // the client-side schema. + // + // Squares are numeric 0..63 — e4 = 28, d5 = 35, a1 = 0, h8 = 63. + // --------------------------------------------------------------------------- + + const addHpBonusChild = { + kind: "add-to-attribute", + params: { attr: "HpBonus", delta: 1 }, + }; + + const validWithOnMove = { + ...validMinimal, + id: "custom:parity-on-move", + name: "Parity OnMove", + primitives: [ + { + kind: "on-move", + params: { primitives: [addHpBonusChild] }, + }, + ], + }; + + const validWithOnTurnEnd = { + ...validMinimal, + id: "custom:parity-on-turn-end", + name: "Parity OnTurnEnd", + primitives: [ + { + kind: "on-turn-end", + params: { primitives: [addHpBonusChild] }, + }, + ], + }; + + const validWithOnPromotion = { + ...validMinimal, + id: "custom:parity-on-promotion", + name: "Parity OnPromotion", + primitives: [ + { + kind: "on-promotion", + params: { primitives: [addHpBonusChild] }, + }, + ], + }; + + const validWithOnCheckReceived = { + ...validMinimal, + id: "custom:parity-on-check-received", + name: "Parity OnCheckReceived", + primitives: [ + { + kind: "on-check-received", + params: { primitives: [addHpBonusChild] }, + }, + ], + }; + + const validWithOnCheckDelivered = { + ...validMinimal, + id: "custom:parity-on-check-delivered", + name: "Parity OnCheckDelivered", + primitives: [ + { + kind: "on-check-delivered", + params: { primitives: [addHpBonusChild] }, + }, + ], + }; + + // on-moved-onto-square: discriminated filter (decisions.md §Trigger) + // Variant A — kind:'squares'. e4 = 28, d5 = 35. + const validWithOnMovedOntoSquareSquares = { + ...validMinimal, + id: "custom:parity-on-moved-onto-square-squares", + name: "Parity OnMovedOntoSquare/Squares", + primitives: [ + { + kind: "on-moved-onto-square", + params: { + filter: { kind: "squares", squares: [28, 35] }, + primitives: [addHpBonusChild], + }, + }, + ], + }; + + // Variant B — kind:'predicate' with file narrowing (file:3 = d-file). + const validWithOnMovedOntoSquarePredicate = { + ...validMinimal, + id: "custom:parity-on-moved-onto-square-predicate", + name: "Parity OnMovedOntoSquare/Predicate", + primitives: [ + { + kind: "on-moved-onto-square", + params: { + filter: { kind: "predicate", file: 3 }, + primitives: [addHpBonusChild], + }, + }, + ], + }; + + // on-captured: target is a TargetResolver (union). Schemas see it as + // `unknown` inside params, so both shapes — string literal and object + // — must pass structurally. + const validWithOnCapturedAllyRelation = { + ...validMinimal, + id: "custom:parity-on-captured-ally", + name: "Parity OnCaptured/Ally", + primitives: [ + { + kind: "on-captured", + params: { + target: { relation: "ally" }, + primitives: [addHpBonusChild], + }, + }, + ], + }; + + const validWithOnCapturedAttacker = { + ...validMinimal, + id: "custom:parity-on-captured-attacker", + name: "Parity OnCaptured/Attacker", + primitives: [ + { + kind: "on-captured", + params: { + target: "attacker", + primitives: [addHpBonusChild], + }, + }, + ], + }; + + // Depth-2 combo: legacy on-capture wrapping a new on-move trigger. + const validNestedNewTriggers = { + ...validMinimal, + id: "custom:parity-nested-new-triggers", + name: "Parity Nested New Triggers", + primitives: [ + { + kind: "on-capture", + params: { + primitives: [ + { + kind: "on-move", + params: { primitives: [addHpBonusChild] }, + }, + ], + }, + }, + ], + }; + describe("both schemas accept", () => { it("minimal valid descriptor", () => { const { client, server } = checkAgreement(validMinimal); @@ -96,6 +261,85 @@ describe("wire-shape parity: client v4 ↔ server v3 (Q4.2)", () => { expect(client).toBe(true); expect(server).toBe(true); }); + + // T13 — 7 new trigger primitive kinds. Each fixture carries one + // new trigger at the top level; structural `kind: z.string().min(1)` + // contract means both schemas must accept. + const newTriggerFixtures: Array<[string, unknown]> = [ + ["on-move trigger", validWithOnMove], + ["on-turn-end trigger", validWithOnTurnEnd], + ["on-promotion trigger", validWithOnPromotion], + ["on-check-received trigger", validWithOnCheckReceived], + ["on-check-delivered trigger", validWithOnCheckDelivered], + [ + "on-moved-onto-square trigger with {kind:'squares'} filter", + validWithOnMovedOntoSquareSquares, + ], + [ + "on-moved-onto-square trigger with {kind:'predicate'} filter", + validWithOnMovedOntoSquarePredicate, + ], + [ + "on-captured trigger with target: {relation:'ally'}", + validWithOnCapturedAllyRelation, + ], + [ + "on-captured trigger with target: 'attacker'", + validWithOnCapturedAttacker, + ], + [ + "new trigger nested inside existing trigger (on-capture > on-move)", + validNestedNewTriggers, + ], + ]; + + for (const [label, desc] of newTriggerFixtures) { + it(label, () => { + const { client, server } = checkAgreement(desc); + expect(client).toBe(true); + expect(server).toBe(true); + }); + } + + // T13 — Document that the wire schemas do NOT enforce nesting depth. + // `params: z.unknown()` means depth-3-plus trees pass structurally; + // the depth-3 cap is the client's `validate.ts` semantic concern + // (MAX_RECURSION_DEPTH = 3), enforced BEFORE send. Server intent + // is explicit: protocol.ts:525-538 says "Deep nesting is the + // client validator's responsibility — the server sees flat arrays + // because nested children are inside `params`." This test pins + // that contract so a future tightening of the wire schema + // (e.g. recursive `z.lazy()` validation of params) is flagged. + it("depth-4 nesting passes both schemas (schemas are structural only)", () => { + const depth4 = { + ...validMinimal, + id: "custom:parity-depth4", + name: "Parity Depth4", + primitives: [ + { + kind: "on-capture", + params: { + primitives: [ + { + kind: "on-move", + params: { + primitives: [ + { + kind: "on-turn-start", + params: { primitives: [addHpBonusChild] }, + }, + ], + }, + }, + ], + }, + }, + ], + }; + const { client, server } = checkAgreement(depth4); + expect(client).toBe(true); + expect(server).toBe(true); + }); }); describe("both schemas reject", () => { @@ -137,6 +381,24 @@ describe("wire-shape parity: client v4 ↔ server v3 (Q4.2)", () => { })), }, ], + // T13 — same length cap applies uniformly to the 7 new trigger + // kinds; the wire schema distinguishes nothing about the kind + // field beyond min(1). Included to guard against any future + // per-kind carve-out regressing the flat .max(50) contract. + [ + "51 on-move triggers (new-kind length cap still enforced)", + { + ...validMinimal, + primitives: Array.from({ length: 51 }, () => ({ + kind: "on-move", + params: { + primitives: [ + { kind: "add-to-attribute", params: { attr: "HpBonus", delta: 1 } }, + ], + }, + })), + }, + ], ]; for (const [label, desc] of rejectionCases) {