Files
settled-reach/client/tests/test_step_canvas_protocol.gd
T
jpmschweitzerandClaude Fable 5 d28d24fd26 feat(ui): step-canvas map component — RTT terrain + stepped zoom (D-255, T-1182)
The two-layer client rebuild per D-255(a)(b)(e), replacing the
_canvas.scale continuous-zoom model with one viewer, one path, all six
rungs:

- step_canvas_protocol.gd: StepCanvasRequest/Response codec against
  the T-1181 wire contract — incl. the discovered png_bytes subtlety
  (rmp_serde without serde_bytes emits a msgpack int-array, not bin;
  decode repacks via PackedByteArray before load_png_from_buffer) and
  the extent-echo rule (read the server-clamped extent, never assume
  the requested one).
- step_canvas/ component: transport (six-rung ladder, cursor-anchored
  scroll steps, edge-scroll/WASD pan with re-request on edge crossing,
  hard reset-to-Global), RTT terrain layer (Image.set_pixel colorize
  per the c1 measured ruling, texture.update reuse on step-cross,
  NEAREST coarse / LINEAR fine per rung), unscaled screen-space
  annotation sibling (courses + settlement markers at literal px),
  in-memory LRU cache (Tier 1; T-1183 layers the disk tiers beneath),
  request lifecycle (pending retry, staleness gate, extent echo).
- Full _canvas.scale retirement in the same change: the zoom-scaled
  canvas model, the _zs compensation family, select_rung /
  MAX_COVERAGE_M / compute_tile_grid, the orbital-mosaic-vs-window
  two-path split, _view_zoom/_canonical_fit_zoom — 10 source files
  deleted; their 14 test suites deleted with them (T-1157 dead-goldens
  rule; replacement visual-capture coverage is re-scoped T-1157).
- Surviving surfaces kept per the ticket: atlas_window_cache.gd's LRU
  shape (the ticket's named file atlas_window_tile_set.gd was the
  retiring orchestrator; the real LRU shape lives in
  atlas_window_cache.gd — cited in step_canvas_cache.gd), overlay
  colors, legend/overlay-bar chrome, AtlasViewer descend geometry.

Determinism boundary per D-255(e): the client interpolates only within
the closed server-supplied input set. 7 new gdUnit suites (164 cases)
incl. a real extent-echo bug caught by its own test during
implementation. Full client suite green (exit 0) with the live-gated
suites running against a worktree server build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 08:53:48 +02:00

