# 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`**.
