5.2 KiB
Code review — NEO-127 (E7M2-04)
Date: 2026-06-07
Scope: Branch NEO-127-e7m2-reward-router-operations vs origin/main — commits 35b8904 … 65ca111
Base: origin/main
Verdict
Approve with nits — router apply, compensating rollback, idempotent replay, and store record flow mirror the NEO-105 encounter-completion precedent; plan AC and module contracts are satisfied; risk is low (server-internal only until NEO-128 wiring).
Summary
NEO-127 adds RewardRouterOperations.TryDeliverQuestCompletion under Game/Rewards/ to apply quest completionRewardBundle rows: item grants via PlayerInventoryOperations (simulate-all pre-flight, sequential apply, compensating rollback) and skill XP via SkillProgressionGrantOperations.TryApplyGrant with fixed mission_reward source kind. RewardDeliveryResult and RewardDeliveryReasonCodes provide structured outcomes; IRewardDeliveryStore records RewardDeliveryEvent on success with idempotent replay via TryGet before apply and race handling when TryRecord returns false. Ten AAA integration tests cover happy paths, inventory deny, skill deny rollback, store replay, unknown-skill deny, null-bundle empty record, and invalid-id deny. Docs (NEO-127 plan reconciliation, E7.M2 module anchor, alignment register, server/README.md) are updated. No player-visible behavior until NEO-128 wires quest completion.
Documentation checked
| Path | Result |
|---|---|
docs/plans/NEO-127-implementation-plan.md |
Matches — kickoff decisions adopted; AC checked; reconciliation accurate. |
docs/plans/E7M2-prototype-backlog.md (E7M2-04) |
Matches — AC checkboxes complete; in-scope skill XP path aligned with TryApplyGrant. |
docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md |
Matches — NEO-127 implementation anchor documents router apply path. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
Matches — E7.M2 row notes E7M2-04 router apply; row correctly stays Planned until NEO-132 capstone. |
docs/decomposition/modules/module_dependency_register.md |
Matches — no register change required for this slice. |
server/README.md |
Matches — reward router section documents entry point, reason codes, apply order, idempotency + store race behavior. |
| Full-stack epic decomposition | N/A — E7M2-04 is server engine only; no client counterpart required. |
Register / tracking: E7.M2 correctly remains Planned until capstone NEO-132; alignment update is appropriate.
Blocking issues
None.
Suggestions
-
Empty / null bundle test — Kickoff decision and plan both require success with empty grant snapshots andDone.TryRecordfor null or both-empty bundles. Code handles this (skips inventory pre-flight, records empty arrays), but no test locks the behavior. A single AAA test withbundle: nullornew QuestRewardBundleRow([], [])would close the gap.TryDeliverQuestCompletion_ShouldRecordEmptyDelivery_WhenBundleIsNull. -
Invalid id deny tests —Done.invalid_player_id/invalid_quest_idare implemented and documented inserver/README.md, but untested. Two small deny-path tests (empty/whitespace ids) would match the fail-closed store precedent from NEO-126.TryDeliverQuestCompletion_ShouldDenyInvalidPlayerId_WhenPlayerIdIsEmptyand…InvalidQuestId…. -
Replay XP assertion — InDone. Gather intro test re-fetches XP after replay.TryDeliverQuestCompletion_ShouldApplyGatherIntroSkillXp_WhenBundleIsSkillOnly, the second-call assertion reusessalvageXpcaptured before the replayDelivercall. Re-fetchGetXpTotalsafter the second call to prove no double XP grant on idempotent replay.
Nits
-
Nit: Pre-flight
TrySimulateAddStackfailure always denies withinventory_full, even when the underlying cause would beinvalid_itemorinvalid_quantityat apply time. Same asEncounterCompletionOperations(NEO-105); acceptable for prototype catalog-gated content; apply-phase denies still return correct passthrough codes. -
Nit: When
TryRecordreturnsfalseand a subsequentTryGetalso misses, the router still returns success with the locally builtdeliveryEvent(line 149 fallback). Extremely unlikely with the in-memory store; mirrors “retain grants, treat as idempotent success” policy from the plan. Consider a debug log if this path ever surfaces in production persistence. -
Nit:
CompensatingRemoveItemsignoresTryRemoveStackoutcomes (same as encounter compensating helper). Acceptable for prototype; multi-item bundles with partial rollback failure would be a future hardening item. -
Nit: Optional plan test for
source_kind_not_allowednegative path not implemented; catalog startup gates make this low priority.
Verification
dotnet test server/NeonSprawl.Server.Tests --filter RewardRouterOperationsTests
All 14 tests pass (verified 2026-06-07; includes concurrent level-up winner perk preservation on TryRecord race loss).
Optional before merge:
- Boot dev server; confirm
/health200 — no new DI registration; behavior unchanged until NEO-128.