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>
213 lines
7.5 KiB
GDScript
213 lines
7.5 KiB
GDScript
## 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()
|
||
)
|