# Manual QA — NEO-48 (perk state GET + branch selection POST) Reference: [implementation plan](../plans/NEO-48-implementation-plan.md), [NEO-47 plan](../plans/NEO-47-implementation-plan.md) (unlock engine). ## Preconditions - Run `NeonSprawl.Server` (`dotnet run` from `server/NeonSprawl.Server`; default `http://localhost:5253`). - Dev player `dev-local-1` exists in position state (default in-memory / seeded Postgres). ## GET empty state ```bash curl -sS "http://localhost:5253/game/players/dev-local-1/perk-state" | jq . ``` - [ ] HTTP **200**; `schemaVersion` **1**; `branchPicks` **[]**; `unlockedPerkIds` **[]** (fresh server). ## POST tier-1 pick (happy path) 1. Grant salvage XP to reach level **2** (prototype curve: **100** total XP): ```bash curl -sS -X POST "http://localhost:5253/game/players/dev-local-1/skill-progression" \ -H "Content-Type: application/json" \ -d '{"schemaVersion":1,"skillId":"salvage","amount":100,"sourceKind":"activity"}' | jq . ``` 2. Select tier-1 branch: ```bash curl -sS -X POST "http://localhost:5253/game/players/dev-local-1/perk-state" \ -H "Content-Type: application/json" \ -d '{"schemaVersion":1,"skillId":"salvage","tierIndex":1,"branchId":"scrap_efficiency"}' | jq . ``` - [ ] `selected: true`; `perkState.branchPicks` contains salvage tier 1 `scrap_efficiency`; `unlockedEvents` empty at tier 1. - [ ] Repeat **GET** — same pick visible. ## POST deny - [ ] Second pick same tier different branch → `selected: false`, `reasonCode` **`branch_already_chosen`**. - [ ] On fresh server without XP grant → `level_too_low`. ## Bruno - [ ] Run folder `bruno/neon-sprawl-server/perk-state/` (GET + POST happy + deny smoke). - [ ] Run **`Post branch select deny branch already chosen`** (self-contained; pre-request grant + first pick). ## Regression ```bash cd server && dotnet test NeonSprawl.sln ``` Expect all tests pass including `PerkStateApiTests`.