8.6 KiB
Code review — NEO-138 (E7M3-06)
Date: 2026-06-15
Scope: Branch NEO-138-e7m3-reward-router-reputation-grants — commits 599d87b … 69c3ad4 vs main
Base: main
Follow-up: Re-reviewed after 69c3ad4 (code-review follow-ups). Prior blocking/suggestions verified in code; 810 tests green.
Verdict
Approve
Summary
This branch lands E7M3-06: RewardRouterOperations.TryDeliverQuestCompletion applies reputationGrants via ReputationOperations.TryApplyDelta after items and skill XP, with compensating rollback (including inverse rep deltas with :rollback suffix) and GrantedReputation on delivery event/result snapshots. Wiring threads IFactionStandingStore + IReputationDeltaStore through quest state, objective wiring, and gameplay APIs. Tests cover happy path, idempotent replay, unknown-faction deny with item/skill rollback, and quest-state integration (operator chain +15 standing, grid contract accept after organic chain completion). Bruno adds operator-chain-quest-flow-helper.js and Accept grid contract after operator chain.bru. 810 server tests pass locally. Core router logic matches NEO-127/NEO-136 precedent and the implementation plan.
Documentation checked
| Path | Result |
|---|---|
docs/plans/NEO-138-implementation-plan.md |
Matches — router apply order, idempotency, rollback, wiring, README, module/alignment updates, Bruno quest-flow, and plan-listed tests (including race rep rollback and audit-row assertion). |
docs/plans/E7M3-pre-production-backlog.md |
Matches — E7M3-06 rep grants on quest completion bundle via router; HTTP standing / client HUD explicitly deferred. |
docs/decomposition/modules/E7_M3_FactionReputationLedger.md |
Matches — Status line notes E7M3-06 landed (NEO-138); rep flow through E7.M2 router documented. |
docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md |
Matches — bundle apply + idempotent delivery store extended with rep rows (implicit via router). |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
Matches — E7.M3 row updated for NEO-138 router extension. |
docs/decomposition/modules/module_dependency_register.md |
N/A change — no register row edit required beyond module doc status. |
server/README.md |
Matches — reward router section documents rep apply order, reason codes, rollback policy, Bruno seq 11 success sibling, telemetry hook comment for reputation_delta. |
Blocking issues
Bruno success smoke asserts wrong quest status after accept.Done. Bruno expectsbruno/neon-sprawl-server/quest-progress/Accept grid contract after operator chain.bruexpectsbody.quest.status === "active"andcurrentStepIndex === 0. After organic operator-chain completion the player receivessurvey_drone_kitin the completion bundle.prototype_quest_grid_contracthas a singleinventory_has_itemobjective for that item.QuestStateOperations.TryAcceptrunsQuestObjectiveWiring.TryCompleteStepIfSatisfiedimmediately after activation, so accept auto-completes the quest and returnscompleted(integration testTryAccept_ShouldAcceptGridContract_WhenOperatorChainCompletedWithRepGrantcorrectly assertsQuestProgressStatus.Completed). CI runs the full quest-progress collection with--bail; seq 11 will fail on the status assertion even when the gate and rep grant work. Fix: expectcompleted(and optionally assert Rust Collective +10 standing or delivery event), or change content/fixture so the kit is not held at accept time ifactiveis the intended smoke signal.completed; docs note auto-complete when kit already held.
Suggestions
Add rep rollback onDone.TryRecordrace loss. ExtendTryDeliverQuestCompletion_ShouldRollbackGrants_WhenTryRecordLosesRace(or add a sibling) with an operator-chain bundle includingreputationGrantsand assert standing returns to pre-call value after simulated race loss — listed explicitly in NEO-138 plan §6 tests table.TryDeliverQuestCompletion_ShouldRollbackReputation_WhenTryRecordLosesRace.Assert audit row on operator-chain router happy path. Plan scenario “audit row queryable” — addDone. Audit row asserted in operator-chain bundle test.IReputationDeltaStorelookup for{idempotencyKey}:rep:{factionId}inTryDeliverQuestCompletion_ShouldApplyItemAndSkillXp_WhenOperatorChainBundle(or the dedicated rep test).Document auto-complete on grid-contract accept in tests/README. Integration test name says “Accept” but the meaningful gate check is standing ≥ 15; the returned snapshot isDone. Comment incompletedbecause the kit is already in inventory. A one-line comment inQuestStateOperationsTestsand Bruno docs block would prevent future “active vs completed” confusion.QuestStateOperationsTests; Bruno docs block updated.Optional: multi-row rep deny rollback test. Plan lists rep deny after items with audit append failure on the second rep row; onlyDone.unknown_factionrollback is covered today. Consider a store stub that failsTryAppendon the second row to lock compensating rollback across grant types.TryDeliverQuestCompletion_ShouldRollbackAllGrants_WhenSecondReputationAuditAppendFailswith forward-rep-only audit stub.Bugbot: fixture reset skips delivery store.Done.resetQuestIdscleared quest progress and faction standing but notIRewardDeliveryStore/ audit rows, so repeated Bruno runs skipped rep re-apply via idempotent delivery (and duplicate audit ids).resetQuestIdsclears delivery + quest-completion audit rows.
Nits
Nit:Done.TryDeliverQuestCompletion_ShouldRecordEmptyDelivery_WhenBundleIsNulldoes not assertGrantedReputationis empty — mirror item/skill empty assertions for snapshot parity.- Nit:
CompensatingRevertReputationskipsAmount == 0rows; fine for prototype bundles but a clamp-to-zero edge case could leave standing changed while snapshot says 0 — defer unless content adds negative grants near floor. - Nit (re-review):
server/README.mdFactionGateOperations section still says grid-contract success Bruno is “deferred until NEO-138” — stale; seq 11 landed. One-line README tweak optional before merge. - Nit (re-review):
TryDeliverQuestCompletion_ShouldRollbackReputation_WhenTryRecordLosesRaceasserts standing only on a full operator-chain bundle; item/skill rollback on the same path is covered by the older skill-only race test + sharedCompensatingRevertAll— acceptable, not blocking.
Re-review (2026-06-15)
| Original finding | Verified |
|---|---|
Bruno seq 11 status active vs completed |
Fixed — Accept grid contract after operator chain.bru expects completed; docs note auto-complete when kit held. |
| Race rep rollback test | Fixed — TryDeliverQuestCompletion_ShouldRollbackReputation_WhenTryRecordLosesRace. |
| Audit row on operator-chain happy path | Fixed — delta id + row fields asserted in bundle test. |
| Grid-contract accept comment | Fixed — QuestStateOperationsTests Arrange comment + Bruno docs. |
| Multi-row audit-append rollback | Fixed — AuditStoreFailingOnSecondForwardRepAppend + full grant rollback test. |
Empty GrantedReputation on null bundle |
Fixed — result + stored event asserted. |
No new blocking issues from follow-up commit. Test stubs (AuditStoreFailingOnSecondForwardRepAppend, race-loss store) are sound; AAA layout on new tests matches project conventions.
Verification
# Full server suite (810 green locally)
dotnet test NeonSprawl.sln
# NEO-138-focused filter
dotnet test NeonSprawl.sln --filter "FullyQualifiedName~RewardRouterOperations|FullyQualifiedName~GridContract|FullyQualifiedName~OperatorChain"
# Bruno quest-progress (seq 11 expects completed after kit held)
cd bruno/neon-sprawl-server
npx --yes @usebruno/cli@3.3.0 run quest-progress --env Local --sandbox=developer --bail
With Postgres + dev fixtures (matches CI):
ConnectionStrings__NeonSprawl='Host=localhost;Port=5432;Database=neon_sprawl;Username=neon_sprawl;Password=neon_sprawl_dev' \
Game__EnableCombatTargetFixtureApi=true \
Game__EnableQuestFixtureApi=true \
Game__EnableResourceNodeFixtureApi=true \
dotnet run --project server/NeonSprawl.Server/NeonSprawl.Server.csproj --urls http://127.0.0.1:5253
Manual: run Accept grid contract after operator chain.bru; confirm accepted: true and quest row completed when survey_drone_kit is already in bag.