diff --git a/bruno/neon-sprawl-server/ability-catalog/folder.bru b/bruno/neon-sprawl-server/ability-catalog/folder.bru new file mode 100644 index 0000000..b32d530 --- /dev/null +++ b/bruno/neon-sprawl-server/ability-catalog/folder.bru @@ -0,0 +1,3 @@ +meta { + name: ability-catalog +} diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index 1a54104..d31efc4 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -76,7 +76,7 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen | E5.M3 | EncounterAndRewardTables | E5.M1, E3.M3, E7.M2 | EncounterDef, RewardTable, EncounterCompleteEvent | Prototype | Planned | | E5.M4 | GroupCombatScaling | E5.M3, E8.M1 | ScalingProfile, PartySizeModifier, CombatDifficultyBand | Pre-production | Planned | -**E5.M1 note:** Epic 5 **Slice 1** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) → [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); label **`E5.M1`**. Gig XP on defeat: **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1_CombatRulesEngine.md](E5_M1_CombatRulesEngine.md). Builds on E1.M4 cast funnel (NEO-28/31/32); extends **`POST …/ability-cast`** with **`CombatResolution`**. **NEO-76 landed:** frozen four-ability catalog + CI ([NEO-76 plan](../../plans/NEO-76-implementation-plan.md)). +**E5.M1 note:** Epic 5 **Slice 1** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) → [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); label **`E5.M1`**. Gig XP on defeat: **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1_CombatRulesEngine.md](E5_M1_CombatRulesEngine.md). Builds on E1.M4 cast funnel (NEO-28/31/32); extends **`POST …/ability-cast`** with **`CombatResolution`**. **NEO-76 landed:** frozen four-ability catalog + CI ([NEO-76 plan](../../plans/NEO-76-implementation-plan.md)). **NEO-77 landed:** fail-fast server load of `content/abilities/*_abilities.json` ([NEO-77 plan](../../plans/NEO-77-implementation-plan.md)). ### Epic 6 — PvP Security diff --git a/docs/plans/NEO-77-implementation-plan.md b/docs/plans/NEO-77-implementation-plan.md index 1c58f36..99b7c6a 100644 --- a/docs/plans/NEO-77-implementation-plan.md +++ b/docs/plans/NEO-77-implementation-plan.md @@ -119,7 +119,7 @@ | Test file | What it covers | |-----------|----------------| -| `server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs` | **Unit:** valid four-ability prototype catalog loads; `abilities` not array; schema violation (negative `baseDamage`, zero `cooldownSeconds`, empty `displayName`); duplicate `id` across files; wrong/missing E5M1 ids; `schemaVersion` ≠ 1; missing dir/schema; no `*_abilities.json` files; invalid JSON. **Host:** `InMemoryWebApplicationFactory` + `/health` + DI `AbilityDefinitionCatalog` count 4; `WebApplicationFactory` with empty abilities dir fails startup with actionable message. | +| `server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs` | **Unit:** valid four-ability prototype catalog loads; `abilities` not array; schema violation (negative `baseDamage`, zero `cooldownSeconds`, empty `displayName`); duplicate `id` across files; E5M1 gate failure (missing id + extra/wrong id); `schemaVersion` ≠ 1; missing dir/schema; no `*_abilities.json` files; invalid JSON. **Host:** `InMemoryWebApplicationFactory` + `/health` + DI `AbilityDefinitionCatalog` count 4; `WebApplicationFactory` with empty abilities dir fails startup with actionable message. | **Manual QA:** Skipped — no player-visible HTTP; acceptance is fully covered by AAA loader/host tests above. diff --git a/docs/reviews/2026-05-24-NEO-77.md b/docs/reviews/2026-05-24-NEO-77.md index fc438b1..e31ccf2 100644 --- a/docs/reviews/2026-05-24-NEO-77.md +++ b/docs/reviews/2026-05-24-NEO-77.md @@ -20,7 +20,7 @@ NEO-77 delivers **E5M1-02**: fail-fast startup load of `content/abilities/*_abil | [`docs/plans/E5M1-prototype-backlog.md`](../plans/E5M1-prototype-backlog.md) · **E5M1-02** | **Matches** — AC checkboxes + landed note. | | [`docs/decomposition/modules/E5_M1_CombatRulesEngine.md`](../decomposition/modules/E5_M1_CombatRulesEngine.md) | **Matches** — Status line and freeze rules reference NEO-77 server load + NEO-79 registry deferral. | | [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) | **Matches** — E5.M1 row updated with NEO-77 landed note; next backlog item NEO-79. | -| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Partially matches** — E5.M1 table **Status** **In Progress** is correct; **E5.M1 note** still cites NEO-76 only (optional bump to mention NEO-77 server load, same pattern as post–NEO-66 E3.M2 note lag). | +| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Matches** — E5.M1 table **Status** **In Progress** is correct; **E5.M1 note** cites NEO-76 + NEO-77 server load. | | [`docs/decomposition/modules/client_server_authority.md`](../decomposition/modules/client_server_authority.md) | **N/A** — server startup catalog only; no client mutation or combat resolution. | | [`scripts/validate_content.py`](../../scripts/validate_content.py) | **Matches** — C# loader mirrors `_validate_ability_catalogs` + `_prototype_e5m1_ability_gate`; sync comment on `PrototypeE5M1AbilityCatalogRules.ExpectedAbilityIds`. | | [`server/README.md`](../../server/README.md) | **Matches** — Ability catalog section (config keys, discovery, fail-fast, E5M1 parity). | @@ -35,9 +35,9 @@ None. ## Suggestions -1. **Add `folder.bru` under `bruno/neon-sprawl-server/ability-catalog/`** — Other catalog smoke folders (`item-catalog`, `recipe-catalog`, `resource-node-catalog`, `mastery-catalog`) include a `folder.bru` meta file; ability-catalog currently has only the health request. Add for Bruno collection parity. +1. ~~**Add `folder.bru` under `bruno/neon-sprawl-server/ability-catalog/`** — Other catalog smoke folders (`item-catalog`, `recipe-catalog`, `resource-node-catalog`, `mastery-catalog`) include a `folder.bru` meta file; ability-catalog currently has only the health request. Add for Bruno collection parity.~~ **Done.** Added `bruno/neon-sprawl-server/ability-catalog/folder.bru`. -2. **E5M1 gate test — extra/wrong id** — `Load_ShouldThrow_WhenE5M1GateFails` removes one ability (missing id). Plan test table also lists wrong roster; consider an AAA case that swaps in an unknown id (e.g. `prototype_extra`) to cover the “got […]” branch, not only “missing”. +2. ~~**E5M1 gate test — extra/wrong id** — `Load_ShouldThrow_WhenE5M1GateFails` removes one ability (missing id). Plan test table also lists wrong roster; consider an AAA case that swaps in an unknown id (e.g. `prototype_extra`) to cover the “got […]” branch, not only “missing”.~~ **Done.** Renamed missing-id test to `Load_ShouldThrow_WhenE5M1GateFailsWithMissingId`; added `Load_ShouldThrow_WhenE5M1GateFailsWithExtraId`. Plan test table updated. ## Nits diff --git a/server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs b/server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs index 30a7623..74041ee 100644 --- a/server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs @@ -161,7 +161,7 @@ public class AbilityDefinitionCatalogLoaderTests } [Fact] - public void Load_ShouldThrow_WhenE5M1GateFails() + public void Load_ShouldThrow_WhenE5M1GateFailsWithMissingId() { // Arrange var (_, abilitiesDir, schemaPath) = CreateTempContentLayout(); @@ -178,6 +178,23 @@ public class AbilityDefinitionCatalogLoaderTests Assert.Contains("prototype E5M1 expects exactly ability ids", ioe.Message, StringComparison.Ordinal); } + [Fact] + public void Load_ShouldThrow_WhenE5M1GateFailsWithExtraId() + { + // Arrange + var (_, abilitiesDir, schemaPath) = CreateTempContentLayout(); + var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject + ?? throw new InvalidOperationException("expected object root"); + GetAbilityRow(root, "prototype_burst")["id"] = "prototype_extra"; + WriteCatalog(abilitiesDir, root.ToJsonString()); + // Act + var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath)); + // Assert + var ioe = Assert.IsType(ex); + Assert.Contains("prototype E5M1 expects exactly ability ids", ioe.Message, StringComparison.Ordinal); + Assert.Contains("prototype_extra", ioe.Message, StringComparison.Ordinal); + } + [Fact] public void Load_ShouldThrow_WhenBaseDamageIsNegative() {