212 lines
7.6 KiB
GDScript
212 lines
7.6 KiB
GDScript
extends GdUnitTestSuite
|
|
|
|
## NEO-24: GdUnit4 suite for `TargetSelectionClient`. Mirrors
|
|
## `position_authority_client_test.gd` — mock HTTP transport enqueues JSON responses and
|
|
## (optionally) tracks request count so the movement-triggered refresh cooldown is testable.
|
|
|
|
const TargetSelectionTestDouble := preload("res://test/target_selection_test_double.gd")
|
|
|
|
const ALPHA_ID := "prototype_target_alpha"
|
|
const BETA_ID := "prototype_target_beta"
|
|
|
|
|
|
# In-process transport: not an HTTPRequest subclass (Godot 4 will not call script `request()`
|
|
# on those). Counts requests so "no lock → no GET" and "cooldown collapses snaps" are testable.
|
|
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_body: 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
|
|
last_body = request_data
|
|
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_client(http_transport: Node) -> Node:
|
|
var c: Node = TargetSelectionTestDouble.new()
|
|
c.set("injected_http", http_transport)
|
|
auto_free(http_transport)
|
|
auto_free(c)
|
|
add_child(c)
|
|
return c
|
|
|
|
|
|
func _target_state_json(locked_id_or_null: Variant, validity: String, sequence: int = 0) -> String:
|
|
var locked := "null"
|
|
if locked_id_or_null is String and not (locked_id_or_null as String).is_empty():
|
|
locked = '"%s"' % (locked_id_or_null as String)
|
|
return (
|
|
'{"schemaVersion":1,"playerId":"dev-local-1","lockedTargetId":%s,' % locked
|
|
+ '"validity":"%s","sequence":%d}' % [validity, sequence]
|
|
)
|
|
|
|
|
|
func _select_response_json(
|
|
applied: bool,
|
|
locked_id_or_null: Variant,
|
|
validity: String,
|
|
sequence: int = 0,
|
|
reason: String = ""
|
|
) -> String:
|
|
var target_state := _target_state_json(locked_id_or_null, validity, sequence)
|
|
if applied:
|
|
return '{"schemaVersion":1,"selectionApplied":true,"targetState":%s}' % target_state
|
|
var reason_segment := ""
|
|
if not reason.is_empty():
|
|
reason_segment = ',"reasonCode":"%s"' % reason
|
|
return (
|
|
'{"schemaVersion":1,"selectionApplied":false%s,"targetState":%s}'
|
|
% [reason_segment, target_state]
|
|
)
|
|
|
|
|
|
func test_sync_get_200_emits_state() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(null, "none"))
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
c.request_sync_from_server()
|
|
assert_signal(c).is_emitted("target_state_changed")
|
|
var state: Dictionary = c.cached_state()
|
|
assert_that(state.get("validity", "")).is_equal("none")
|
|
assert_that(state.get("lockedTargetId", "non-null-sentinel")).is_null()
|
|
|
|
|
|
func test_tab_from_no_lock_selects_first_id() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
|
|
)
|
|
var c := _make_client(transport)
|
|
monitor_signals(c)
|
|
c.request_tab_next()
|
|
assert_that(transport.last_url).contains("/target/select")
|
|
assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST)
|
|
assert_that(transport.last_body).contains('"targetId":"%s"' % ALPHA_ID)
|
|
assert_signal(c).is_emitted("target_state_changed")
|
|
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
|
|
|
|
|
|
func test_tab_from_alpha_requests_beta() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
|
|
)
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, BETA_ID, "ok", 2)
|
|
)
|
|
var c := _make_client(transport)
|
|
c.request_tab_next()
|
|
c.request_tab_next()
|
|
assert_that(transport.last_body).contains('"targetId":"%s"' % BETA_ID)
|
|
assert_that(c.cached_state().get("lockedTargetId")).is_equal(BETA_ID)
|
|
|
|
|
|
func test_tab_wraps_from_beta_to_alpha() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
|
|
)
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, BETA_ID, "ok", 2)
|
|
)
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 3)
|
|
)
|
|
var c := _make_client(transport)
|
|
c.request_tab_next()
|
|
c.request_tab_next()
|
|
c.request_tab_next()
|
|
assert_that(transport.last_body).contains('"targetId":"%s"' % ALPHA_ID)
|
|
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
|
|
|
|
|
|
func test_denial_reflects_authoritative_target_state() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
# Server denies: selectionApplied=false, lockedTargetId=null, reasonCode=out_of_range.
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS,
|
|
200,
|
|
_select_response_json(false, null, "none", 0, "out_of_range")
|
|
)
|
|
var c := _make_client(transport)
|
|
c.request_select_target_id(ALPHA_ID)
|
|
var state: Dictionary = c.cached_state()
|
|
assert_that(bool(state.get("selectionApplied", true))).is_false()
|
|
assert_that(state.get("reasonCode", "")).is_equal("out_of_range")
|
|
assert_that(state.get("lockedTargetId", "sentinel")).is_null()
|
|
|
|
|
|
func test_clear_issues_post_without_target_id() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, null, "none", 4))
|
|
var c := _make_client(transport)
|
|
c.request_clear_target()
|
|
assert_that(transport.last_url).contains("/target/select")
|
|
assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST)
|
|
assert_that(transport.last_body).not_contains("targetId")
|
|
|
|
|
|
func test_authoritative_ack_while_locked_fires_refresh_get() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
# Step 1: lock alpha via POST.
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
|
|
)
|
|
# Step 2: movement-triggered GET shows out_of_range (soft lock).
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(ALPHA_ID, "out_of_range", 1)
|
|
)
|
|
var c := _make_client(transport)
|
|
c.request_select_target_id(ALPHA_ID)
|
|
var count_before_ack: int = transport.request_count
|
|
c.on_authoritative_ack(Vector3.ZERO)
|
|
assert_that(transport.request_count).is_equal(count_before_ack + 1)
|
|
assert_that(c.cached_state().get("validity")).is_equal("out_of_range")
|
|
assert_that(c.cached_state().get("lockedTargetId")).is_equal(ALPHA_ID)
|
|
|
|
|
|
func test_authoritative_ack_without_lock_fires_nothing() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
var c := _make_client(transport)
|
|
c.on_authoritative_ack(Vector3.ZERO)
|
|
assert_that(transport.request_count).is_equal(0)
|
|
|
|
|
|
func test_two_acks_within_cooldown_collapse_to_one_get() -> void:
|
|
var transport := MockHttpTransport.new()
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _select_response_json(true, ALPHA_ID, "ok", 1)
|
|
)
|
|
transport.enqueue(
|
|
HTTPRequest.RESULT_SUCCESS, 200, _target_state_json(ALPHA_ID, "out_of_range", 1)
|
|
)
|
|
var c := _make_client(transport)
|
|
c.request_select_target_id(ALPHA_ID)
|
|
var count_before: int = transport.request_count
|
|
c.on_authoritative_ack(Vector3.ZERO)
|
|
c.on_authoritative_ack(Vector3.ZERO)
|
|
assert_that(transport.request_count).is_equal(count_before + 1)
|