extends GdUnitTestSuite const PositionAuthorityTestDouble := preload("res://test/position_authority_test_double.gd") # In-process transport: not an HTTPRequest subclass (Godot 4 won't call script request() on those). class MockHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var _queue: Array[Dictionary] = [] func enqueue(result: int, code: int, body: String) -> void: _queue.append({"result": result, "code": code, "body": body}) func request( _url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_GET, _request_data: String = "" ) -> Error: if _queue.is_empty(): return ERR_UNAVAILABLE var r: Dictionary = _queue.pop_front() var body_bytes: PackedByteArray = str(r["body"]).to_utf8_buffer() request_completed.emit(r["result"], r["code"], PackedStringArray(), body_bytes) return OK # Holds _busy without completing (never emits request_completed). class HangingHttpTransport: extends Node @warning_ignore("unused_signal") signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var request_count: int = 0 func request( _url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_GET, _request_data: String = "" ) -> Error: request_count += 1 return OK func _make_client(http_transport: Node) -> Node: var c: Node = PositionAuthorityTestDouble.new() c.set("injected_http", http_transport) auto_free(http_transport) auto_free(c) add_child(c) return c func test_sync_boot_get_200_emits_snap() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":1,"y":0.9,"z":-5}}') var c := _make_client(transport) monitor_signals(c) # Act c.sync_from_server() # Assert assert_signal(c).is_emitted("authoritative_position_received", Vector3(1.0, 0.9, -5.0), true) # NEO-24: boot sync also emits the cooldown-throttled heartbeat. func test_sync_boot_get_200_emits_authoritative_ack() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":1,"y":0.9,"z":-5}}') var c := _make_client(transport) monitor_signals(c) # Act c.sync_from_server() # Assert assert_signal(c).is_emitted("authoritative_ack", Vector3(1.0, 0.9, -5.0)) func test_sync_boot_get_200_malformed_position_does_not_emit() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":"invalid"}') var c := _make_client(transport) monitor_signals(c) # Act c.sync_from_server() # Assert assert_signal(c).wait_until(400).is_not_emitted("authoritative_position_received") func test_move_stream_post_400_emits_move_rejected_then_boot_get() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 400, '{"reasonCode":"vertical_step_exceeded"}') transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":0,"y":0.9,"z":0}}') var c := _make_client(transport) monitor_signals(c) # Act c.submit_stream_targets([Vector3(1.0, 2.0, 3.0)]) # Assert assert_signal(c).is_emitted("move_rejected", "vertical_step_exceeded") assert_signal(c).is_emitted("authoritative_position_received", Vector3(0.0, 0.9, 0.0), true) func test_move_stream_post_400_without_reason_emits_unknown() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 400, "{}") transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"position":{"x":1,"y":0.9,"z":1}}') var c := _make_client(transport) monitor_signals(c) # Act c.submit_stream_targets([Vector3.ZERO]) # Assert assert_signal(c).is_emitted("move_rejected", "unknown") assert_signal(c).is_emitted("authoritative_position_received", Vector3(1.0, 0.9, 1.0), true) func test_second_sync_while_busy_is_ignored() -> void: # Arrange var transport := HangingHttpTransport.new() var c := _make_client(transport) # Act c.sync_from_server() c.sync_from_server() # Assert assert_that(transport.request_count).is_equal(1) func test_move_stream_post_200_does_not_emit_position() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, ( '{"schemaVersion":1,"playerId":"dev-local-1","position":{"x":-4.9,"y":0.9,"z":-5},' + '"sequence":3}' ) ) var c := _make_client(transport) monitor_signals(c) # Act c.submit_stream_targets([Vector3(-4.9, 0.9, -5.0)]) # Assert assert_signal(c).wait_until(400).is_not_emitted("authoritative_position_received") # NEO-24: move-stream 200 must emit `authoritative_ack` so a held target lock can re-validate # during normal locomotion (without re-introducing the snap rubber-band). func test_move_stream_post_200_emits_authoritative_ack() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, ( '{"schemaVersion":1,"playerId":"dev-local-1","position":{"x":-4.9,"y":0.9,"z":-5},' + '"sequence":3}' ) ) var c := _make_client(transport) monitor_signals(c) # Act c.submit_stream_targets([Vector3(-4.9, 0.9, -5.0)]) # Assert assert_signal(c).is_emitted("authoritative_ack", Vector3(-4.9, 0.9, -5.0))