---
phase: 06.7
slug: network-model-client-trust-fall-trigger
status: draft
nyquist_compliant: true
wave_0_complete: false
created: 2026-05-17
---

# Phase 06.7 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.
> Substance extracted from `06.7-RESEARCH.md` §Validation Architecture (line 747) per
> plan-checker iteration 1 blocker fix.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | `vitest` 4.1.5 (server) / 3.2.4 (client) `[VERIFIED: package.json files]` |
| **Config file** | Implicit `vitest` config in each app + workspace |
| **Quick run command (server)** | `pnpm -C apps/server test` |
| **Quick run command (client)** | `pnpm -C apps/client test` |
| **Full suite command** | `pnpm -r test` (workspace-recursive) |
| **Integration tests (server)** | `pnpm -C apps/server test:integration` |
| **Estimated runtime** | ~45s per-package quick run; ~3min workspace-recursive |

---

## Sampling Rate

- **After every task commit:** `pnpm -C <affected-package> test` (server / client / protocol / game-logic — only the changed one)
- **After every plan wave:** `pnpm -r test` + `pnpm -r typecheck` + `pnpm trace:check`
- **Before `/gsd-verify-work`:** Full suite green + `pnpm -C apps/server test:integration` green + `pnpm -C apps/client test:e2e` green + operator UAT signed off
- **Max feedback latency:** < 60s for quick run; < 5min for wave gate

---

## Per-Task Verification Map (Phase Requirements → Test Map)

| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| REQ-SRV-03 | server `position_update` handler validates with cPositionUpdateSchema (zod strict) and writes to PlayerState | unit | `pnpm -C apps/server test position-update` | Wave 0: NEW `apps/server/test/position-update.test.ts` |
| REQ-SRV-03 | server `position_update` handler is rate-limited and rejects stale seq | unit | `pnpm -C apps/server test position-update-rate-limit` | Wave 0: NEW (covered in same file) |
| REQ-SRV-03 | server `set_sprite_override` handler accepts valid sprite_id, writes to PlayerState.sprite_override | unit | `pnpm -C apps/server test sprite-override` | Wave 0: NEW `apps/server/test/sprite-override.test.ts` |
| REQ-SRV-03 | end-to-end: client sends position_update → server stores → broadcasts → second client sees update | integration | `pnpm -C apps/server test:integration position-update` | Wave 0: NEW `apps/server/test/position-update.integ.test.ts` |
| REQ-SRV-03 | PROTOCOL_VERSION 3 client rejected at handshake (close 4400) | integration | `pnpm -C apps/server test:integration protocol-v4-handshake` | Wave 0: NEW `apps/server/test/protocol-v4-handshake.integ.test.ts` (mirrors `protocol-v2-handshake.integ.test.ts`) |
| REQ-SRV-14 | `step()` still advances platform positions (regression — platforms not broken by carve-out) | unit | `pnpm -C packages/game-logic test step-pass-through` | Wave 0: NEW `packages/game-logic/test/step-pass-through.test.ts` (codex #6 — per-player skip-when-no-input contract; existing step-bno-fidelity / platform-cycle still pass) |
| REQ-CLI-04 | reconciler.onServerSnapshot is a no-op for self-player — sprite.setPosition + sprite.tweenTo never called (D-04 acceptance criterion) | unit | `pnpm -C apps/client test reconciler-self-noop` | Wave 0: NEW assertion added to `apps/client/src/__test__/reconciler.test.ts` |
| REQ-CLI-04 | PositionDispatcher sends position_update at 30 Hz on tick, correctly populated from PredictionEngine + SpriteStateMachine output | unit | `pnpm -C apps/client test position-dispatcher` | Wave 0: NEW `apps/client/src/__test__/position-dispatcher.test.ts` |
| REQ-CLI-04 | predictor continues to drive local sprite position on every tick (regression — local feel preserved) | unit | `pnpm -C apps/client test prediction` | ✅ existing `apps/client/src/__test__/prediction.test.ts` |
| REQ-CLI-04 | anim_state packing — 16 base poses round-trip pack→unpack | unit | `pnpm -C packages/protocol test anim-state` | Wave 0: NEW `packages/protocol/test/anim-state.test.ts` |
| REQ-CLI-04 | **D-06 idle cadence guard** — PositionDispatcher.sendTick emits valid payload when `getLocalState()` returns `{vx:0, vy:0}` (idle player still sends 30 Hz) | unit | `pnpm -C apps/client test position-dispatcher` | Wave 0: NEW assertion in `apps/client/src/__test__/position-dispatcher.test.ts` (added per plan-checker iter 1 warning #3) |
| REQ-SRV-07 | **Codex review #1** — RATES.position_update = {rate:35, burst:60}; 30 Hz stream is NOT drop-rate-limited; > 60/s stream sees drops | unit | `pnpm -C apps/server test rate-limit` | Wave 0: EXTEND existing `apps/server/test/rate-limit.test.ts` with codex #1 cases |
| REQ-SRV-07 | **Codex review #1** — RATES.set_sprite_override = {rate:2, burst:5}; 5-burst-then-1-drop matches chat_send envelope | unit | `pnpm -C apps/server test rate-limit` | Wave 0: same file (codex #1) |
| REQ-SRV-14 | **Codex review #6** — step() per-player skip-when-no-input: players without an input frame are COPIED THROUGH UNCHANGED (vx/vy/x/y/last_input_seq survive empty-input ticks). Mixed-input case (one player has input, one doesn't) proves the contract is per-player, not global | unit | `pnpm -C packages/game-logic test step-pass-through` | Wave 0: NEW `packages/game-logic/test/step-pass-through.test.ts` |
| REQ-SRV-03 | **Codex review #3** — positive guard: after position_update, sending c2s.input axes does NOT alter PlayerState.x or PlayerState.facing (held-input axes have no effect under Path A + per-player skip) | integration | `pnpm -C apps/server test:integration position-update` | Wave 0: case added to `apps/server/test/position-update.integ.test.ts` |
| REQ-CLI-08 | two-player smoke: A moves, B sees A move smoothly (no rubber-band, no diagonal-stop drift, no hitching on simulated packet delay) | e2e | `pnpm -C apps/client test:e2e two-player-movement` | partial — covered by existing CLI-08 e2e; ADD assertion for diagonal-stop drift regression |
| REQ-CLI-08 | manual operator UAT on staging — diagonal-stop drift bug closed; dropped-packet hitching bug closed; idle-cadence ~30 Hz visible in DevTools Network | manual-only | (operator session — captures in `06.7-HUMAN-UAT.md`) | Wave 0: NEW operator UAT script |

**Manual-only justification (REQ-CLI-08 row 2):** the two original motivating bugs (D-02) are subjective movement-feel issues caught by the operator's eye, not by automated thresholds. The automated CLI-08 e2e covers structural correctness (positions update, broadcast happens, two players see each other); operator UAT closes the feel question. Standard for 06.* operator-UAT-gated phases (see `04-HUMAN-UAT.md` precedent).

---

## Wave 0 Requirements

- [ ] `apps/server/test/rate-limit.test.ts` — EXTEND with codex review #1 cases (RATES.position_update + RATES.set_sprite_override; 30 Hz not dropped; > 60/s drops) → REQ-SRV-07
- [ ] `apps/server/test/position-update.test.ts` — unit tests for `cPositionUpdateSchema.safeParse` rejection paths + handler `PlayerState` writes → REQ-SRV-03
- [ ] `apps/server/test/position-update.integ.test.ts` — two-client roundtrip integration test + **codex review #3 positive guard** (held input has no effect after position_update) → REQ-SRV-03
- [ ] `apps/server/test/sprite-override.test.ts` — handler + schema → REQ-SRV-03
- [ ] `apps/server/test/protocol-v4-handshake.integ.test.ts` — version-bump rejection → REQ-SRV-03
- [ ] `packages/game-logic/test/step-pass-through.test.ts` — NEW per **codex review #6** — per-player skip-when-no-input contract + platform-advance regression → REQ-SRV-14
- [ ] `apps/client/src/__test__/position-dispatcher.test.ts` — dispatcher unit (incl. D-06 idle-cadence assertion) → REQ-CLI-04
- [ ] `packages/protocol/test/anim-state.test.ts` — `packAnimState`/`unpackAnimState` round-trip + **codex review #5** zero-cycle-import assertion → REQ-CLI-04
- [ ] `apps/client/src/__test__/reconciler.test.ts` — ADD self-noop assertion (file exists; new test case) → REQ-CLI-04 D-04 acceptance
- [ ] `apps/client/src/__test__/colyseus-client.test.ts` — EXTEND with **codex review #4 client-side** literal flip (protocol_version: 3 → 4 at :132, :142) → REQ-CLI-04
- [ ] `apps/server/test/layout-derive.test.ts` — EXTEND with **codex review #4 server-side** literal flip (expect(PROTOCOL_VERSION).toBe(3) → toBe(4) at :158) → REQ-SRV-03
- [ ] **Codex review #3 retirement list (parallel to D-04 client-side D-57b/D-58b retirement):** `apps/server/test/event-driven-input.integ.test.ts`, `apps/server/test/d62b-player-aabb.integ.test.ts`, `apps/server/test/d62-derived-collision.integ.test.ts` — RETIRED (git rm). `apps/server/test/d58c-set-facing.integ.test.ts` + `apps/server/test/d58c-direction-broadcast.integ.test.ts` — PARTIAL retire (per-case decision; set_facing handler round-trip cases RETAINED).
- [ ] Update existing `apps/client/src/__test__/prediction.test.ts` — confirm predictor still drives sprite (regression guard) → REQ-CLI-04
- [ ] `.planning/phases/06.7-network-model-client-trust-fall-trigger/06.7-HUMAN-UAT.md` — operator UAT script covering diagonal-stop drift + simulated-packet-delay hitching + idle-cadence visibility (with **codex review #8** Test 4 = automated-coverage note only, no `?protocol_version_override` reference) → REQ-CLI-08

*Framework install: none — `vitest` already pinned and runs in every package.*

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Diagonal-stop drift closed (~10 px horizontal shift on stop no longer observed) | REQ-CLI-08 (D-02) | Movement-feel; needs human eye | Operator UAT Test 1 — walk diagonally on staging, release keys, observe no shift |
| Dropped-packet hitching closed | REQ-CLI-08 (D-02) | Network-jitter perception | Operator UAT Test 2 — simulated packet delay via DevTools throttling; observe smooth interpolation |
| Idle cadence visible at ~30 Hz | REQ-CLI-04 (D-06) | Wire-cadence visual confirmation | Operator UAT Test 6 (added per plan-checker iter 1 warning #3) — idle player; open DevTools Network panel; observe ~30 `position_update` frames/sec |
| Rollback drill via `/gsd-undo` | REQ-CLI-08 (D-12, D-14) | Disaster-recovery rehearsal | Operator UAT Test 5 — run `/gsd-undo` on the 06.7 phase commits, verify staging reverts to 06.6 behavior |

---

## Validation Sign-Off

- [x] All tasks have `<automated>` verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references
- [x] No watch-mode flags
- [x] Feedback latency < 60s for quick run
- [x] `nyquist_compliant: true` set in frontmatter

**Approval:** pending (plan-checker re-verification after iter 1 revision)
