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,
|
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", () => {
|
describe("both schemas accept", () => {
|
||||||
it("minimal valid descriptor", () => {
|
it("minimal valid descriptor", () => {
|
||||||
const { client, server } = checkAgreement(validMinimal);
|
const { client, server } = checkAgreement(validMinimal);
|
||||||
|
|
@ -96,6 +261,85 @@ describe("wire-shape parity: client v4 ↔ server v3 (Q4.2)", () => {
|
||||||
expect(client).toBe(true);
|
expect(client).toBe(true);
|
||||||
expect(server).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", () => {
|
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) {
|
for (const [label, desc] of rejectionCases) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue