extends GdUnitTestSuite ## NEO-85: cast accept + target lock change trigger combat-target GET refresh. const CastClient := preload("res://scripts/ability_cast_client.gd") const CombatTargetsClient := preload("res://scripts/combat_targets_client.gd") class MockCastTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var body_json: String = ( '{"schemaVersion":1,"accepted":true,"combatResolution":' + '{"abilityId":"prototype_pulse","targetId":"prototype_npc_melee",' + '"damageDealt":25,"targetRemainingHp":75,"targetDefeated":false}}' ) func request( _url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_POST, _request_data: String = "" ) -> Error: request_completed.emit( HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_json.to_utf8_buffer() ) return OK class SpyCombatTargetsClient: extends Node var sync_calls: int = 0 func request_sync_from_server() -> void: sync_calls += 1 class CombatRefreshHarness: extends Node var combat_sync_calls: int = 0 var last_target_state: Dictionary = {} var _cast: Node var _combat: SpyCombatTargetsClient func setup(parent: Node, cast_transport: Node) -> void: _combat = SpyCombatTargetsClient.new() _cast = CastClient.new() _cast.set("injected_http", cast_transport) parent.add_child(_cast) parent.add_child(_combat) _cast.connect("cast_result_received", Callable(self, "_on_cast_result")) last_target_state = {} func _on_cast_result(accepted: bool, _reason_code: String, _resolution: Dictionary) -> void: if not accepted: return _request_combat_targets_refresh() func _request_combat_targets_refresh() -> void: combat_sync_calls += 1 _combat.request_sync_from_server() func on_target_state_changed(state: Dictionary) -> void: var prev_lock := _locked_target_id_from_state(last_target_state) last_target_state = state.duplicate() var new_lock := _locked_target_id_from_state(last_target_state) if new_lock != prev_lock: _request_combat_targets_refresh() func _locked_target_id_from_state(state: Dictionary) -> String: var locked_variant: Variant = state.get("lockedTargetId", null) if locked_variant is String: return (locked_variant as String).strip_edges() return "" func request_cast() -> void: _cast.call("request_cast", 0, "prototype_pulse", "prototype_npc_melee") func test_successful_cast_triggers_combat_target_sync() -> void: # Arrange var transport := MockCastTransport.new() var harness := CombatRefreshHarness.new() auto_free(transport) auto_free(harness) add_child(harness) harness.setup(self, transport) monitor_signals(harness._cast) # Act harness.request_cast() # Assert await assert_signal(harness._cast).is_emitted("cast_result_received", true, "", any()) assert_that(harness.combat_sync_calls).is_equal(1) assert_that(harness._combat.sync_calls).is_equal(1) func test_target_lock_change_triggers_combat_target_sync() -> void: # Arrange var transport := MockCastTransport.new() var harness := CombatRefreshHarness.new() auto_free(transport) auto_free(harness) add_child(harness) harness.setup(self, transport) # Act harness.on_target_state_changed( {"lockedTargetId": "prototype_npc_melee", "validity": "ok", "sequence": 1} ) # Assert assert_that(harness.combat_sync_calls).is_equal(1) assert_that(harness._combat.sync_calls).is_equal(1) func test_target_validity_only_update_skips_combat_target_sync() -> void: # Arrange var transport := MockCastTransport.new() var harness := CombatRefreshHarness.new() auto_free(transport) auto_free(harness) add_child(harness) harness.setup(self, transport) harness.on_target_state_changed( {"lockedTargetId": "prototype_npc_melee", "validity": "ok", "sequence": 1} ) # Act harness.on_target_state_changed( {"lockedTargetId": "prototype_npc_melee", "validity": "out_of_range", "sequence": 2} ) # Assert assert_that(harness.combat_sync_calls).is_equal(1)