# NEO-30 — Manual QA checklist | Field | Value | |-------|-------| | Key | NEO-30 | | Title | E1M4-05: Slice 3 telemetry hooks for cast funnel | | Linear | https://linear.app/neon-sprawl/issue/NEO-30/e1m4-05-slice-3-telemetry-hooks-for-cast-funnel | | Plan | `docs/plans/NEO-30-implementation-plan.md` | | Branch | `NEO-30-cast-funnel-telemetry-hooks` | ## 1) Code / docs sanity - [ ] Open `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` and confirm **NEO-30** comments mark **`ability_cast_denied`** on each JSON deny return and **`ability_cast_requested`** on the final accept return; each deny branch notes **TODO(E9.M1)**. - [ ] Confirm **400** / **404** early returns are documented as **not** Slice 3 `ability_cast_denied` JSON paths. - [ ] Skim `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` — snapshot mentions NEO-30 hook file paths; **E1M4-05** row links to Linear **NEO-30**. ## 2) Client hook vocabulary (regression with NEO-31 / NEO-28) - [ ] With server running and slot **0** bound to `prototype_pulse`, **Tab** lock **alpha**, press **1**: Godot Output line starts with **`ability_cast_requested`** and includes `slot=0`, `ability_id=prototype_pulse`, `targetId=` consistent with HUD lock. - [ ] **Optional (server stdout):** same scenario with **`NEON_SPRAWL_API_LOG=1`** (see [server README — Run](../../server/README.md)) — server console shows **`[ability-cast] ability_cast_requested`** for that accept (Slice 3 hook name alignment). - [ ] **Without** lock (or wrong target), press **1**: **`CastFeedbackLabel`** shows **`ability_cast_denied: …`** with a **non-empty** reason; Output warning includes **`ability_cast_denied`** and **`reasonCode=`** (see [`docs/manual-qa/NEO-28.md`](NEO-28.md) for concrete cases). ## 3) Server JSON deny (reason non-empty) - [ ] `curl` or Bruno: provoke a **200** deny (e.g. `loadout_mismatch` per [`docs/manual-qa/NEO-31.md`](NEO-31.md)) — response JSON has **`accepted": false`** and a **non-empty** **`reasonCode`**.