fix(simulation): PR #191 review round — n-clamp mirror, min_wl band quantization, coalescing coverage, fixture consumer
All seven Hoshe/Tyre findings addressed, none retracted:
- n-clamp/echo/staleness triangle (Tyre C1): client _clamp_window_n_mirror
(bit-for-bit twin of the server clamp, canonicalize_district_center
precedent) applied before _n is stored/sent; server test pins the
quarter n=32 -> echo 16 contract.
- min_wl band quantization (Hoshe 1/Tyre C3): quantize_min_wl_m snaps to
MIN_WL_BANDS_M {0, 32768, 16384, 8192, 4096} before cache key and echo
(design doc §5's unbounded-key fix), reusing the one true
OCTAVE_WAVELENGTHS_M array; docstrings now state the server-quantizes/
client-sends-raw split; same-band cache-sharing test.
- coalescing granularity axis (Hoshe 2): two tests pin different-
granularity requests as separate in-flight slots and same-granularity
coalescing unchanged.
- orphaned fixture (Hoshe 3): test_protocol.gd consumer decodes
atlas_response_ready_with_window.msgpack through the real IPC path and
asserts the new fields.
- atlas_window_request coverage (Hoshe 4): new test file — stale-drop on
granularity mismatch, old-server-shape defaults accepted, clamp mirror
formula + wiring. First draft's quarter-via-request_now test would have
passed for the wrong reason (request_now resets granularity by design
until T-1153) — split into formula pin + reachable-path wiring proof.
- granularity type seam (Tyre C2): field + resolver docstrings state
finer-only integer multiples with resolve_window_granularity as the
single widening point; matching contract note added to the D-226
T-1143-rulings amendment.
cargo --lib 1807/1807; goldens bit-identical; gdlint clean.
This commit is contained in:
@@ -39,6 +39,16 @@ class_name AtlasMapProtocol
|
||||
## requests quarter granularity yet (that's T-1153); this function just makes
|
||||
## it possible to ask, byte-compatible with every existing caller that
|
||||
## doesn't pass them.
|
||||
##
|
||||
## **Quantization split (PR #191 review, Hoshe 1 / Tyre C3):** `window_min_wl_m`
|
||||
## is sent HERE as a raw, unquantized value — this codec does NOT snap it to
|
||||
## the design doc §5 fixed band set. The SERVER is the one place quantization
|
||||
## happens (`serve_district_window` → `quantize_min_wl_m`, `layer_proxy.rs`):
|
||||
## it snaps every request's value to the nearest band before touching the
|
||||
## cache key or the echo, so a caller here is free to send a
|
||||
## viewport-continuous estimate (e.g. `E/C` from the rung-selection rule) —
|
||||
## don't pre-quantize client-side, it would just duplicate logic the server
|
||||
## already owns and could drift out of sync with it.
|
||||
static func encode_atlas_layer_request(
|
||||
mp,
|
||||
body_id: String,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
## T-1150 (PR #191 review, Hoshe 4): atlas_window_request.gd had NO test file
|
||||
## at all before this — direct coverage of the granularity/min_wl_m staleness
|
||||
## guard, the n-clamp mirror (Tyre C1), and the old-server-shape default
|
||||
## disposition. Follows test_atlas_window_viewer.gd's own
|
||||
## "AtlasWindowRequest — cache reuse" section conventions (same
|
||||
## instantiation pattern: `AtlasWindowRequest.new(owner_stub)`, `add_child()`
|
||||
## for the debounce Timer, hand-built response dicts) rather than
|
||||
## re-inventing a shape.
|
||||
class_name TestAtlasWindowRequest
|
||||
extends GdUnitTestSuite
|
||||
|
||||
# atlas_window_request.gd has no class_name (review #8 precedent throughout
|
||||
# this cluster) — preloaded once here, not re-load()ed per test (gdlint
|
||||
# duplicated-load).
|
||||
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
||||
|
||||
|
||||
## Build a hand-authored DistrictWindowLayer dict, granularity-aware
|
||||
## (T-1150) — mirrors test_atlas_window_viewer.gd's own _mock_window(), with
|
||||
## granularity/min_wl_m added as optional params so callers can build both
|
||||
## rungs' echo shapes with one helper.
|
||||
static func _mock_window(
|
||||
center: Vector2i, n: int = 2, granularity: int = 1, min_wl_m: int = 0
|
||||
) -> Dictionary:
|
||||
return {
|
||||
"center": [center.x, center.y],
|
||||
"n": n,
|
||||
"granularity": granularity,
|
||||
"min_wl_m": min_wl_m,
|
||||
"morphology": PackedByteArray([8, 14, 0, 1]),
|
||||
"elev_q": PackedByteArray([40, 90, 5, 60]),
|
||||
"temp_dc": [120, 95, -32768, 60],
|
||||
"moisture_q": PackedByteArray([50, 30, 90, 20]),
|
||||
"vegetation": PackedByteArray([2, 1, 6, 3]),
|
||||
"glaciation": PackedByteArray([0, 0, 1, 2]),
|
||||
}
|
||||
|
||||
|
||||
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
|
||||
return {"body_id": body_id, "status": "Ready", "district_window": window}
|
||||
|
||||
|
||||
func _make_request() -> Variant:
|
||||
var owner_stub := RefCounted.new()
|
||||
var req = auto_free(AtlasWindowRequest.new(owner_stub))
|
||||
add_child(req)
|
||||
return req
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# (a) granularity mismatch on the echo -> dropped as stale
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## The mandatory item-(a) case: request_now() asks at the default district
|
||||
## granularity (1); a response echoing granularity=4 (quarter) for the SAME
|
||||
## center/n must be dropped as stale, not accepted — a different rung's
|
||||
## derive answering a request for a different rung is exactly as stale as a
|
||||
## mismatched center (T-1150 extends §2's guard to this axis).
|
||||
func test_on_response_with_mismatched_granularity_is_dropped_as_stale() -> void:
|
||||
var req = _make_request()
|
||||
req.request_now("GJ380c", Vector2i(2, 2), 2)
|
||||
assert_bool(req.is_pending()).is_true()
|
||||
|
||||
var quarter_window: Dictionary = _mock_window(Vector2i(2, 2), 2, 4, 0)
|
||||
req.on_response(_mock_response("GJ380c", quarter_window))
|
||||
|
||||
assert_bool(req.is_pending()).override_failure_message(
|
||||
"a granularity-mismatched response must be dropped as stale, leaving the district request still pending"
|
||||
).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# (b) old-server-shape response (no granularity/min_wl_m keys) -> defaults
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## A response from a hypothetical pre-T-1150 server (or any response whose
|
||||
## district_window dict simply omits the new keys) must decode granularity
|
||||
## as district (1) and min_wl_m as 0 via the same defaulting on_response()
|
||||
## already applies — and since request_now()'s own defaults are identical,
|
||||
## the response is ACCEPTED, not treated as stale just because two keys are
|
||||
## missing.
|
||||
func test_on_response_missing_granularity_and_min_wl_defaults_and_is_accepted() -> void:
|
||||
var req = _make_request()
|
||||
req.request_now("GJ380c", Vector2i(3, 3), 2)
|
||||
assert_bool(req.is_pending()).is_true()
|
||||
|
||||
# Old-shape window: no "granularity"/"min_wl_m" keys at all.
|
||||
var old_shape_window := {
|
||||
"center": [3, 3],
|
||||
"n": 2,
|
||||
"morphology": PackedByteArray([8, 14, 0, 1]),
|
||||
"elev_q": PackedByteArray([40, 90, 5, 60]),
|
||||
"temp_dc": [120, 95, -32768, 60],
|
||||
"moisture_q": PackedByteArray([50, 30, 90, 20]),
|
||||
"vegetation": PackedByteArray([2, 1, 6, 3]),
|
||||
"glaciation": PackedByteArray([0, 0, 1, 2]),
|
||||
}
|
||||
req.on_response(_mock_response("GJ380c", old_shape_window))
|
||||
|
||||
assert_bool(req.is_pending()).override_failure_message(
|
||||
(
|
||||
"an old-server-shape response (missing granularity/min_wl_m) must "
|
||||
+ "default to district/0 and be ACCEPTED, not dropped as stale"
|
||||
)
|
||||
).is_false()
|
||||
|
||||
var received: Array = []
|
||||
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
|
||||
# Re-request the same (body, center, n) — must now be a cache hit, proving
|
||||
# on_response() actually stored the old-shape window under the
|
||||
# district/0 key, not silently discarding it.
|
||||
req.request_now("GJ380c", Vector2i(3, 3), 2)
|
||||
assert_int(received.size()).is_equal(1)
|
||||
assert_bool(req.is_pending()).is_false()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# (c) n-clamp mirror (Tyre C1) — quarter n=32 stores clamped n=16
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## **Item (c) as literally scoped by the ticket** ("the clamp-mirror from
|
||||
## item 1"): `_clamp_window_n_mirror()` reproduces the server's
|
||||
## `clamp_window_n(raw_n, granularity)` bit-for-bit, INCLUDING the quarter
|
||||
## n=32 -> 16 case — pinned directly against the static helper, independent
|
||||
## of the request/response plumbing (`request_now()` has no public
|
||||
## "request quarter" entry point today; T-1150 is struct/key plumbing only,
|
||||
## requesting quarter is T-1153's job — see the class-level docstring on
|
||||
## `_clamp_window_n_mirror()` for why calling `request_now()` at district
|
||||
## granularity can never itself exercise the quarter branch: it unconditionally
|
||||
## resets `_granularity` to district BEFORE clamping, by design, since no
|
||||
## caller can ask for quarter yet).
|
||||
func test_clamp_window_n_mirror_matches_server_formula_at_quarter_n32() -> void:
|
||||
assert_int(AtlasWindowRequest._clamp_window_n_mirror(32, 4)).is_equal(16)
|
||||
# District granularity: the per-axis cap (64) governs, matching the
|
||||
# server's clamp_window_n_district_granularity_uses_per_axis_cap test.
|
||||
assert_int(AtlasWindowRequest._clamp_window_n_mirror(640, 1)).is_equal(64)
|
||||
# Small n well under budget at quarter granularity stays unclamped,
|
||||
# matching clamp_window_n_quarter_granularity_leaves_small_n_unclamped.
|
||||
assert_int(AtlasWindowRequest._clamp_window_n_mirror(8, 4)).is_equal(8)
|
||||
|
||||
|
||||
## **Item (c), the request/response half:** `request_now()` actually WIRES
|
||||
## the mirror in (not just defines it) — a request for a district-legal but
|
||||
## per-axis-oversized `n` (e.g. 640, mirroring the server's own
|
||||
## `DISTRICT_WINDOW_MAX_N*10` oversized-request test) stores the CLAMPED
|
||||
## `_n=64`, so a server response echoing the server's OWN clamped n=64 is
|
||||
## ACCEPTED, not rejected as stale for "not matching" the raw 640 that was
|
||||
## asked for. This is the exact n-clamp/echo/staleness triangle Tyre C1
|
||||
## flagged, exercised through the reachable (district) path today; the
|
||||
## quarter-specific n=32->16 number is pinned by the formula test above since
|
||||
## no public API can drive quarter through `request_now()` yet.
|
||||
func test_oversized_n_request_stores_clamped_n_and_accepts_matching_echo() -> void:
|
||||
var req = _make_request()
|
||||
req.request_now("GJ380c", Vector2i(4, 4), 640)
|
||||
|
||||
assert_int(req._n).override_failure_message(
|
||||
(
|
||||
"request_now() must mirror the server's clamp_window_n(640, granularity=1) "
|
||||
+ "== 64 BEFORE storing _n, not store the raw requested 640"
|
||||
)
|
||||
).is_equal(64)
|
||||
assert_bool(req.is_pending()).is_true()
|
||||
|
||||
# The server's real response for this request echoes n=64 (its own
|
||||
# clamp_window_n() result) — must be ACCEPTED, not stale.
|
||||
var clamped_echo: Dictionary = _mock_window(Vector2i(4, 4), 64, 1, 0)
|
||||
req.on_response(_mock_response("GJ380c", clamped_echo))
|
||||
|
||||
assert_bool(req.is_pending()).override_failure_message(
|
||||
(
|
||||
"a response echoing the CLAMPED n=64 must be accepted, since _n was "
|
||||
+ "already clamped to 64 before the request fired"
|
||||
)
|
||||
).is_false()
|
||||
@@ -511,6 +511,27 @@ func test_decode_atlas_response_not_found() -> void:
|
||||
assert_that(resp.status).is_equal("NotFound")
|
||||
|
||||
|
||||
## PR #191 review, Hoshe 3: `atlas_response_ready_with_window.msgpack` had NO
|
||||
## consumer anywhere in client/tests — regenerated by the T-1150 `granularity`/
|
||||
## `min_wl_m` field additions but nothing decoded it through the real IPC path.
|
||||
## This is that consumer, matching the sibling `test_decode_atlas_response_*`
|
||||
## tests' style/fixture-dir convention above: full decode_atlas_layer_response()
|
||||
## round trip (not a hand-built Dictionary like test_atlas_data_delivery.gd's
|
||||
## passthrough tests), confirming `district_window.granularity`/`.min_wl_m`
|
||||
## (T-1150's two new echo fields) survive the real client decode path.
|
||||
func test_decode_atlas_response_ready_with_window() -> void:
|
||||
var bytes := _load_fixture("atlas_response_ready_with_window")
|
||||
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||
assert_that(resp).is_not_null()
|
||||
assert_that(resp.status).is_equal("Ready")
|
||||
assert_that(resp.district_window).is_not_null()
|
||||
var window: Dictionary = resp.district_window
|
||||
assert_that(window.get("center")).is_equal([10, -5])
|
||||
assert_that(int(window.get("n"))).is_equal(2)
|
||||
assert_that(int(window.get("granularity"))).is_equal(1)
|
||||
assert_that(int(window.get("min_wl_m"))).is_equal(0)
|
||||
|
||||
|
||||
func test_snapshot_is_not_decoded_as_atlas_response() -> void:
|
||||
# Disambiguation: an ObserverSnapshot has no "status" key, so the atlas
|
||||
# decoder rejects it. receive_bytes relies on this to route correctly.
|
||||
|
||||
@@ -48,6 +48,15 @@ const MAX_RETRIES: int = 20 # ~10s ceiling, matches atlas_generation_proxy.gd's
|
||||
const DEFAULT_GRANULARITY: int = AtlasWindowCache.DISTRICT_GRANULARITY
|
||||
const DEFAULT_MIN_WL_M: int = 0
|
||||
|
||||
## Mirrors server/src/atlas/layer_proxy.rs's DISTRICT_WINDOW_MAX_N /
|
||||
## WIRE_CAP_CELLS exactly (PR #191 review, Tyre C1). `_clamp_window_n_mirror()`
|
||||
## below reproduces `clamp_window_n()` bit-for-bit — the load-bearing-mirror
|
||||
## pattern `AtlasDescendGeometry.canonicalize_district_center()` already uses
|
||||
## for the server's `normalize_window_center()`. Keep both numbers in sync
|
||||
## with the server constants of the same name if either ever changes.
|
||||
const SERVER_DISTRICT_WINDOW_MAX_N: int = 64
|
||||
const SERVER_WIRE_CAP_CELLS: int = 4_096
|
||||
|
||||
var _owner = null # AtlasWindowViewer (untyped to avoid cyclic ref)
|
||||
var _cache = null # AtlasWindowCache
|
||||
var _body_id: String = ""
|
||||
@@ -86,6 +95,38 @@ func reset() -> void:
|
||||
_debounce_timer.stop()
|
||||
|
||||
|
||||
## Mirrors server/src/atlas/layer_proxy.rs's `clamp_window_n(raw_n,
|
||||
## granularity)` EXACTLY (PR #191 review, Tyre C1 — "the sharpest" finding):
|
||||
## `serve_district_window` echoes the CLAMPED `n` back in
|
||||
## `DistrictWindowLayer.n`, but `on_response()`'s staleness guard compares the
|
||||
## echo against `_n`. Without this mirror, `_n` would hold the RAW requested
|
||||
## value while the server echoes the CLAMPED one — the moment a caller
|
||||
## requests quarter (granularity=4) at n=32, the server clamps to n=16 and
|
||||
## echoes THAT, `on_response()` sees `echoed_n=16 != _n=32`, decides the
|
||||
## response is stale, and the window silently never loads (no error, no log
|
||||
## on this side — just an eternally-pending request).
|
||||
##
|
||||
## Clamping HERE, before `_n` is ever stored or sent, means `_n` already
|
||||
## equals what the server will echo — no drift between the two sides, the
|
||||
## SAME load-bearing-mirror pattern `AtlasDescendGeometry.
|
||||
## canonicalize_district_center()` uses for the server's
|
||||
## `normalize_window_center()` (see that function's docstring for the general
|
||||
## rationale: canonicalizing before the request is sent means the client's
|
||||
## held state already equals what the server will echo back).
|
||||
##
|
||||
## Formula, bit-for-bit: `n = raw_n.clamp(1, SERVER_DISTRICT_WINDOW_MAX_N)`,
|
||||
## then `n = min(n, floor(sqrt(SERVER_WIRE_CAP_CELLS) / max(granularity, 1)))`
|
||||
## — applied in that order (per-axis cap first, then the granularity-aware
|
||||
## wire-size ceiling), matching `clamp_window_n`'s own comment ("Applied AFTER
|
||||
## the per-axis clamp so a request that already satisfies
|
||||
## DISTRICT_WINDOW_MAX_N still shrinks further at granularity 4").
|
||||
static func _clamp_window_n_mirror(raw_n: int, granularity: int) -> int:
|
||||
var n: int = clampi(raw_n, 1, SERVER_DISTRICT_WINDOW_MAX_N)
|
||||
var g: int = maxi(granularity, 1)
|
||||
var cap_n: int = int(floor(sqrt(float(SERVER_WIRE_CAP_CELLS)) / float(g)))
|
||||
return mini(n, maxi(cap_n, 1))
|
||||
|
||||
|
||||
## Entry point + pan re-request: request the window centered on `center`
|
||||
## (a DistrictPos-equivalent Vector2i) for `body_id`. Cache hit -> immediate
|
||||
## synchronous window_ready emit, no network traffic at all. Cache miss ->
|
||||
@@ -97,12 +138,12 @@ func reset() -> void:
|
||||
func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEFAULT_N) -> void:
|
||||
_body_id = body_id
|
||||
_center = center
|
||||
_n = n
|
||||
_granularity = DEFAULT_GRANULARITY
|
||||
_min_wl_m = DEFAULT_MIN_WL_M
|
||||
_n = _clamp_window_n_mirror(n, _granularity) # Tyre C1 — mirror BEFORE storing/requesting
|
||||
_debounce_timer.stop() # a direct request supersedes any pending debounced one
|
||||
|
||||
var cached: Variant = _cache.get_window(body_id, center, n, _granularity, _min_wl_m)
|
||||
var cached: Variant = _cache.get_window(body_id, center, _n, _granularity, _min_wl_m)
|
||||
if cached != null:
|
||||
_pending = false
|
||||
_retries = 0
|
||||
@@ -111,7 +152,7 @@ func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEF
|
||||
|
||||
_pending = true
|
||||
_retries = 0
|
||||
SimBridge.request_atlas_layers(body_id, "Topography", center, n, _granularity, _min_wl_m)
|
||||
SimBridge.request_atlas_layers(body_id, "Topography", center, _n, _granularity, _min_wl_m)
|
||||
|
||||
|
||||
## Pan-triggered re-request (§4/§5: "150ms after the last drag-release, not
|
||||
@@ -122,9 +163,9 @@ func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEF
|
||||
func request_debounced(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEFAULT_N) -> void:
|
||||
_body_id = body_id
|
||||
_center = center
|
||||
_n = n
|
||||
_granularity = DEFAULT_GRANULARITY
|
||||
_min_wl_m = DEFAULT_MIN_WL_M
|
||||
_n = _clamp_window_n_mirror(n, _granularity) # Tyre C1 — mirror BEFORE storing/requesting
|
||||
_debounce_timer.start()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user