test(client): retry input-roundtrip server spawn on fresh port when it dies pre-connect

The server binds AFTER ~250ms of plugin/sim init, so a random-port collision (os error 98) escapes the 0.15s spawn-liveness check and surfaced as a hard test failure at the push gate. Death during the connect window now retries on a fresh port within MAX_PORT_ATTEMPTS. Verified: targeted suite 6/6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 19:05:43 +02:00
co-authored by Claude Fable 5
parent 974897b889
commit bba14cb9ba
+41 -34
View File
@@ -26,17 +26,13 @@ static func _random_test_port() -> int:
func _spawn_server(server_path: String) -> bool:
for attempt in range(MAX_PORT_ATTEMPTS):
_test_port = _random_test_port()
var addr := "127.0.0.1:%d" % _test_port
_server_pid = OS.create_process(server_path, [addr])
if _server_pid <= 0:
continue
await get_tree().create_timer(0.15).timeout
if OS.is_process_running(_server_pid):
return true
_test_port = _random_test_port()
var addr := "127.0.0.1:%d" % _test_port
_server_pid = OS.create_process(server_path, [addr])
if _server_pid <= 0:
_server_pid = -1
return false
return false
return true
func after_test() -> void:
@@ -51,37 +47,48 @@ func after_test() -> void:
## Spawn the server, connect, and complete the protocol handshake.
## Binary absence is handled by per-test `_do_skip` — by the time this runs the
## binary exists, so any failure here is a real failure (asserted loudly).
## The server binds its port AFTER plugin/simulation init (~250ms), so a
## random-port collision surfaces as a mid-connect process death, not an
## instant spawn failure — a death during the connect window therefore
## retries on a fresh port instead of failing the test (the push gate hit
## exactly that race: "Failed to bind ... Address already in use").
func _connect_to_server() -> bool:
var server_path := _server_binary_path()
var spawned := await _spawn_server(server_path)
assert_bool(spawned).override_failure_message(
"server spawn failed after %d port attempts" % MAX_PORT_ATTEMPTS
).is_true()
if not spawned:
return false
_bridge = LocalBridge.new()
var connected := false
var elapsed := 0.0
while elapsed < CONNECT_TIMEOUT:
if _server_pid > 0 and not OS.is_process_running(_server_pid):
push_warning("Server process died during connection")
for _attempt in range(MAX_PORT_ATTEMPTS):
if not _spawn_server(server_path):
continue
_bridge = LocalBridge.new()
var elapsed := 0.0
while elapsed < CONNECT_TIMEOUT:
if not OS.is_process_running(_server_pid):
push_warning(
"Server died pre-connect (port %d likely in use) — retrying" % _test_port
)
break
if _bridge.get_status() == StreamPeerTCP.STATUS_NONE:
_bridge.connect_to_server("127.0.0.1", _test_port)
_bridge.poll()
if _bridge.get_status() == StreamPeerTCP.STATUS_CONNECTED:
connected = true
break
if _bridge.get_status() == StreamPeerTCP.STATUS_ERROR:
_bridge.disconnect_from_server()
_bridge.reset()
await get_tree().create_timer(0.1).timeout
elapsed += 0.1
if connected:
break
if _bridge.get_status() == StreamPeerTCP.STATUS_NONE:
_bridge.connect_to_server("127.0.0.1", _test_port)
_bridge.poll()
if _bridge.get_status() == StreamPeerTCP.STATUS_CONNECTED:
connected = true
break
if _bridge.get_status() == StreamPeerTCP.STATUS_ERROR:
_bridge.disconnect_from_server()
_bridge.reset()
await get_tree().create_timer(0.1).timeout
elapsed += 0.1
_bridge.disconnect_from_server()
_bridge = null
if _server_pid > 0 and OS.is_process_running(_server_pid):
OS.kill(_server_pid)
_server_pid = -1
assert_bool(connected).override_failure_message(
"TCP connect to spawned server failed within %.1fs" % CONNECT_TIMEOUT
"TCP connect to spawned server failed within %.1fs across %d port attempts"
% [CONNECT_TIMEOUT, MAX_PORT_ATTEMPTS]
).is_true()
if not connected:
return false