extends GdUnitTestSuite ## NEO-110: `EncounterProgressClient` GET parse + failure signals. const EncounterProgressClient := preload("res://scripts/encounter_progress_client.gd") const PROTOTYPE_ENCOUNTER_ID := "prototype_combat_pocket" var _progress_capture: Dictionary = {} func _capture_progress(snapshot: Dictionary) -> void: _progress_capture = snapshot class MockHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) var last_url: String = "" var response_code: int = 200 var body_json: String = "" func request( url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_GET, _request_data: String = "" ) -> Error: last_url = url request_completed.emit( HTTPRequest.RESULT_SUCCESS, response_code, PackedStringArray(), body_json.to_utf8_buffer() ) return OK static func _inactive_progression_json() -> String: return ( '{"schemaVersion":1,"playerId":"dev-local-1","encounters":' + '[{"encounterId":"prototype_combat_pocket","state":"inactive","defeatedTargetIds":[]}]}' ) static func _active_two_defeats_json() -> String: return ( '{"schemaVersion":1,"playerId":"dev-local-1","encounters":' + '[{"encounterId":"prototype_combat_pocket","state":"active",' + '"defeatedTargetIds":["prototype_npc_melee","prototype_npc_ranged"]}]}' ) static func _completed_with_grants_json() -> String: return ( '{"schemaVersion":1,"playerId":"dev-local-1","encounters":' + '[{"encounterId":"prototype_combat_pocket","state":"completed",' + '"defeatedTargetIds":["prototype_npc_elite","prototype_npc_melee","prototype_npc_ranged"],' + '"completedAt":"2026-05-31T12:00:00Z",' + '"rewardGrantSummary":[{"itemId":"scrap_metal_bulk","quantity":10},' + '{"itemId":"contract_handoff_token","quantity":1}]}]}' ) func _make_client(transport: Node) -> Node: var c: Node = EncounterProgressClient.new() c.set("injected_http", transport) auto_free(transport) auto_free(c) add_child(c) return c func test_parse_inactive_encounter_row() -> void: # Arrange var json := _inactive_progression_json() # Act var snapshot: Variant = EncounterProgressClient.parse_encounter_progress_json(json) # Assert assert_that(snapshot is Dictionary).is_true() var c: Node = EncounterProgressClient.new() auto_free(c) var row: Dictionary = c.call("encounter_row", PROTOTYPE_ENCOUNTER_ID, snapshot as Dictionary) assert_that(str(row.get("state", ""))).is_equal("inactive") assert_that((row.get("defeatedTargetIds", []) as Array).size()).is_equal(0) func test_parse_active_row_has_two_defeats() -> void: # Arrange var json := _active_two_defeats_json() # Act var snapshot: Variant = EncounterProgressClient.parse_encounter_progress_json(json) # Assert assert_that(snapshot is Dictionary).is_true() var c: Node = EncounterProgressClient.new() auto_free(c) var row: Dictionary = c.call("encounter_row", PROTOTYPE_ENCOUNTER_ID, snapshot as Dictionary) assert_that(str(row.get("state", ""))).is_equal("active") assert_that((row.get("defeatedTargetIds", []) as Array).size()).is_equal(2) func test_parse_completed_row_has_reward_grant_summary() -> void: # Arrange var json := _completed_with_grants_json() # Act var snapshot: Variant = EncounterProgressClient.parse_encounter_progress_json(json) # Assert assert_that(snapshot is Dictionary).is_true() var c: Node = EncounterProgressClient.new() auto_free(c) var row: Dictionary = c.call("encounter_row", PROTOTYPE_ENCOUNTER_ID, snapshot as Dictionary) assert_that(str(row.get("state", ""))).is_equal("completed") var grants: Variant = row.get("rewardGrantSummary", null) assert_that(grants is Array).is_true() assert_that((grants as Array).size()).is_equal(2) func test_request_sync_emits_encounter_progress_received() -> void: # Arrange _progress_capture = {} var transport := MockHttpTransport.new() transport.body_json = _inactive_progression_json() var c := _make_client(transport) c.connect("encounter_progress_received", Callable(self, "_capture_progress")) monitor_signals(c) # Act c.call("request_sync_from_server") # Assert await assert_signal(c).is_emitted("encounter_progress_received", any()) assert_that(_progress_capture.get("playerId", "")).is_equal("dev-local-1") assert_that(transport.last_url).contains("/encounter-progress") func test_http_404_emits_encounter_sync_failed() -> void: # Arrange var transport := MockHttpTransport.new() transport.response_code = 404 var c := _make_client(transport) monitor_signals(c) # Act c.call("request_sync_from_server") # Assert await assert_signal(c).is_emitted("encounter_sync_failed", "HTTP 404 (player unknown)") func test_parse_encounter_progress_json_returns_null_for_schema_mismatch() -> void: # Arrange var json := '{"schemaVersion":2,"playerId":"dev-local-1","encounters":[]}' # Act var snapshot: Variant = EncounterProgressClient.parse_encounter_progress_json(json) # Assert assert_that(snapshot).is_null() func test_invalid_schema_emits_encounter_sync_failed() -> void: # Arrange var transport := MockHttpTransport.new() transport.body_json = '{"schemaVersion":2,"playerId":"dev-local-1","encounters":[]}' var c := _make_client(transport) monitor_signals(c) # Act c.call("request_sync_from_server") # Assert await assert_signal(c).is_emitted( "encounter_sync_failed", "non-JSON body or schemaVersion mismatch" )