extends GdUnitTestSuite # main.gd sets base_url/dev_player_id on AbilityCastClient from the same authority block as # HotbarLoadoutClient (NEO-31). # NEO-30: Slice 3 ability_cast_denied dev warning includes reasonCode from AbilityCastResponse; # server deny hook sites are documented in AbilityCastApi.cs (gdlint: script doc lines <= 100 cols). const CastClient := preload("res://scripts/ability_cast_client.gd") class MockHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) 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: last_url = url last_method = method last_body = request_data if _queue.is_empty(): return ERR_UNAVAILABLE var r: Dictionary = _queue.pop_front() request_completed.emit( r["result"], r["code"], PackedStringArray(), str(r["body"]).to_utf8_buffer() ) return OK class HoldFirstThenAutoTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var request_count: int = 0 var last_body: String = "" var _first: bool = true func request( _url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_GET, request_data: String = "" ) -> Error: request_count += 1 last_body = request_data var body_json := '{"schemaVersion":1,"accepted":true}' var body_bytes: PackedByteArray = body_json.to_utf8_buffer() if _first: _first = false call_deferred("_emit", body_bytes) else: request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes) return OK func _emit(body_bytes: PackedByteArray) -> void: request_completed.emit(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_bytes) func _make_client(transport: Node) -> Node: var c: Node = CastClient.new() c.set("injected_http", transport) auto_free(transport) auto_free(c) add_child(c) return c func test_request_cast_posts_expected_payload_with_null_target() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}') var c := _make_client(transport) # Act var started: bool = bool(c.call("request_cast", 2, "prototype_guard", null)) # Assert assert_that(started).is_true() assert_that(transport.last_url).contains("/ability-cast") assert_that(transport.last_method).is_equal(HTTPClient.METHOD_POST) var parsed: Variant = JSON.parse_string(transport.last_body) assert_that(parsed is Dictionary).is_true() var body: Dictionary = parsed assert_that(int(body.get("schemaVersion", 0))).is_equal(1) assert_that(int(body.get("slotIndex", -1))).is_equal(2) assert_that(body.get("abilityId")).is_equal("prototype_guard") assert_that(body.get("targetId", "missing")).is_equal(null) func test_request_cast_posts_target_id_string_when_set() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}') var c := _make_client(transport) # Act var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee")) # Assert assert_that(started).is_true() var parsed: Variant = JSON.parse_string(transport.last_body) var body: Dictionary = parsed assert_that(body.get("targetId")).is_equal("prototype_npc_melee") func test_request_cast_returns_false_when_http_request_fails_to_start() -> void: # Arrange # Empty queue → transport.request returns ERR_UNAVAILABLE (same as failed start). var transport := MockHttpTransport.new() var c := _make_client(transport) # Act var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null)) # Assert assert_that(started).is_false() func test_request_cast_while_busy_is_ignored() -> void: # Arrange var transport := HoldFirstThenAutoTransport.new() var c := _make_client(transport) # Act var first_started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null)) var second_started: bool = bool(c.call("request_cast", 1, "prototype_guard", null)) await get_tree().process_frame await get_tree().process_frame # Assert assert_that(first_started).is_true() assert_that(second_started).is_false() assert_that(transport.request_count).is_equal(1) var parsed: Variant = JSON.parse_string(transport.last_body) var body: Dictionary = parsed assert_that(int(body.get("slotIndex", -1))).is_equal(0) func test_cast_result_received_emits_false_with_reason_on_denied() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":false,"reasonCode":"invalid_target"}' ) var c := _make_client(transport) var got: Array = [] c.connect( "cast_result_received", func(accepted: bool, reason: String, resolution: Dictionary) -> void: got.append([accepted, reason, resolution]) ) # Act var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee")) # Assert assert_that(started).is_true() assert_that(got.size()).is_equal(1) assert_that(bool(got[0][0])).is_false() assert_that(str(got[0][1])).is_equal("invalid_target") assert_that((got[0][2] as Dictionary).is_empty()).is_true() func test_cast_result_received_emits_true_with_empty_reason_on_accepted() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue(HTTPRequest.RESULT_SUCCESS, 200, '{"schemaVersion":1,"accepted":true}') var c := _make_client(transport) var got: Array = [] c.connect( "cast_result_received", func(accepted: bool, reason: String, resolution: Dictionary) -> void: got.append([accepted, reason, resolution]) ) # Act var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", null)) # Assert assert_that(started).is_true() assert_that(got.size()).is_equal(1) assert_that(bool(got[0][0])).is_true() assert_that(str(got[0][1])).is_equal("") assert_that((got[0][2] as Dictionary).is_empty()).is_true() func test_cast_result_received_emits_combat_resolution_on_accept() -> void: # Arrange var transport := MockHttpTransport.new() transport.enqueue( HTTPRequest.RESULT_SUCCESS, 200, ( '{"schemaVersion":1,"accepted":true,"combatResolution":' + '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",' + '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}' ) ) var c := _make_client(transport) var got: Array = [] c.connect( "cast_result_received", func(accepted: bool, reason: String, resolution: Dictionary) -> void: got.append([accepted, reason, resolution]) ) # Act var started: bool = bool(c.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee")) # Assert assert_that(started).is_true() assert_that(got.size()).is_equal(1) assert_that(bool(got[0][0])).is_true() var resolution: Dictionary = got[0][2] assert_that(int(resolution.get("damageDealt", 0))).is_equal(25) assert_that(int(resolution.get("targetRemainingHp", 0))).is_equal(75) assert_that(bool(resolution.get("targetDefeated", false))).is_false() func test_parse_cast_response_json_maps_combat_resolution() -> void: # Arrange var body := ( '{"schemaVersion":1,"accepted":true,"combatResolution":' + '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",' + '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}' ) # Act var parsed: Variant = CastClient.parse_cast_response_json(body) # Assert assert_that(parsed is Dictionary).is_true() var data: Dictionary = parsed assert_that(bool(data.get("accepted", false))).is_true() var resolution: Dictionary = data.get("resolution", {}) as Dictionary assert_that(int(resolution.get("damageDealt", 0))).is_equal(25)