fix(client): cold-start round 2 — DERIVING TERRAIN state, legend re-fit, release server for make atlas

Item 1: while zero tiles have arrived, the viewer draws a centered
screen-space 'DERIVING TERRAIN…' label (text_dim role, no new hue),
dropping the instant the first tile lands — a cold wait now reads as
loading, not broken. New has_any_tile_arrived() predicate (distinct
from has_pending_tiles(): both true mid-arrival, tested exactly there).

Item 2: legend re-fit root-caused empirically, two plausible fixes
disproven by trace before the real one: a manually-positioned Control's
size NEVER tracks a shrinking minimum in this parenting shape, and
RichTextLabel.fit_content reports degenerate minimums until laid out at
real width once — so reset must be DEFERRED and run after refill, not
inside clear(). ImplantPanel.reset_to_content_size() (call_deferred),
wired into both legend refresh()es. The load-bearing test compares
size.y to get_minimum_size().y — a size-to-size comparison passed
trivially with both numbers equally stuck (caught on first draft).

Item 3: make atlas now builds the RELEASE server and passes
SR_SERVER_BIN (a cold DEBUG server delivers zero tiles for >10s on a
new body — live-measured — vs 210ms warm; release serves cold in well
under a second). atlas_standalone._server_binary_path() honors the env
override per the SR_PORT two-tier precedent, debug path unchanged
when unset.

