5.3 KiB
Code review — NEO-48 perk state GET + branch selection POST
Date: 2026-05-17
Scope: Branch NEO-48-perk-state-get-branch-selection-post-bruno · commits 5290044–39c54c3 vs main
Base: main
Follow-up: Suggestions 1–3 and nit 1 addressed on branch; r2 follow-up (aba2644) closed fixture/testing items — see 2026-05-17-NEO-48-r2.md.
Verdict
Approve
Summary
NEO-48 adds thin HTTP mapping for versioned GET/POST /game/players/{id}/perk-state: read model (branchPicks, unlockedPerkIds), branch selection via PerkUnlockEngine.TrySelectBranch, and structured selected / reasonCode / perkState / unlockedEvents envelopes mirroring NEO-38 grant patterns. Eight integration tests cover 404/400 gates, happy tier-1 pick after XP grant, level_too_low / branch_already_chosen denies, and path-auto unlockedEvents at level 4+. Bruno folder, README, manual QA, and E2.M3 decomposition rows align with the implementation plan. 185 server tests pass locally. Low merge risk; no engine or persistence logic duplicated in the API layer.
Documentation checked
| Document | Result |
|---|---|
docs/plans/NEO-48-implementation-plan.md |
Matches — same-path GET/POST, schema v1, deny envelope, unlockedEvents on success, position-state 404 gate, Bruno/README/tests/manual QA; acceptance checklist complete. |
docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md |
Matches — NEO-48 handoff section; HTTP responsibility listed under module responsibilities. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
Matches — E2.M3 row cites NEO-48 landed + README/Bruno links. |
docs/decomposition/modules/module_dependency_register.md |
Matches — E2.M3 note includes NEO-48 perk-state HTTP. |
docs/decomposition/modules/client_server_authority.md |
Matches — branch picks and unlock evaluation remain server-authoritative; no client changes (out of scope). |
docs/plans/NEO-47-implementation-plan.md |
Matches — API delegates to engine/store; no duplicate unlock rules; telemetry stays in TryUnlockPerks (NEO-49). |
Register/tracking: E2.M3 module doc, alignment row, and register note updated in implementation commit.
Blocking issues
None.
Suggestions
Bruno deny forDone. Addedbranch_already_chosen— Plan and manual QA describe a second POST to the same tier with a different branch; automated tests cover it (PostPerkState_ShouldDenyBranchAlreadyChosen_OnSecondPick), but Bruno only shipsPost branch select deny level too low.bru. A fourth request (grant + first pick + conflicting second pick) would make the collection self-contained for CI/manual runs without relying on manual QA alone.Post branch select deny branch already chosen.bruwithscript:pre-requestgrant + first pick viabru.sendRequest.Optional thin HTTP deny smoke — Engine tests lockDone.unknown_track,unknown_tier, andunknown_branch; one API test each (badskillId, badtierIndex, badbranchIdon dev player after level gate) would guard JSON field mapping if DTOs change. Non-blocking givenPerkUnlockEngineTestscoverage.PostPerkState_ShouldDenyUnknownTrack_*,UnknownTier_*,UnknownBranch_*inPerkStateApiTests.POST null body — Handlers return 400 forDone.body is null(same as skill-progression). Neither API suite asserts empty-body 400 today; add a one-liner inPerkStateApiTestsif you want parity documentation in tests.PostPerkState_ShouldReturnBadRequest_WhenBodyNull.
Nits
Nit:Done. FolderPost branch select deny level too low.bruintentionally passes when salvage is already leveled (restart note) — acceptable for shared dev player; document in Bruno folderdocsif operators hit false greens often.docsblock inperk-state/folder.bru.Nit: Path-auto tier-2 unlocks surfaced inDeferred. Documented in NEO-47/NEO-49; no API change required for NEO-48.unlockedEventsusesource: level_up(notbranch_pick) — consistent with NEO-47/NEO-49 notes; clients/telemetry should treatsourceas unlock channel, not HTTP verb.Nit:Deferred. Accepted integration-test pattern; no change.PostPerkState_ShouldSelectTier1_AndGetReflectsPick_AfterSalvageLevel2issues a follow-up GET inside Assert for end-to-end verification — valid AAA (verification in Assert); grant status checks in Arrange match other API tests in the repo.
Verification
# From repo root
dotnet test NeonSprawl.sln --filter "FullyQualifiedName~PerkStateApi"
# Full server suite (195 tests at final verification)
dotnet test NeonSprawl.sln
# Manual / Bruno
# docs/manual-qa/NEO-48.md
# bruno/neon-sprawl-server/perk-state/
Confirm GET empty state, POST tier-1 happy path after salvage grant (100 XP), POST deny codes, and GET reflects persisted pick; optional path-auto unlockedEvents at level 4+ (500 total XP before pick).