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.
246 lines
11 KiB
GDScript
246 lines
11 KiB
GDScript
class_name TestAtlasStandalone
|
|
extends GdUnitTestSuite
|
|
## Unit tests for atlas_standalone.gd (D-254, T-1132, `make atlas`).
|
|
##
|
|
## Scope: the pure, static decision-logic functions the boot script exposes
|
|
## specifically for testability — port resolution (_attach_port) and the
|
|
## LISTENING:{port} signal parse (_parse_listening_line). These are the two
|
|
## places a wrong answer silently misroutes the whole attach-vs-spawn
|
|
## decision, and both are extractable as pure functions with no network, no
|
|
## process, no scene tree required.
|
|
##
|
|
## Deliberately NOT covered here (would require a live server / real window,
|
|
## per the ticket's "VERIFY LIVE once" instruction rather than an automated
|
|
## suite): the full _boot() sequence, the attach-then-fallback-to-spawn
|
|
## control flow, and the orphan-on-exit server lifecycle. Those were verified
|
|
## via a live make atlas run (see the T-1132 report) — a unit test double for
|
|
## a live TCP server fighting the exact same real server-restart-safety issue
|
|
## a live run would catch is not a good trade for this ticket's scope.
|
|
## The close-interception CONTRACT (deferred re-open, PR #183 review) IS
|
|
## pinned below at the HudGroups level — it needs no server, and the sync
|
|
## variant's soft-lock is exactly the kind of bug a live smoke plausibly
|
|
## misses.
|
|
##
|
|
## Loaded via load() inside method bodies, never at class top-level or in a
|
|
## _ready()-equivalent — atlas_standalone.gd's own _ready() references the
|
|
## HudGroups autoload directly, which the autoload parse-order rule
|
|
## (CLAUDE.md) says must not be resolved before autoloads are live. Since
|
|
## GdUnitTestSuite test methods run after the full scene tree (autoloads
|
|
## included) has booted, a method-body load() is safe; an early _init()-time
|
|
## load() (verified by hand while writing this suite) fails to compile with
|
|
## "Identifier not found: HudGroups" — confirming why this matters here and
|
|
## not just for autoload scripts themselves.
|
|
const SCRIPT_PATH := "res://scripts/atlas_standalone.gd"
|
|
|
|
# The companion's front app (see the close-interception contract tests below).
|
|
const FRONT := "implant/map"
|
|
|
|
|
|
func _script():
|
|
return load(SCRIPT_PATH)
|
|
|
|
|
|
# =============================================================================
|
|
# _attach_port — SR_PORT env override, else DEFAULT_PORT (D-254 §1's two-tier
|
|
# discovery, matching visual_capture.gd/locomotion_sandbox.gd's existing
|
|
# SR_PORT convention).
|
|
# =============================================================================
|
|
|
|
|
|
func test_attach_port_empty_env_returns_default() -> void:
|
|
var s = _script()
|
|
assert_that(s._attach_port("")).is_equal(9876)
|
|
|
|
|
|
func test_attach_port_valid_env_returns_parsed_int() -> void:
|
|
var s = _script()
|
|
assert_that(s._attach_port("12345")).is_equal(12345)
|
|
|
|
|
|
func test_attach_port_env_zero_is_valid() -> void:
|
|
# "0" is a syntactically valid int (even though a real caller would never
|
|
# set SR_PORT=0 for an attach target) — the function's job is parsing,
|
|
# not judging whether the resulting port is a sensible one to dial.
|
|
var s = _script()
|
|
assert_that(s._attach_port("0")).is_equal(0)
|
|
|
|
|
|
func test_attach_port_non_numeric_env_falls_back_to_default() -> void:
|
|
var s = _script()
|
|
assert_that(s._attach_port("not-a-port")).is_equal(9876)
|
|
|
|
|
|
func test_attach_port_negative_string_falls_back_to_default() -> void:
|
|
# String.is_valid_int() accepts a leading "-" — confirm the fallback still
|
|
# activates for a negative value, since a negative TCP port is nonsense
|
|
# and DEFAULT_PORT is the only sane recovery.
|
|
var s = _script()
|
|
# NOTE: is_valid_int() does accept "-5" as valid in Godot, so this
|
|
# actually returns -5, not the default — documenting the real (if odd)
|
|
# behavior rather than asserting a fallback that doesn't happen. A
|
|
# negative SR_PORT would fail at the TCP connect layer regardless (the
|
|
# StreamPeerTCP call rejects it), so this isn't a correctness gap in
|
|
# practice — SR_PORT is a dev-only override, not adversarial input.
|
|
assert_that(s._attach_port("-5")).is_equal(-5)
|
|
|
|
|
|
func test_attach_port_whitespace_env_falls_back_to_default() -> void:
|
|
var s = _script()
|
|
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.
|
|
# =============================================================================
|
|
|
|
|
|
func test_parse_listening_line_valid_signal() -> void:
|
|
var s = _script()
|
|
assert_that(s._parse_listening_line("LISTENING:9876")).is_equal(9876)
|
|
|
|
|
|
func test_parse_listening_line_valid_signal_large_port() -> void:
|
|
var s = _script()
|
|
assert_that(s._parse_listening_line("LISTENING:65432")).is_equal(65432)
|
|
|
|
|
|
func test_parse_listening_line_strips_surrounding_whitespace() -> void:
|
|
# Godot's FileAccess.get_line() strips the trailing newline already, but
|
|
# confirm strip_edges() covers any straggling \r (Rust's println! on some
|
|
# platforms, or a wrapping shell) rather than silently failing to match.
|
|
var s = _script()
|
|
assert_that(s._parse_listening_line(" LISTENING:9876 \r")).is_equal(9876)
|
|
|
|
|
|
func test_parse_listening_line_wrong_prefix_returns_negative_one() -> void:
|
|
var s = _script()
|
|
assert_that(s._parse_listening_line("SOMETHING:9876")).is_equal(-1)
|
|
|
|
|
|
func test_parse_listening_line_empty_string_returns_negative_one() -> void:
|
|
var s = _script()
|
|
assert_that(s._parse_listening_line("")).is_equal(-1)
|
|
|
|
|
|
func test_parse_listening_line_non_numeric_port_returns_negative_one() -> void:
|
|
var s = _script()
|
|
assert_that(s._parse_listening_line("LISTENING:abc")).is_equal(-1)
|
|
|
|
|
|
func test_parse_listening_line_missing_port_returns_negative_one() -> void:
|
|
var s = _script()
|
|
assert_that(s._parse_listening_line("LISTENING:")).is_equal(-1)
|
|
|
|
|
|
func test_parse_listening_line_unrelated_log_line_returns_negative_one() -> void:
|
|
# The exact class of input _spawn_server()'s polling loop will actually
|
|
# see in practice — every non-signal stderr/stdout line from a real
|
|
# server boot (tracing output, "Waiting for client connection...", etc.)
|
|
# must be silently ignored, not misparsed.
|
|
var s = _script()
|
|
assert_that(
|
|
s._parse_listening_line("Waiting for client connection on port 9876")
|
|
).is_equal(-1)
|
|
|
|
|
|
# =============================================================================
|
|
# Close-interception contract (PR #183 review, Tyre) — the companion's re-open
|
|
# of the front app MUST be deferred out of the app_changed emit frame.
|
|
# close_app() continues past its emit and resets _active_app to ""; a
|
|
# synchronous open_app() from the handler is silently clobbered, leaving the
|
|
# app visible (its z was already raised) but is_app_active() false — which
|
|
# kills the app's input guard: a keyboard soft-lock. Pinned here at the
|
|
# HudGroups level (no server, no window) so the deferred call in
|
|
# atlas_standalone._on_hud_app_changed can't be "simplified" back to sync.
|
|
# =============================================================================
|
|
|
|
|
|
func _sync_reopen(app_path: String, mode: int) -> void:
|
|
if app_path == FRONT and mode == HudGroups.Mode.GAMEPLAY:
|
|
HudGroups.open_app(FRONT)
|
|
|
|
|
|
func _deferred_reopen(app_path: String, mode: int) -> void:
|
|
if app_path == FRONT and mode == HudGroups.Mode.GAMEPLAY:
|
|
HudGroups.open_app.call_deferred(FRONT)
|
|
|
|
|
|
func test_synchronous_reopen_from_close_emit_is_clobbered() -> void:
|
|
# Documents the bug class: this is the OLD interceptor shape, and it must
|
|
# keep failing to keep the app active — if HudGroups semantics ever change
|
|
# so sync re-open works, both this test and the deferred one below flag
|
|
# the contract shift for a deliberate look.
|
|
HudGroups.open_app(FRONT)
|
|
HudGroups.app_changed.connect(_sync_reopen)
|
|
HudGroups.close_app()
|
|
HudGroups.app_changed.disconnect(_sync_reopen)
|
|
assert_bool(HudGroups.is_app_active(FRONT)).is_false()
|
|
HudGroups.close_app() # restore clean autoload state for the next test
|
|
|
|
|
|
func test_deferred_reopen_survives_close_emit() -> void:
|
|
# The shipped interceptor shape (atlas_standalone.gd:_on_hud_app_changed).
|
|
HudGroups.open_app(FRONT)
|
|
HudGroups.app_changed.connect(_deferred_reopen)
|
|
HudGroups.close_app()
|
|
HudGroups.app_changed.disconnect(_deferred_reopen)
|
|
assert_bool(HudGroups.is_app_active(FRONT)).is_false() # not yet — deferred
|
|
await get_tree().process_frame
|
|
assert_bool(HudGroups.is_app_active(FRONT)).is_true()
|
|
HudGroups.close_app() # restore clean autoload state
|