All fixes revert-verified; nine suites green collateral-checked;
gdlint clean.
This commit is contained in:
2026-07-22 19:51:12 +02:00
parent d4526e51ff
commit dd13760d62
11 changed files with 489 additions and 120 deletions
+154 -4
View File
@@ -1,15 +1,17 @@
## PR #192 cold-start dossier — coordinator's live repro against a freshly-
## spawned (cold) server (`make atlas` shape, first AnalyzeBody taking
## seconds): BUG 1 (tile-mosaic paint never resolving) and the legend-
## stacking half of BUG 2. Split out of test_atlas_zoom_ladder.gd purely for
## file-length reasons (gdlint max-file-lines) — same instantiation/mock-
## response conventions as that file, not a different testing philosophy.
## seconds): BUG 1 (tile-mosaic paint never resolving), the legend-stacking
## half of BUG 2, and BUG 3 (the "DERIVING TERRAIN…" pending-state label,
## round 2). Split out of test_atlas_zoom_ladder.gd purely for file-length
## reasons (gdlint max-file-lines) — same instantiation/mock-response
## conventions as that file, not a different testing philosophy.
## RegionalScreen's own re-entry-guard half of BUG 2 is covered separately
## in test_regional_screen.gd (a different layer — nav, not the viewer).
class_name TestAtlasColdStart
extends GdUnitTestSuite
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## Dudley's WINDOW_GRANULARITY_REGION_KEY sentinel — mirrors
## test_atlas_zoom_ladder.gd's own constant (see that file's doc for why the
@@ -159,3 +161,151 @@ func test_legend_refresh_is_idempotent_against_same_frame_re_entry() -> void:
+ " free immediately, not defer via queue_free()"
) % [baseline_count, after_count]
).is_equal(baseline_count)
## PR #192 cold-start round 2: the children-count fix above is necessary but
## not sufficient — the coordinator's live scene dump showed the CONTAINER
## itself measured 260x2343px even after that fix landed. Root cause turned
## out to be BROADER than "only after stacking": this panel is manually
## positioned under AtlasWindowViewer (not inside a parent Container), so
## `size` NEVER tracks a shrinking `get_minimum_size()` on its own at
## all — confirmed directly (instrumented and reverted) that even a
## completely FRESH, never-refreshed-twice legend shows `size` frozen at
## whatever it happened to be on its very first measurement, while
## get_minimum_size() reports the correct value the whole time. The
## regression here therefore compares `size.y` against the RELIABLE ground
## truth (`get_minimum_size().y`, confirmed correct in every trace) rather
## than an earlier `size.y` snapshot — comparing size-to-size would pass
## trivially if BOTH numbers were equally stuck at the same stale value,
## which is exactly what silently happened during earlier drafts of this
## test. Reproduces the stacking shape (N same-frame refresh() calls) for
## realism, matching the coordinator's own trigger — the coordinator's
## acceptance bar: "after N refreshes the panel rect height must be within
## one legend's height" (of the CORRECT single-legend height, i.e. the
## settled minimum size).
func test_legend_panel_shrinks_back_after_a_stacking_window() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
# Reproduce the stacking window directly (same-frame repeat refresh()
# calls, matching the pre-fix trigger shape) — this drives the panel's
# minimum size up the same way N repeat enter_orbital() calls did live.
for _i in range(10):
v._legend_panel.refresh()
await get_tree().process_frame
await get_tree().process_frame
# A further, ordinary refresh() (the kind every real rung change already
# triggers) must leave the panel within one legend's height.
v._legend_panel.refresh()
# reset_to_content_size() is deferred (see its own doc — get_minimum_size()
# is momentarily wrong for RichTextLabel.fit_content children until the
# panel's real width has been laid out once) — a real frame must elapse
# for the deferred reset_size() call to actually run.
await get_tree().process_frame
await get_tree().process_frame
var one_legend_height: float = v._legend_panel.get_minimum_size().y
assert_float(one_legend_height).override_failure_message(
"sanity: a single settled legend must have a real, non-zero measured minimum height"
).is_greater(0.0)
assert_float(v._legend_panel.size.y).override_failure_message(
(
"after a stacking window, the legend panel's rect height (%.1f) must"
+ " shrink back to within one legend's height (%.1f, the panel's own"
+ " correctly-settled get_minimum_size()) — reset_to_content_size()"
+ " must actually collapse the Control back down, not just hold onto"
+ " its previously-grown size"
) % [v._legend_panel.size.y, one_legend_height]
).is_less_equal(one_legend_height + 1.0) # +1.0: float rounding slack
# =============================================================================
# BUG 3 (round 2) — "DERIVING TERRAIN…" label: the subtle per-tile
# COLOR_BORDER_FADE wash alone was invisible in a live cold capture. The
# viewer draws an unmistakable centered label while ZERO tiles have arrived,
# dropping it the instant even one lands.
# =============================================================================
## The viewer must show the label exactly while is_tile_mode() is true AND
## has_any_tile_arrived() is false — the coordinator's "ZERO tiles have
## arrived" trigger condition, pinned directly against real tile-set state
## (not a mock) via a real enter_orbital() on a tiling body. Also exercises
## _draw()'s ACTUAL dispatch to _draw_deriving_terrain_label() through a
## real frame (queue_redraw() + await process_frame, matching the
## _CountingOverlay spy pattern from the BUG 1 self-heal tests above) —
## proving the draw call itself is reachable and doesn't error, not just
## that the underlying predicate is correct.
func test_deriving_terrain_label_condition_true_before_any_tile_arrives() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — needs tiling
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
assert_bool(v._tile_set.has_any_tile_arrived()).override_failure_message(
"sanity: entry must leave every tile unarrived before any response arrives"
).is_false()
v.queue_redraw()
await get_tree().process_frame
## The instant even ONE tile lands, the label's own gate condition must flip
## off — per-tile washes alone are the right treatment once real content is
## visibly filling in (coordinator: "dropping to per-tile washes once the
## first tile lands").
func test_deriving_terrain_label_condition_false_after_one_tile_arrives() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
var tile_set = v.get_tile_set()
var first_tile: Dictionary = tile_set.get_tiles()[0]
var window: Dictionary = _mock_window(first_tile["center"])
window["granularity_v2"] = "Region"
window["granularity"] = SERVER_LEGACY_GRANULARITY_REGION_SENTINEL
window["n"] = AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(tile_set.has_any_tile_arrived()).override_failure_message(
"the label's own gate condition (NOT has_any_tile_arrived()) must flip"
+ " false the instant a single tile lands, dropping the label"
).is_true()
## Single-window mode (District/Quarter/small-body Region, not tile mode)
## never shows this label at all — it's a mosaic-specific cue for the
## "whole orbital rest state is still deriving" case, not every wait state
## (the single-window path already has its own COLOR_BORDER_FADE treatment,
## unchanged by this round).
func test_deriving_terrain_label_never_applies_outside_tile_mode() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: a District-rung enter() must never be tile mode"
).is_false()
## centered_label_baseline() pure geometry: the X component is the
## VIEWPORT-CENTERED text block's left-anchor-adjusted X (viewport center
## minus half the text width — draw_string() itself does the final
## horizontal centering from there via HORIZONTAL_ALIGNMENT_CENTER, this
## only sets up where that alignment measures from); the Y component sits
## at viewport-center (a draw_string() baseline is the text's OWN vertical
## center here, by construction: center.y - text.y/2 + text.y/2 == center.y).
func test_centered_label_baseline_centers_a_symmetric_case() -> void:
var viewport_size := Vector2(1000.0, 800.0)
var text_size := Vector2(200.0, 40.0)
var baseline: Vector2 = AtlasWindowGeometry.centered_label_baseline(viewport_size, text_size)
assert_that(baseline).is_equal(Vector2(400.0, 400.0))
## A zero-size viewport (never laid out yet) must not crash — degenerate
## input, not a real scenario, but the function must stay total.
func test_centered_label_baseline_zero_viewport_does_not_crash() -> void:
var baseline: Vector2 = AtlasWindowGeometry.centered_label_baseline(
Vector2.ZERO, Vector2(100.0, 20.0)
)
assert_that(baseline).is_equal(Vector2(-50.0, 0.0))
+54
View File
@@ -89,6 +89,60 @@ func test_attach_port_whitespace_env_falls_back_to_default() -> void:
assert_that(s._attach_port(" ")).is_equal(9876)
# =============================================================================
# _server_binary_path — SR_SERVER_BIN env override, else the debug build path
# (same two-tier shape as _attach_port's SR_PORT). PR #192 cold-start round 2:
# `make atlas` now builds RELEASE and passes SR_SERVER_BIN so a spawned cold
# server's first AnalyzeBody is sub-second instead of a debug build's
# multi-second derivation.
# =============================================================================
func test_server_binary_path_unset_env_falls_back_to_debug_path() -> void:
var s = _script()
assert_that(s._server_binary_path("", "/project")).is_equal(
"/project/../server/target/debug/settled-reach-server"
)
func test_server_binary_path_relative_env_is_joined_to_project_root() -> void:
# The exact shape make atlas's own SR_SERVER_BIN value takes: relative to
# the repo root, not to client/'s res:// tree — matching the Makefile's
# own "server/target/release/settled-reach-server" string.
var s = _script()
assert_that(
s._server_binary_path("server/target/release/settled-reach-server", "/project")
).is_equal("/project/../server/target/release/settled-reach-server")
func test_server_binary_path_absolute_env_is_used_verbatim() -> void:
var s = _script()
assert_that(s._server_binary_path("/opt/custom/settled-reach-server", "/project")).is_equal(
"/opt/custom/settled-reach-server"
)
## The acceptance shape the coordinator asked for verbatim: "env set -> that
## path used; unset -> debug path unchanged" — via the REAL OS.get_environment
## read (zero override_env arg), not the injectable param the tests above use
## for isolation. OS.set_environment() is the standard gdUnit4-safe way to
## drive a real env var for the duration of one test without touching the
## actual process environment permanently.
func test_server_binary_path_real_env_set_overrides_debug_path() -> void:
var s = _script()
OS.set_environment("SR_SERVER_BIN", "server/target/release/settled-reach-server")
var result: String = s._server_binary_path("", "/project")
OS.set_environment("SR_SERVER_BIN", "")
assert_that(result).is_equal("/project/../server/target/release/settled-reach-server")
func test_server_binary_path_real_env_unset_leaves_debug_path_unchanged() -> void:
var s = _script()
OS.set_environment("SR_SERVER_BIN", "")
var result: String = s._server_binary_path("", "/project")
assert_that(result).is_equal("/project/../server/target/debug/settled-reach-server")
# =============================================================================
# _parse_listening_line — main.rs's "LISTENING:{port}" stdout signal
# (server/src/main.rs) parsed to an int, or -1 if the line doesn't match.
@@ -142,6 +142,79 @@ func test_all_tiles_arriving_flips_fully_arrived() -> void:
).is_true()
# =============================================================================
# has_pending_tiles() / has_any_tile_arrived() — PR #192 cold-start dossier.
# Distinct predicates (both can be true at once, mid-arrival): the viewer's
# self-healing redraw (BUG 1) polls has_pending_tiles(); the "DERIVING
# TERRAIN…" label (BUG 3) polls has_any_tile_arrived() to know when to drop.
# =============================================================================
func test_has_pending_tiles_true_immediately_after_enter() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
assert_bool(ts.has_pending_tiles()).override_failure_message(
"every tile is unarrived right after enter() — has_pending_tiles() must be true"
).is_true()
func test_has_pending_tiles_false_once_every_tile_has_arrived() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
for tile: Dictionary in ts.get_tiles():
var window: Dictionary = _mock_window(
tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(ts.has_pending_tiles()).is_false()
func test_has_pending_tiles_true_while_only_some_tiles_have_arrived() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var first_tile: Dictionary = ts.get_tiles()[0]
var window: Dictionary = _mock_window(
first_tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(ts.has_pending_tiles()).override_failure_message(
"5 of 6 tiles still unarrived — has_pending_tiles() must stay true"
).is_true()
func test_has_any_tile_arrived_false_immediately_after_enter() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
assert_bool(ts.has_any_tile_arrived()).override_failure_message(
"nothing has arrived right after enter() — has_any_tile_arrived() must be false"
).is_false()
## The exact mid-arrival case both predicates must agree can coexist: one
## tile in, five still pending — the point the "DERIVING TERRAIN…" label
## must drop (has_any_tile_arrived() flips true) while the self-heal must
## keep redrawing (has_pending_tiles() stays true).
func test_has_any_tile_arrived_true_after_a_single_tile_lands() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var first_tile: Dictionary = ts.get_tiles()[0]
var window: Dictionary = _mock_window(
first_tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(ts.has_any_tile_arrived()).is_true()
assert_bool(ts.has_pending_tiles()).override_failure_message(
"sanity: the other 5 tiles are still pending at the same moment"
).is_true()
func test_has_any_tile_arrived_false_for_an_empty_tile_set() -> void:
var ts = _make_tile_set()
assert_bool(ts.has_any_tile_arrived()).override_failure_message(
"an empty tile set (never entered) must not vacuously report arrival"
).is_false()
## A response for a body the tile set is NOT currently showing (a stale
## response from a body the player has since navigated away from) must not
## be adopted by any tile — the SAME body_id staleness guard every other