feat(client): T-1132 standalone Atlas companion shell + make atlas (D-254 SS3)
New atlas_standalone scene/script: attach-or-spawn boot (one REAL
connect_to_sim attempt at SR_PORT-or-9876 — a separate throwaway TCP
probe was proven by live run to kill a pre-accept-loop server via
broken handshake pipe; never abandon a connected socket), else spawn
--port 0 via new ServerProcess.start_with_pipe + LISTENING:{port}
stdout parse, retry against the resolved port. Reader role wired end
to end: protocol.encode_startup_message optional role param (empty
omits the wire key — byte-identical for all existing callers),
sim_bridge.connection_role suppresses the post-handshake
RequestAllSettings auto-send, hud_groups skips AutoPause/AutoResume
sends for readers (all three would otherwise burn Reader violation
strikes per the T-1130 matrix — endorsed by Oscar).
Generic implant host per D-254 SS3: the shell instantiates ALL
registered implant apps; implant_app_manifest gains
available_in_companion (opt-out, default true) and
implant_registry.instantiate_all a standalone filter param (default
preserves hud.gd behavior byte-identically). Boot order is
instantiate_all THEN open_app (reverse renders a permanently black
window — app_changed fires with no listener; matches hud.gd's order).
Owned-server lifecycle: _exit_tree stops a spawned child, attached
servers survive companion close. Known engine limitation documented:
raw SIGTERM bypasses all Godot notifications and orphans a spawned
server; WM close paths verified clean.
Live-verified: spawn-mode (301 systems rendered from systems.db over
the wire), attach-mode, two simultaneous readers, clean shutdown with
zero orphan processes. 14 new gdUnit tests (port/LISTENING parsing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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, the HudGroups close-interception behavior, 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.
|
||||
##
|
||||
## 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"
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# _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)
|
||||
Reference in New Issue
Block a user