diff --git a/docs/plans/NEO-58-implementation-plan.md b/docs/plans/NEO-58-implementation-plan.md index d17d228..2ddea04 100644 --- a/docs/plans/NEO-58-implementation-plan.md +++ b/docs/plans/NEO-58-implementation-plan.md @@ -96,11 +96,12 @@ | Item | Coverage | |------|----------| -| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs` | **Add** — AAA: `Load` happy path with repo-shaped JSON; failures for missing dir/schema, bad `schemaVersion`, schema violations, duplicate `nodeDefId`, incomplete Slice 2 ids, lens set mismatch, orphan yield `nodeDefId`, unknown `itemId`, duplicate yield `nodeDefId`, missing yield for node; `Host_ShouldResolveCatalogFromDi`; `Host_ShouldFailStartup_WhenCatalogDirectoryInvalid`. | +| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs` | **Add** — AAA: `Load` happy path with repo-shaped JSON; failures for missing dir/schema, bad `schemaVersion`, schema violations, duplicate `nodeDefId`, incomplete Slice 2 ids, lens set mismatch, orphan yield `nodeDefId`, unknown `itemId`, duplicate yield `nodeDefId`, missing yield for node, empty `*_resource_nodes.json` / `*_resource_yields.json` directories; `Host_ShouldResolveCatalogFromDi`; `Host_ShouldFailStartup_WhenCatalogDirectoryInvalid`. | | `server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs` | **Change** if needed — ensure resource-node content is discoverable in test host (same ancestor walk as items). | | **CI / PR gate** | Existing `scripts/validate_content.py` remains content regression; server tests guard C#/Python drift for Slice 2 constants. | +| **Bruno smoke (optional)** | `bruno/neon-sprawl-server/resource-node-catalog/Health after resource node catalog load.bru` — `GET /health` after boot; no resource-node HTTP API this story (pre-commit hook when `Program.cs` changes). | -No Bruno or client tests (out of scope). +No client tests (out of scope). ## Open questions / risks diff --git a/docs/reviews/2026-05-24-NEO-58.md b/docs/reviews/2026-05-24-NEO-58.md index 443e9ce..c13ea55 100644 --- a/docs/reviews/2026-05-24-NEO-58.md +++ b/docs/reviews/2026-05-24-NEO-58.md @@ -32,15 +32,15 @@ None. ## Suggestions -1. **Plan vs Bruno** — [`NEO-58-implementation-plan.md`](../plans/NEO-58-implementation-plan.md) lists Bruno as out of scope, but `bruno/neon-sprawl-server/resource-node-catalog/` adds a health smoke request. Either add a one-line note under **Tests** (“optional Bruno smoke”) or drop the folder to keep plan scope literal; the request itself is fine for manual boot verification. +1. ~~**Plan vs Bruno** — [`NEO-58-implementation-plan.md`](../plans/NEO-58-implementation-plan.md) lists Bruno as out of scope, but `bruno/neon-sprawl-server/resource-node-catalog/` adds a health smoke request. Either add a one-line note under **Tests** (“optional Bruno smoke”) or drop the folder to keep plan scope literal; the request itself is fine for manual boot verification.~~ **Done.** Plan **Tests** table now documents optional Bruno health smoke (pre-commit when `Program.cs` changes). -2. **Explicit empty-catalog file test (optional)** — `Host_ShouldFailStartup_WhenCatalogDirectoryInvalid` covers an empty directory; a loader unit test asserting `no *_resource_nodes.json files` (and yields) would mirror item-catalog negative coverage and make failure messages grep-friendly in CI logs. +2. ~~**Explicit empty-catalog file test (optional)** — `Host_ShouldFailStartup_WhenCatalogDirectoryInvalid` covers an empty directory; a loader unit test asserting `no *_resource_nodes.json files` (and yields) would mirror item-catalog negative coverage and make failure messages grep-friendly in CI logs.~~ **Done.** Added `Load_ShouldThrow_WhenNoResourceNodesJsonFiles` and `Load_ShouldThrow_WhenNoResourceYieldsJsonFiles`. ## Nits - Nit: Slice 2 lens gate checks **set** equality (same as Python/NEO-57); duplicate `gatherLens` on two nodes still fails via set mismatch — acceptable for prototype. -- Nit: `Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds` uses `/health` in **Act** (NEO-51 item-catalog host test does the same); strict AAA would move the HTTP call to **Arrange** and keep **Act** as `GetRequiredService()` only. +- ~~Nit: `Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds` uses `/health` in **Act** (NEO-51 item-catalog host test does the same); strict AAA would move the HTTP call to **Arrange** and keep **Act** as `GetRequiredService()` only.~~ **Done.** Host test is sync; **Act** is `GetRequiredService()` only (`CreateClient()` in **Arrange** builds the host). - Nit: No `docs/manual-qa/NEO-58.md` — consistent with plan (server boot-only, no user-facing HTTP for resource nodes); Bruno health folder is sufficient smoke. diff --git a/server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs b/server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs index 70951a9..2cd9af5 100644 --- a/server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeCatalogLoaderTests.cs @@ -1,4 +1,3 @@ -using System.Net; using System.Text; using System.Text.Json.Nodes; using Microsoft.AspNetCore.Mvc.Testing; @@ -486,6 +485,44 @@ public class ResourceNodeCatalogLoaderTests Assert.Contains(missingDir, ioe.Message, StringComparison.Ordinal); } + [Fact] + public void Load_ShouldThrow_WhenNoResourceNodesJsonFiles() + { + // Arrange + var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout(); + WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson); + // Act + var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load( + resourceNodesDir, + nodeSchemaPath, + yieldSchemaPath, + ValidPrototypeItemIds, + NullLogger.Instance)); + // Assert + var ioe = Assert.IsType(ex); + Assert.Contains("no *_resource_nodes.json files", ioe.Message, StringComparison.Ordinal); + Assert.Contains(resourceNodesDir, ioe.Message, StringComparison.Ordinal); + } + + [Fact] + public void Load_ShouldThrow_WhenNoResourceYieldsJsonFiles() + { + // Arrange + var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout(); + WriteNodeCatalog(resourceNodesDir, ValidPrototypeNodeCatalogJson); + // Act + var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load( + resourceNodesDir, + nodeSchemaPath, + yieldSchemaPath, + ValidPrototypeItemIds, + NullLogger.Instance)); + // Assert + var ioe = Assert.IsType(ex); + Assert.Contains("no *_resource_yields.json files", ioe.Message, StringComparison.Ordinal); + Assert.Contains(resourceNodesDir, ioe.Message, StringComparison.Ordinal); + } + [Fact] public void Load_ShouldThrow_WhenNodeSchemaFileMissing() { @@ -506,16 +543,14 @@ public class ResourceNodeCatalogLoaderTests } [Fact] - public async Task Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds() + public void Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds() { // Arrange - await using var factory = new InMemoryWebApplicationFactory(); - using var client = factory.CreateClient(); + using var factory = new InMemoryWebApplicationFactory(); + _ = factory.CreateClient(); // Act - var response = await client.GetAsync("/health"); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); var catalog = factory.Services.GetRequiredService(); + // Assert Assert.Equal(4, catalog.DistinctNodeCount); Assert.True(catalog.TryGetNode("prototype_bio_mat_gamma", out var node)); Assert.Equal("bio", node!.GatherLens);