## #411: Integration test — full input roundtrip through live server. ## Validates the complete path: encode inputs → TCP wire → server deserialize → ## simulation systems → ObserverSnapshot → client decode. ## Catches integration seams that unit tests miss by exercising both movement ## and interaction actions against the real server binary. ## Requires: server binary built (cargo build in server/) class_name TestInputRoundtrip extends GdUnitTestSuite const CONNECT_TIMEOUT: float = 3.0 const RESPONSE_TIMEOUT: float = 5.0 const MAX_PORT_ATTEMPTS: int = 5 var _server_pid: int = -1 var _bridge: LocalBridge = null var _test_port: int = 0 func _server_binary_path() -> String: var project_dir := ProjectSettings.globalize_path("res://") return project_dir.path_join("../server/target/debug/settled-reach-server") static func _random_test_port() -> int: return 49152 + (randi() % (65535 - 49152 + 1)) 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 _server_pid = -1 return false func after_test() -> void: if _bridge != null: _bridge.disconnect_from_server() _bridge = null if _server_pid > 0 and OS.is_process_running(_server_pid): OS.kill(_server_pid) _server_pid = -1 func _connect_to_server() -> bool: var server_path := _server_binary_path() if not FileAccess.file_exists(server_path): push_warning("Input roundtrip test skipped: server binary not found at %s" % server_path) return false var spawned := await _spawn_server(server_path) if not spawned: return false _bridge = LocalBridge.new() 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") return false 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: return true if _bridge.get_status() == StreamPeerTCP.STATUS_ERROR: _bridge.disconnect_from_server() _bridge.reset() await get_tree().create_timer(0.1).timeout elapsed += 0.1 return false ## Send a batch input and receive the snapshot response. func _send_and_receive(action_name: String, tick: int = 0, action_data: Variant = null) -> Variant: var input_entry := {"tick": tick, "action_name": action_name} if action_data != null: input_entry["action_data"] = action_data var inputs: Array = [input_entry] var encoded := Protocol.encode_player_inputs(inputs) assert_that(encoded.size()).is_greater(0) var send_err := _bridge.send_message(encoded) assert_that(send_err).is_equal(OK) var snapshot_bytes := PackedByteArray() var elapsed := 0.0 while elapsed < RESPONSE_TIMEOUT: _bridge.poll() snapshot_bytes = _bridge.poll_message() if snapshot_bytes.size() > 0: break await get_tree().create_timer(0.05).timeout elapsed += 0.05 assert_that(snapshot_bytes.size()).is_greater(0) var snapshot: Variant = Protocol.decode_snapshot(snapshot_bytes) assert_that(snapshot).is_not_null() return snapshot ## Find the player entity in a snapshot by kind. static func _find_player(snapshot: Dictionary) -> Dictionary: for entity in snapshot.entities: if entity.kind.variant == "Player": return entity return {} # -- Movement roundtrip: send movement, verify position changes ---------------- func test_movement_roundtrip() -> void: var ok := await _connect_to_server() if not ok: return # Use Interact (server no-op) to get a baseline snapshot without moving. var baseline: Dictionary = await _send_and_receive("Interact", 0) var player := _find_player(baseline) assert_that(player.size()).is_greater(0) # Player starts at (16,16) → render coords (16.5, 16.5) var start_x: float = player.x var start_y: float = player.y assert_float(start_x).is_equal_approx(16.5, 0.001) assert_float(start_y).is_equal_approx(16.5, 0.001) # Send MoveNorth — player should move to (16, 15) → (16.5, 15.5) var snap1: Dictionary = await _send_and_receive("MoveNorth", 1) var p1 := _find_player(snap1) assert_that(p1.size()).is_greater(0) assert_float(p1.x).is_equal_approx(16.5, 0.001) assert_float(p1.y).is_equal_approx(15.5, 0.001) # Send MoveEast — player should move to (17, 15) → (17.5, 15.5) var snap2: Dictionary = await _send_and_receive("MoveEast", 2) var p2 := _find_player(snap2) assert_that(p2.size()).is_greater(0) assert_float(p2.x).is_equal_approx(17.5, 0.001) assert_float(p2.y).is_equal_approx(15.5, 0.001) # Verify position actually changed from baseline assert_that(p2.x != start_x or p2.y != start_y).is_true() # -- Interact roundtrip: server accepts without crashing ----------------------- func test_interact_roundtrip() -> void: var ok := await _connect_to_server() if not ok: return # Send Interact action — unit variant, no target data. # Server accepts it (currently a no-op) and responds with a valid snapshot. var snapshot: Dictionary = await _send_and_receive("Interact", 0) # Snapshot should be valid with correct protocol version assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snapshot.entities.size()).is_greater(0) # Player should be at start position (Interact doesn't move) var player := _find_player(snapshot) assert_that(player.size()).is_greater(0) assert_float(player.x).is_equal_approx(16.5, 0.001) assert_float(player.y).is_equal_approx(16.5, 0.001) # Send Interact again at next tick — server should still accept it var snap2: Dictionary = await _send_and_receive("Interact", 1) assert_that(snap2).is_not_null() assert_that(snap2.version).is_equal(Protocol.PROTOCOL_VERSION) # -- Mixed sequence: movement then interact in one session --------------------- func test_move_then_interact() -> void: var ok := await _connect_to_server() if not ok: return # Move player first var snap1: Dictionary = await _send_and_receive("MoveNorth", 0) var p1 := _find_player(snap1) assert_that(p1.size()).is_greater(0) var moved_x: float = p1.x var moved_y: float = p1.y # Now send Interact — player should stay where they moved to var snap2: Dictionary = await _send_and_receive("Interact", 1) var p2 := _find_player(snap2) assert_that(p2.size()).is_greater(0) assert_float(p2.x).is_equal_approx(moved_x, 0.001) assert_float(p2.y).is_equal_approx(moved_y, 0.001)