149 lines
5.5 KiB
GDScript
149 lines
5.5 KiB
GDScript
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_target_alpha"
|
|
|
|
|
|
# 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:
|
|
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"))
|
|
|
|
# Establish the lock first.
|
|
target_client.request_select_target_id(ALPHA_ID)
|
|
assert_that(target_client.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
|
|
var get_count_baseline: int = target_transport.request_count
|
|
|
|
# Simulate WASD locomotion: authority POSTs move-stream, server echoes 200.
|
|
authority.submit_stream_targets([Vector3(-20.0, 0.9, -20.0)])
|
|
|
|
# The target client must have issued exactly one refresh GET in response to the ack.
|
|
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:
|
|
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"))
|
|
|
|
authority.submit_stream_targets([Vector3.ZERO])
|
|
|
|
assert_that(target_transport.request_count).is_equal(0)
|