test(server): add wire-parity fixtures for 7 new trigger primitive kinds
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.
This commit is contained in:
parent
776c192874
commit
161bc0e78a
1 changed files with 262 additions and 0 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue