NEO-72: fix HTTP client header types and strengthen tests.

Use PackedStringArray for request_completed headers so handlers run;
assert inventory payload shape and display_name_for after sync per review.
pull/106/head
VinPropane 2026-05-24 18:43:39 -04:00
parent 15fcc09493
commit e7a61a1b3f
6 changed files with 32 additions and 38 deletions

View File

@ -203,7 +203,7 @@ On **Linux** (including GitHub Actions), the path must be **`gdUnit4`** with a c
**CI:** The **GDScript** workflow fails the job if Godot prints **`SCRIPT ERROR:`** or **`ERROR: Failed to load script`** while running GdUnit. GdUnit alone can still exit **0** when a test file fails to parse (that suite is skipped); the workflow guards against that.
**Scope:** Unit tests cover **`player.gd`**, **`player_locomotion_wasd.gd`** (pure WASD seam helpers), **`position_authority_client.gd`**, **`isometric_follow_camera.gd`** (eye math + effective zoom distance), **`camera_state.gd`**, and **`zoom_band_config.gd`**. **`main.gd`** and scene wiring are **not** automated here—use manual checks above.
**Scope:** Unit tests cover **`player.gd`**, **`player_locomotion_wasd.gd`** (pure WASD seam helpers), **`position_authority_client.gd`**, **`inventory_client.gd`**, **`item_definitions_client.gd`** (NEO-72), **`isometric_follow_camera.gd`** (eye math + effective zoom distance), **`camera_state.gd`**, and **`zoom_band_config.gd`**. **`main.gd`** and scene wiring are **not** automated here—use manual checks above.
**Camera zoom:** Input actions **`camera_zoom_in`** / **`camera_zoom_out`** (mouse wheel, **`=`** / **`-`**, keypad **+** / ****) are defined in **`project.godot`**. On layouts where **`+`** requires **Shift**, remap or add a binding under **Project → Project Settings → Input Map** if needed.

View File

@ -59,7 +59,7 @@ static func parse_inventory_json(text: String) -> Variant:
func _on_request_completed(
result: int, response_code: int, _headers: PackedByteArray, body: PackedByteArray
result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray
) -> void:
_busy = false
if result != HTTPRequest.RESULT_SUCCESS:

View File

@ -75,7 +75,7 @@ func _base_root() -> String:
func _on_request_completed(
result: int, response_code: int, _headers: PackedByteArray, body: PackedByteArray
result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray
) -> void:
_busy = false
if result != HTTPRequest.RESULT_SUCCESS:

View File

@ -4,6 +4,12 @@ extends GdUnitTestSuite
const InventoryClient := preload("res://scripts/inventory_client.gd")
var _inventory_capture: Dictionary = {}
func _capture_inventory(snapshot: Dictionary) -> void:
_inventory_capture = snapshot
class MockHttpTransport:
extends Node
@ -64,14 +70,19 @@ func test_parse_inventory_json_empty_grid() -> void:
func test_request_sync_emits_inventory_received() -> void:
# Arrange
_inventory_capture = {}
var transport := MockHttpTransport.new()
transport.body_json = _empty_inventory_json()
var c := _make_client(transport)
c.connect("inventory_received", Callable(self, "_capture_inventory"))
monitor_signals(c)
# Act
c.call("request_sync_from_server")
# Assert
assert_signal(c).is_emitted("inventory_received")
assert_that(_inventory_capture.has("bagSlots")).is_true()
assert_that((_inventory_capture["bagSlots"] as Array).size()).is_equal(24)
assert_that((_inventory_capture["equipmentSlots"] as Array).size()).is_equal(1)
assert_that(transport.last_url).contains("/inventory")

View File

@ -4,6 +4,11 @@ extends GdUnitTestSuite
const ItemDefsClient := preload("res://scripts/item_definitions_client.gd")
const _SAMPLE_BODY_JSON := (
'{"schemaVersion":1,"items":[{"id":"scrap_metal_bulk",'
+ '"displayName":"Scrap Metal (Bulk)","inventorySlotKind":"bag"}]}'
)
class MockHttpTransport:
extends Node
@ -69,43 +74,10 @@ func test_build_definitions_map_extracts_display_name() -> void:
assert_that(built["scrap_metal_bulk"]["displayName"]).is_equal("Scrap Metal (Bulk)")
func test_parse_mock_transport_json_body() -> void:
# Arrange
var json := (
'{"schemaVersion":1,"items":[{"id":"scrap_metal_bulk",'
+ '"displayName":"Scrap Metal (Bulk)","inventorySlotKind":"bag"}]}'
)
# Act
var parsed: Variant = JSON.parse_string(json)
var built: Dictionary = ItemDefsClient.build_definitions_map(parsed)
# Assert
assert_that(built.has("scrap_metal_bulk")).is_true()
func test_handler_body_bytes_parse_builds_map() -> void:
# Arrange
var body_json := (
'{"schemaVersion":1,"items":[{"id":"scrap_metal_bulk",'
+ '"displayName":"Scrap Metal (Bulk)","inventorySlotKind":"bag"}]}'
)
var body_bytes: PackedByteArray = body_json.to_utf8_buffer()
# Act
var text: String = body_bytes.get_string_from_utf8()
var parsed: Variant = JSON.parse_string(text)
var built: Dictionary = ItemDefsClient.build_definitions_map(parsed)
# Assert
assert_that(int((parsed as Dictionary).get("schemaVersion", -1))).is_equal(1)
assert_that(built.has("scrap_metal_bulk")).is_true()
func test_request_sync_hits_definitions_endpoint_and_emits() -> void:
# Arrange
var transport := MockHttpTransport.new()
var body_json := (
'{"schemaVersion":1,"items":[{"id":"scrap_metal_bulk",'
+ '"displayName":"Scrap Metal (Bulk)","inventorySlotKind":"bag"}]}'
)
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, body_json)
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _SAMPLE_BODY_JSON)
var c := _make_client(transport)
monitor_signals(c)
# Act
@ -115,6 +87,17 @@ func test_request_sync_hits_definitions_endpoint_and_emits() -> void:
assert_signal(c).is_emitted("definitions_ready")
func test_display_name_for_after_request_sync() -> void:
# Arrange
var transport := MockHttpTransport.new()
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _SAMPLE_BODY_JSON)
var c := _make_client(transport)
# Act
c.call("request_sync_from_server")
# Assert
assert_that(c.call("display_name_for", "scrap_metal_bulk")).is_equal("Scrap Metal (Bulk)")
func test_http_error_does_not_emit_definitions_ready() -> void:
# Arrange
var transport := MockHttpTransport.new()

