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