213 lines
7.5 KiB
GDScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## T-1182 tests: StepCanvasRequest/StepCanvasResponse wire codec
## (step_canvas_protocol.gd, delegated via protocol.gd) — the D-255(c)
## tagged-envelope carrier. Mirrors test_atlas_data_delivery.gd's own
## encode/decode round-trip + decode_inbound classification conventions.
class_name TestStepCanvasProtocol
extends GdUnitTestSuite
# =============================================================================
# Encode
# =============================================================================
func test_encode_step_canvas_request_carries_discriminator_and_fields() -> void:
var bytes := Protocol.encode_step_canvas_request(
"GJ380c", "District", Vector2i(10, 20), Vector2i(64, 64), 512
)
assert_int(bytes.size()).is_greater(0)
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status == null).is_true()
var raw: Dictionary = decoded.value
assert_bool(raw.get("step_canvas")).is_true()
assert_str(raw.get("body_id")).is_equal("GJ380c")
assert_str(raw.get("rung")).is_equal("District")
assert_that(raw.get("center")).is_equal([10, 20])
assert_that(raw.get("extent")).is_equal([64, 64])
assert_int(raw.get("min_wl_m")).is_equal(512)
## The rung is sent as a bare string (rmp_serde's unit-variant convention),
## never a raw integer — a Global request must carry the literal tag
## "Global", matching step_canvas.rs's own StepCanvasRung enum encoding.
func test_encode_step_canvas_request_global_rung_is_bare_string() -> void:
var bytes := Protocol.encode_step_canvas_request(
"GJ380c", "Global", Vector2i.ZERO, Vector2i.ZERO, 0
)
var decoded = Messagepack.decode(bytes)
assert_str(decoded.value.get("rung")).is_equal("Global")
# =============================================================================
# Decode — status variants
# =============================================================================
func test_step_canvas_response_from_raw_decodes_ready_status() -> void:
var raw := {
"body_id": "GJ380c",
"rung": "District",
"center": [10, 20],
"extent": [64, 64],
"min_wl_m": 0,
"status": "Ready",
"canvas": null,
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
assert_str(decoded["status"]).is_equal("Ready")
assert_str(decoded["error"]).is_equal("")
assert_str(decoded["rung"]).is_equal("District")
assert_that(decoded["center"]).is_equal(Vector2i(10, 20))
assert_that(decoded["extent"]).is_equal(Vector2i(64, 64))
func test_step_canvas_response_from_raw_decodes_pending_status() -> void:
var raw := {"body_id": "GJ380c", "rung": "Chunk", "status": "Pending"}
var decoded = Protocol.step_canvas_response_from_raw(raw)
assert_str(decoded["status"]).is_equal("Pending")
assert_that(decoded["canvas"]).is_null()
func test_step_canvas_response_from_raw_decodes_error_status() -> void:
var raw := {
"body_id": "GJ380c", "rung": "Region", "status": {"Error": "no heightmap"}
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
assert_str(decoded["status"]).is_equal("Error")
assert_str(decoded["error"]).is_equal("no heightmap")
## Not a step-canvas response (no "rung" key) -> null, so decode_inbound's
## dispatch doesn't misroute a plain AtlasLayerResponse here.
func test_step_canvas_response_from_raw_returns_null_without_rung_key() -> void:
var raw := {"body_id": "GJ380c", "status": "Ready"}
assert_that(Protocol.step_canvas_response_from_raw(raw)).is_null()
# =============================================================================
# decode_inbound classification — the "rung" discriminator must win BEFORE
# the generic "status"-only AtlasLayerResponse fallback (a step-canvas
# response also carries "status").
# =============================================================================
func test_decode_inbound_classifies_step_canvas() -> void:
var raw := {"body_id": "GJ380c", "rung": "District", "status": "Pending"}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound["kind"]).is_equal("step_canvas")
func test_decode_inbound_still_classifies_plain_atlas_response() -> void:
var raw := {"body_id": "GJ380c", "status": "Ready"}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound["kind"]).is_equal("atlas")
# =============================================================================
# EncodedStepCanvas — the PNG-per-field array-of-int wire shape
# (step_canvas.rs's png_bytes: Vec<u8> with NO serde_bytes anywhere in this
# codebase serializes via serialize_seq, a msgpack ARRAY of ints, never a
# `bin` blob — decode_png_field()/_decode_encoded_canvas() must repack that
# Array into a PackedByteArray, not expect messagepack.gd's bin_8/16/32 path).
# =============================================================================
func test_decode_png_field_repacks_array_of_ints_to_packed_byte_array() -> void:
# A 1x1 all-black L8 PNG's real byte stream, as it would arrive already
# decoded off the wire (a plain Array of ints, one per byte) — using real
# PNG magic bytes so a downstream Image.load_png_from_buffer() call
# would also succeed, not just this repack step in isolation.
var png_bytes := PackedByteArray([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
var as_array: Array = []
for b in png_bytes:
as_array.append(b)
var field_raw := {"png_bytes": as_array}
var result: PackedByteArray = Protocol.step_canvas_response_from_raw(
{
"body_id": "GJ380c",
"rung": "Chunk",
"status": "Ready",
"canvas":
{
"width": 1,
"height": 1,
"morphology": field_raw,
"elev_q": {},
"temp_dc": {"values": []},
"moisture_q": {},
"vegetation": {},
"settlement_id": {"values": []},
"glaciation": {},
"flooded_q": {},
"courses": [],
"cliffs": [],
},
}
)["canvas"]["morphology"]
assert_that(result).is_equal(png_bytes)
func test_decode_encoded_canvas_passes_through_temp_dc_and_settlement_id_as_arrays() -> void:
var raw := {
"body_id": "GJ380c",
"rung": "Chunk",
"status": "Ready",
"canvas":
{
"width": 2,
"height": 1,
"morphology": {},
"elev_q": {},
"temp_dc": {"values": [120, -32768]},
"moisture_q": {},
"vegetation": {},
"settlement_id": {"values": [0, 7]},
"glaciation": {},
"flooded_q": {},
"courses": [],
"cliffs": [],
},
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
var canvas: Dictionary = decoded["canvas"]
assert_that(canvas["temp_dc"]).is_equal([120, -32768])
assert_that(canvas["settlement_id"]).is_equal([0, 7])
func test_decode_encoded_canvas_passes_through_courses_and_cliffs_unshaped() -> void:
var courses := [{"edge_id": 1, "class": 2, "points": [[0, 0], [100, 100]], "terminus": "Mouth"}]
var cliffs := [{"point": [5, 5], "channel_depth_dm": 10, "cliff_edge": true}]
var raw := {
"body_id": "GJ380c",
"rung": "Chunk",
"status": "Ready",
"canvas":
{
"width": 1,
"height": 1,
"morphology": {},
"elev_q": {},
"temp_dc": {"values": []},
"moisture_q": {},
"vegetation": {},
"settlement_id": {"values": []},
"glaciation": {},
"flooded_q": {},
"courses": courses,
"cliffs": cliffs,
},
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
var canvas: Dictionary = decoded["canvas"]
assert_that(canvas["courses"]).is_equal(courses)
assert_that(canvas["cliffs"]).is_equal(cliffs)
func test_decode_png_field_malformed_input_returns_empty_packed_byte_array() -> void:
assert_that(Protocol._scp().decode_png_field(null)).is_equal(PackedByteArray())
assert_that(Protocol._scp().decode_png_field({"png_bytes": "not an array"})).is_equal(
PackedByteArray()
)