Files
settled-reach/client/tests/test_step_canvas_protocol.gd
T
jpmschweitzerandClaude Opus 5 3ec35b87c8 fix(client): the deep rungs were flat because relief_q fell off the wire (T-1213)
`relief_q` is the one field with signal below District — elev_q's 80 m steps
quantise sub-district detail away, which is precisely why relief_q was invented.
The server has encoded it since 5eb394b36 and the terrain layer has asked for it
by name ever since. step_canvas_protocol.gd's decode dictionary never listed the
key, so `canvas.get("relief_q")` was always null and the plane arrived nowhere.
The server half of that change landed; the protocol half did not.

That is the whole reason Region and below rendered as a flat wash. Measured plane
variety at District before the fix:

    {morphology: 1, elev_q: 11, relief_q: 0, moisture_q: 25, vegetation: 3}

A 0 there means ABSENT, not constant — a distinction the capture could not make
until this commit adds it, and the reason two earlier sessions read the flatness
as a missing generator rather than a missing key.

Also spends the field properly. It drove a stipple PROBABILITY only, so a ridge
and a plain differed in dot density, which at one pixel per cell reads as noise;
and `_ruggedness()` took absf(relief_q - 50), discarding the sign the server
deliberately preserved ("a hollow and a rise are different ground... the reverse
is not recoverable"). Relief now shades continuously and signed — rises lighten,
hollows darken — UNDER the stipple rather than instead of it. Ruggedness
(unsigned) and elevation (signed) are different questions and both are worth
asking.

Ladder, before -> after (tooling/atlas-flatness, lum p1-p99):

    Global    145.69 -> 145.69   unchanged, correct: relief_q is flat 50 at
                                 orbital rungs by construction
    Region     33.59 ->  71.01   2.1x
    District   13.72 ->  77.01   5.6x
    Quarter    11.01 ->  42.56   3.9x

Structure retention Global->Quarter: 7.6% -> 29%.

NOT finished, and the ticket says so: Region now reads as heavy speckle, because
ruggedness is real data instead of an elev_q-gradient fallback and far more cells
earn a mark than the T-1194 tuning assumed; District reads as soft blobby relief,
form without directionality. Both are grammar/tuning follow-ups on a channel that
finally carries signal.

0.4.9 is a REQUIRED bump. The disk cache stores the DECODED canvas, so every
earlier entry physically lacks the field and would keep rendering flat against a
build that reads it — the first bump in this series where a warm cache is wrong
about CONTENT, not merely stale. tooling/canvas_sources.py gains
step_canvas_protocol.gd for the same reason: it decides which planes exist, the
cache stores its output, and the T-1242 gate would not have flagged this fix
while the registry stopped at ui/.../step_canvas/.

Regression cover: every protocol test passed throughout the weeks the plane was
missing, because each asserted a field it already knew about and none asserted
the SET. There is now a test walking all eight dense planes of EncodedStepCanvas,
verified by disabling the fix and watching it fail by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 15:22:50 +02:00

296 lines
11 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])
## T-1213 REGRESSION. `relief_q` shipped server-side, the terrain layer asked for
## it by name, and this decode never listed the key — so the plane the deep rungs
## exist to draw arrived nowhere for weeks, and District rendered as a flat wash.
## Every test here passed throughout, because each asserted a field it already
## knew about; none asserted the SET.
##
## So this one is written against the wire contract rather than against a list of
## fields someone remembered: every dense plane the server encodes must survive
## the decode. Adding a plane to EncodedStepCanvas without adding it here now
## fails loudly instead of rendering as "that rung is just flat".
func test_decode_encoded_canvas_carries_every_dense_plane() -> void:
# The eight PNG-per-field planes of EncodedStepCanvas (step_canvas.rs).
var planes := [
"morphology",
"elev_q",
"moisture_q",
"vegetation",
"lake_margin_q",
"relief_q",
"glaciation",
"flooded_q",
]
var canvas_in := {"width": 1, "height": 1, "temp_dc": {"values": [0]}}
for plane: String in planes:
# A one-pixel L8 PNG payload in the array-of-ints shape the wire uses.
canvas_in[plane] = {"png_bytes": [137, 80, 78, 71]}
var raw := {
"body_id": "GJ380c",
"rung": "District",
"status": "Ready",
"canvas": canvas_in,
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
var canvas: Dictionary = decoded["canvas"]
for plane: String in planes:
assert_bool(canvas.has(plane)).override_failure_message(
(
"decoded canvas is missing the '%s' plane — the server encodes it and "
+ "the renderer reads it, so a missing key here renders as a flat rung "
+ "rather than as an error (T-1213)"
)
% plane
).is_true()
assert_that(canvas[plane]).override_failure_message(
"plane '%s' decoded to null rather than bytes" % plane
).is_not_null()
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()
)
## PR #203 review (Hoshe finding 1) — the genuine msgpack `bin_8`/`bin_16`/
## `bin_32` decode path. messagepack.gd's own decoder returns
## `StreamPeerBuffer.get_partial_data()`'s `[Error, PackedByteArray]` pair for
## a bin-typed field — which passes decode_png_field()'s `is Array` guard
## just as readily as the real (today's) array-of-ints shape, so this must be
## exercised with the REAL bin-shaped decode output, not a hand-built
## Dictionary, to prove the fix actually detects it. Built by round-tripping
## a genuine PackedByteArray value through Messagepack.encode()/decode()
## directly (bypassing step_canvas_protocol.gd's own encoder, which never
## sends a PackedByteArray for png_bytes today) — this is exactly the shape
## a future serde_bytes-annotated server would produce.
func test_decode_png_field_handles_the_genuine_bin_type_shape() -> void:
var real_png_bytes := PackedByteArray([0x89, 0x50, 0x4e, 0x47, 1, 2, 3, 4])
var encoded = Messagepack.encode(real_png_bytes)
assert_that(encoded.status).is_null()
var decoded = Messagepack.decode(encoded.value)
assert_that(decoded.status).is_null()
# Sanity: this really is the [error, data] pair shape, not the plain-array
# shape the rest of this suite exercises — if messagepack.gd's own
# behavior ever changes, this assertion fails loudly instead of the test
# silently exercising the wrong code path.
var bin_shape: Variant = decoded.value
assert_bool(bin_shape is Array).is_true()
assert_int((bin_shape as Array).size()).is_equal(2)
assert_bool((bin_shape as Array)[0] is int).is_true()
assert_bool((bin_shape as Array)[1] is PackedByteArray).is_true()
var result: PackedByteArray = Protocol._scp().decode_png_field({"png_bytes": bin_shape})
assert_that(result).override_failure_message(
"bin-shaped png_bytes must decode to the REAL inner bytes, not silently"
+ " collapse to [0, 0] via PackedByteArray([int, PackedByteArray]) coercion"
).is_equal(real_png_bytes)