View File

@ -50,7 +50,7 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen
**E3.M1 note:** Epic 3 **Slice 2** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) → [NEO-64](https://linear.app/neon-sprawl/issue/NEO-64); label **`E3.M1`**. See [E3M1-prototype-backlog.md](../../plans/E3M1-prototype-backlog.md), [E3_M1_ResourceNodeAndGatherLoop.md](E3_M1_ResourceNodeAndGatherLoop.md). **NEO-57** (content + CI), **NEO-58** (server fail-fast load), **NEO-59** (`IResourceNodeDefinitionRegistry` + DI), **NEO-60** (`GET /game/world/resource-node-definitions`), **NEO-61** (`IResourceNodeInstanceStore` + depletion operations, `V006`), **NEO-62** (`GatherOperations` + `GatherResult`), **NEO-63** (interact → gather engine + four registry anchors + Bruno), **NEO-64** (comment-only **`resource_gathered`** / **`gather_node_depleted`** telemetry hook sites in `GatherOperations.TryGather`) — E3M1-01**E3M1-08** complete; register row **Ready**.
**E3.M3 note:** Epic 3 **Slice 1** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) → [NEO-56](https://linear.app/neon-sprawl/issue/NEO-56); label **`E3.M3`**. See [E3M3-prototype-backlog.md](../../plans/E3M3-prototype-backlog.md), [E3_M3_ItemizationAndInventorySchema.md](E3_M3_ItemizationAndInventorySchema.md). **NEO-50** (content + CI), **NEO-51** (server fail-fast load), **NEO-52** (`IItemDefinitionRegistry` + DI), **NEO-53** (`GET /game/world/item-definitions`), **NEO-54** (inventory store + stack/slot rules engine), **NEO-55** (`GET`/`POST /game/players/{id}/inventory`), and **NEO-56** (comment-only `item_created` / `inventory_transfer_denied` telemetry hook sites in `PlayerInventoryOperations`) — Epic 3 Slice 1 backlog **E3M3-01****E3M3-07** complete; register row **In Progress** until Slice 2+; later slices update the alignment table as they land.
**E3.M3 note:** Epic 3 **Slice 1** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) → [NEO-56](https://linear.app/neon-sprawl/issue/NEO-56); label **`E3.M3`**. See [E3M3-prototype-backlog.md](../../plans/E3M3-prototype-backlog.md), [E3_M3_ItemizationAndInventorySchema.md](E3_M3_ItemizationAndInventorySchema.md). **NEO-50** (content + CI), **NEO-51** (server fail-fast load), **NEO-52** (`IItemDefinitionRegistry` + DI), **NEO-53** (`GET /game/world/item-definitions`), **NEO-54** (inventory store + stack/slot rules engine), **NEO-55** (`GET`/`POST /game/players/{id}/inventory`), and **NEO-56** (comment-only `item_created` / `inventory_transfer_denied` telemetry hook sites in `PlayerInventoryOperations`) — Epic 3 Slice 1 backlog **E3M3-01****E3M3-07** complete; **NEO-72** (client inventory HUD — `inventory_client.gd`, `item_definitions_client.gd`, `InventoryLabel`; [`NEO-72` manual QA](../../manual-qa/NEO-72.md)) — Epic 3 Slice 5 **E3S5-01** client landed; register row **In Progress** until Slice 2+; later slices update the alignment table as they land.
**E3.M2 note:** Epic 3 **Slice 3** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) → [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71); label **`E3.M2`**. See [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md), [E3_M2_RefinementAndRecipeExecution.md](E3_M2_RefinementAndRecipeExecution.md). **NEO-42** (refine XP prep helper) is **Done**; **NEO-65****NEO-71** complete — **NEO-71** (comment-only **`item_crafted`** / **`craft_failed`** telemetry hook sites in `CraftOperations.TryCraft`) — E3M2-01**E3M2-07** complete; register row **Ready**. Upstream: E3.M1 **Ready**, E3.M3 inventory landed.