# NEO-29 — Manual QA checklist

| Field | Value |
|-------|-------|
| Key | NEO-29 |
| Title | E1M4-01: HotbarLoadout v1 contract + baseline persistence path |
| Linear | https://linear.app/neon-sprawl/issue/NEO-29/e1m4-01-hotbarloadout-v1-contract-baseline-persistence-path |
| Plan | `docs/plans/NEO-29-implementation-plan.md` |
| Branch | `NEO-29-hotbarloadout-v1-contract-baseline-persistence-path` |

## 1) Setup sanity

- [ ] Start server: `cd server/NeonSprawl.Server && dotnet run`.
- [ ] Confirm health endpoint responds: `curl -s http://localhost:5253/health`.
- [ ] Confirm baseline loadout for dev player is reachable: `curl -s http://localhost:5253/game/players/dev-local-1/hotbar-loadout`.

## 2) Happy path (bind and re-read)

- [ ] Bind slot 0 + slot 3 in one request:
  `curl -s -X POST http://localhost:5253/game/players/dev-local-1/hotbar-loadout -H "Content-Type: application/json" -d '{"schemaVersion":1,"slots":[{"slotIndex":0,"abilityId":"prototype_pulse"},{"slotIndex":3,"abilityId":"prototype_burst"}]}'`
- [ ] Verify response contains `updated=true` and `loadout.slots[0].abilityId == "prototype_pulse"` and `loadout.slots[3].abilityId == "prototype_burst"`.
- [ ] Run GET again and verify the same two bindings are still present.

## 3) Reconnect / persistence behavior

- [ ] If Postgres is configured (`ConnectionStrings__NeonSprawl` set), stop and restart the server, then GET loadout and verify slot 0/3 bindings survived restart.
- [ ] If Postgres is not configured (in-memory fallback), verify loadout behavior in the same process is stable and document that restart reset is expected for this mode.

## 4) Deny paths and reason codes

- [ ] Send out-of-bounds slot (`slotIndex: 8`) and confirm response has `updated=false` with `reasonCode == "slot_out_of_bounds"`.
- [ ] Send unknown ability id (`abilityId: "not_real"`) and confirm response has `updated=false` with `reasonCode == "unknown_ability"`.
- [ ] Send duplicate slot index entries in one POST and confirm response has `updated=false` with `reasonCode == "duplicate_slot"`.

## 5) Client hydration sanity

- [ ] Run `client/scenes/main.tscn` in Godot after slot bindings exist on server.
- [ ] Confirm no startup warnings from `HotbarLoadoutClient` and verify `HotbarState` node exists under the scene root with 8-slot mirror populated from server response.

## 6) Client-driven update sanity (no curl)

- [ ] With `main.tscn` running, open the remote SceneTree and select `HotbarLoadoutClient` under the scene root.
- [ ] Execute `request_bind_slot(2, "prototype_guard")` on that node (Editor script console or remote call).
- [ ] Confirm no client warning log from `HotbarLoadoutClient` and verify `HotbarState.slots_snapshot()` now contains `"prototype_guard"` at index `2`.
- [ ] Restart the client scene and verify hydration still reflects slot `2` from server (proves client fetch + apply path, not just local mutation).

## 7) Client deny-path signal sanity

- [ ] With `main.tscn` running, execute `request_bind_slot(2, "not_real")` on `HotbarLoadoutClient`.
- [ ] Confirm the client logs/handles a deny response (server `updated=false`, `reasonCode="unknown_ability"`), and `HotbarState` remains unchanged from the last valid loadout.
