63 lines
5.4 KiB
Markdown
63 lines
5.4 KiB
Markdown
# Code review — NEO-108 (E5M3-08)
|
|
|
|
**Date:** 2026-05-31
|
|
**Scope:** Branch `NEO-108-get-encounter-progress-wire-dtos-bruno` vs `51e2007` (merge-base on `main`) — commits `1601858` … `07ebb9e`
|
|
**Base:** `51e2007` (main at branch point, post NEO-107 merge)
|
|
|
|
## Verdict
|
|
|
|
**Approve with nits**
|
|
|
|
## Summary
|
|
|
|
NEO-108 adds **`GET /game/players/{id}/encounter-progress`** — a versioned read model projecting catalog encounters from **`IEncounterDefinitionRegistry`**, **`IEncounterProgressStore`**, **`IEncounterCompletionStore`**, and **`IEncounterCompleteEventStore`**, with the same **`IPositionStateStore`** 404 gate as gig/skill progression. **`EncounterProgressApi.BuildSnapshot`** is internal and testable; state derivation (`inactive` / `active` / `completed`), ordinal-sorted **`defeatedTargetIds`**, and commit-time **`rewardGrantSummary`** match the kickoff plan. Five AAA integration tests cover 404, inactive default, 1- and 2-defeat active rows, and completed + grant summary; Bruno adds inactive smoke and a three-NPC defeat spine with intermediate GET assertions and idempotent second GET. Docs (plan, backlog E5M3-08, E5.M3 snapshot, alignment register, module register, `server/README.md`) are updated. Godot poll client remains correctly deferred to **NEO-110**. Risk is low: read-only projection with fail-fast invariant checks on completed rows.
|
|
|
|
## Documentation checked
|
|
|
|
| Path | Result |
|
|
|------|--------|
|
|
| `docs/plans/NEO-108-implementation-plan.md` | **Matches** — kickoff decisions adopted; acceptance checklist checked; reconciliation section accurate; frozen row progression table reflected in code, tests, and Bruno. |
|
|
| `docs/plans/E5M3-prototype-backlog.md` (E5M3-08) | **Matches** — acceptance criteria checked; landed note cites plan; client counterpart **NEO-110** called out. |
|
|
| `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | **Matches** — Status row includes **E5M3-08 / NEO-108**; implementation snapshot bullet added with README + Bruno links. |
|
|
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E5.M3 row notes **NEO-108** per-player GET. |
|
|
| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — E5.M3 note cites **NEO-108** GET landed. |
|
|
| Full-stack epic decomposition | **N/A** — plan explicitly defers client counterpart to **NEO-110**; no prototype slice completion claim. |
|
|
| `server/README.md` | **Matches** — new **Per-player encounter progress (NEO-108)** section; NEO-106 section cross-links GET for loot verification. |
|
|
|
|
## Blocking issues
|
|
|
|
None.
|
|
|
|
## Suggestions
|
|
|
|
1. ~~**Invariant violation tests for `BuildSnapshot`** — Plan adopts fail-fast **`InvalidOperationException`** when **`completed`** but **`completedAt`** or **`EncounterCompleteEvent`** is missing. No direct unit test exercises these paths (would require a small fake/mocked store pair). A focused **`BuildSnapshot`** test would lock the contract without a full defeat spine.~~ **Done.** `BuildSnapshot_ShouldThrow_WhenCompletedButCompleteEventMissing` + `BuildSnapshot_ShouldThrow_WhenCompletedButCompletedAtMissing` (`CompletedWithoutTimestampStore` fake).
|
|
|
|
2. ~~**Idempotent second GET in C# tests** — Bruno **`Get encounter progress after defeat spine.bru`** asserts a second GET stays **`completed`** with unchanged grant summary; the C# suite stops after one GET on the completed case. A second **`GetAsync`** in the completed test (or a dedicated test) would mirror Bruno at the integration layer.~~ **Done.** Second **`GetAsync`** in completed test asserts stable state, **`completedAt`**, and grant summary.
|
|
|
|
3. ~~**Whitespace / empty player id 404** — Route trims **`id`** and returns 404 when empty; only **`missing-player`** is tested. A **`/game/players/%20/encounter-progress`** (or `""` after trim) case would match the documented gate explicitly (same gap exists on gig progression — low priority).~~ **Done.** `GetEncounterProgress_ShouldReturnNotFound_WhenPlayerIdIsWhitespaceOnly`.
|
|
|
|
## Nits
|
|
|
|
- ~~Nit: **`MapRow`** calls **`progressStore.TryGetProgress`** twice on the **`active`** path (once for **`defeatedTargetIds`**, again for **`Started`**). A single **`TryGet`** with reused **`progress`** variable would avoid the duplicate lookup (prototype catalog size = 1; cosmetic only).~~ **Done.**
|
|
|
|
- Nit: **`EncounterProgressApiTests`** defeat helper binds hotbar **slot 0**; Bruno spine uses **slot 3** per plan (cooldown isolation). Both are fine in isolation; no change required unless slot 0 conflicts emerge in combined Bruno runs.
|
|
|
|
- ~~Nit: **`MapGrantSummary`** preserves reward-table grant order (scrap then token). C# completed test asserts quantities via **`Single(g => g.ItemId == …)`** (order-agnostic); Bruno asserts array order. Document or test order explicitly if clients will diff arrays.~~ **Done.** XML on **`MapGrantSummary`**; completed test asserts full grant array order (matches Bruno).
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
# NEO-108-focused (5 tests — passed locally)
|
|
cd server && dotnet test NeonSprawl.Server.Tests/NeonSprawl.Server.Tests.csproj \
|
|
--filter "FullyQualifiedName~EncounterProgressApiTests"
|
|
|
|
# Encounters module parity (79 tests — passed locally)
|
|
cd server && dotnet test NeonSprawl.Server.Tests/NeonSprawl.Server.Tests.csproj \
|
|
--filter "FullyQualifiedName~Encounters"
|
|
|
|
# Bruno (server running on baseUrl)
|
|
# bruno/neon-sprawl-server/encounter-progress/
|
|
```
|
|
|
|
**Local run:** 5/5 `EncounterProgressApiTests` passed; 79/79 `Encounters` filter passed.
|