extends GdUnitTestSuite ## NEO-24 integration test (code-review follow-up to the blocking issue): ## ## `main.gd` connects `PositionAuthorityClient.authoritative_ack` → ## `TargetSelectionClient.on_authoritative_ack`. The unit suites cover each side ## in isolation, but do not prove the two actually meet during normal locomotion ## (a successful `move-stream` POST, not a boot resync). This suite wires the ## **real** scripts together with mock HTTP transports and exercises that path ## end-to-end: with a lock held, a `move-stream` 200 must produce a throttled ## `GET /target` refresh on the target client's transport. const PositionAuthorityTestDouble := preload("res://test/position_authority_test_double.gd") const TargetSelectionTestDouble := preload("res://test/target_selection_test_double.gd") const ALPHA_ID := "prototype_npc_melee" # Mirrors the transports from the per-client suites: a queue-backed `Node` with a # `request()` that synchronously emits `request_completed`. Counts requests so the # test can assert exactly how many GETs reached the target-client transport. class MockHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var request_count: int = 0 var last_url: String = "" var last_method: HTTPClient.Method = HTTPClient.METHOD_GET 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: request_count += 1 last_url = url last_method = method 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 func _make_authority(transport: Node) -> Node: var a: Node = PositionAuthorityTestDouble.new() a.set("injected_http", transport) auto_free(transport) auto_free(a) add_child(a) return a func _make_target_client(transport: Node) -> Node: var c: Node = TargetSelectionTestDouble.new() c.set("injected_http", transport) auto_free(transport) auto_free(c) add_child(c) return c func _select_response_json(locked_id: String, validity: String, sequence: int) -> String: var state := ( '{"schemaVersion":1,"playerId":"dev-local-1","lockedTargetId":"%s",' % locked_id + '"validity":"%s","sequence":%d}' % [validity, sequence] ) return '{"schemaVersion":1,"selectionApplied":true,"targetState":%s}' % state func _target_state_json(locked_id: String, validity: String, sequence: int) -> String: return ( '{"schemaVersion":1,"playerId":"dev-local-1","lockedTargetId":"%s",' % locked_id + '"validity":"%s","sequence":%d}' % [validity, sequence] ) func test_move_stream_200_triggers_target_refresh_while_locked() -> void: # Arrange var auth_transport := MockHttpTransport.new() var target_transport := MockHttpTransport.new() # 1) Target client locks alpha via POST. target_transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(ALPHA_ID, "ok", 1) ) # 2) After the move-stream POST, the target client refreshes with GET. target_transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(ALPHA_ID, "out_of_range", 1) ) # 3) The authority's move-stream POST echoes position (triggers `authoritative_ack`). auth_transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, ( '{"schemaVersion":1,"playerId":"dev-local-1","position":{"x":-20,"y":0.9,"z":-20},' + '"sequence":3}' ) ) var authority := _make_authority(auth_transport) var target_client := _make_target_client(target_transport) # Same wiring as `main.gd` so this test fails loudly if someone renames the signal # or handler without updating both ends. authority.connect("authoritative_ack", Callable(target_client, "on_authoritative_ack")) # Act target_client.request_select_target_id(ALPHA_ID) # Assert assert_that(target_client.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID) var get_count_baseline: int = target_transport.request_count # Act authority.submit_stream_targets([Vector3(-20.0, 0.9, -20.0)]) # Assert var new_requests: int = target_transport.request_count - get_count_baseline assert_that(new_requests).is_equal(1) assert_that(target_transport.last_url).contains("/game/players/dev-local-1/target") assert_that(target_transport.last_method).is_equal(HTTPClient.METHOD_GET) assert_that(target_client.cached_state().get("validity")).is_equal("out_of_range") # Soft lock is preserved on out-of-range. assert_that(target_client.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID) func test_move_stream_200_does_not_refresh_without_lock() -> void: # Arrange var auth_transport := MockHttpTransport.new() var target_transport := MockHttpTransport.new() auth_transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"playerId":"dev-local-1","position":{"x":0,"y":0.9,"z":0},"sequence":1}' ) var authority := _make_authority(auth_transport) var target_client := _make_target_client(target_transport) authority.connect("authoritative_ack", Callable(target_client, "on_authoritative_ack")) # Act authority.submit_stream_targets([Vector3.ZERO]) # Assert assert_that(target_transport.request_count).is_equal(0)