extends GdUnitTestSuite ## NEO-153: contract HUD controller issue key, active label, reward transition. const ContractHudController := preload("res://scripts/contract_hud_controller.gd") const ContractClient := preload("res://scripts/contract_client.gd") const INSTANCE_ID := "ci_test_instance" const ENCOUNTER_ID := "prototype_combat_pocket" class MockHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) 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: request_completed.emit( HTTPRequest.RESULT_SUCCESS, response_code, PackedStringArray(), body_json.to_utf8_buffer() ) return OK class NoopHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) func request( _url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_GET, _request_data: String = "" ) -> Error: return OK class PendingIssueHttpTransport: extends Node signal request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray ) func request( _url: String, _custom_headers: PackedStringArray = PackedStringArray(), _method: HTTPClient.Method = HTTPClient.METHOD_POST, _request_data: String = "" ) -> Error: return OK static func _active_snapshot() -> Dictionary: return { "schemaVersion": 1, "playerId": "dev-local-1", "contracts": [ { "contractInstanceId": INSTANCE_ID, "templateId": ContractHudController.PROTOTYPE_CONTRACT_TEMPLATE_ID, "templateDisplayName": "Clear Combat Pocket (Repeat)", "status": "active", "encounterTemplateId": ENCOUNTER_ID, "seedBucket": ContractHudController.PROTOTYPE_CONTRACT_SEED_BUCKET, "issuedAt": "2026-06-28T12:00:00Z", } ], } static func _completed_snapshot() -> Dictionary: return { "schemaVersion": 1, "playerId": "dev-local-1", "contracts": [ { "contractInstanceId": INSTANCE_ID, "templateId": ContractHudController.PROTOTYPE_CONTRACT_TEMPLATE_ID, "templateDisplayName": "Clear Combat Pocket (Repeat)", "status": "completed", "encounterTemplateId": ENCOUNTER_ID, "seedBucket": ContractHudController.PROTOTYPE_CONTRACT_SEED_BUCKET, "issuedAt": "2026-06-28T12:00:00Z", "completedAt": "2026-06-28T13:00:00Z", "completionRewardSummary": { "itemGrants": [{"itemId": "scrap_metal_bulk", "quantity": 5}], "skillXpGrants": [{"skillId": "salvage", "amount": 15}], }, } ], } func _make_contract_client(sync_transport: Node, issue_transport: Node = null) -> Node: var c: Node = ContractClient.new() c.set("injected_sync_http", sync_transport) var issue_http: Node = issue_transport if issue_transport != null else sync_transport c.set("injected_issue_http", issue_http) auto_free(sync_transport) if issue_transport != null and issue_transport != sync_transport: auto_free(issue_transport) auto_free(c) add_child(c) return c func _build_controller(sync_transport: Node, issue_transport: Node) -> Dictionary: var controller: Node = ContractHudController.new() var contract: Node = _make_contract_client(sync_transport, issue_transport) var active_label := Label.new() var issue_label := Label.new() var reward_label := Label.new() auto_free(controller) auto_free(active_label) auto_free(issue_label) auto_free(reward_label) add_child(controller) await get_tree().process_frame controller.call( "setup", contract, active_label, issue_label, reward_label, Callable(self, "_noop_http_config") ) await get_tree().process_frame return { "controller": controller, "contract": contract, "active_label": active_label, "issue_label": issue_label, "reward_label": reward_label, } func _noop_http_config(_client: Node) -> void: pass func test_try_issue_key_input_handles_shift_c() -> void: # Arrange var built: Dictionary = await _build_controller( NoopHttpTransport.new(), PendingIssueHttpTransport.new() ) var event := InputEventKey.new() event.pressed = true event.shift_pressed = true event.keycode = KEY_C # Act var handled: bool = bool(built["controller"].call("try_issue_key_input", event)) # Assert assert_bool(handled).is_true() assert_str(built["issue_label"].text).contains("sending") func test_active_label_shows_encounter_objective_id() -> void: # Arrange var built: Dictionary = await _build_controller( NoopHttpTransport.new(), NoopHttpTransport.new() ) # Act built["controller"].call("_on_contracts_received", _active_snapshot()) # Assert assert_str(built["active_label"].text).contains(ENCOUNTER_ID) assert_str(built["active_label"].text).contains("active") func test_issue_deny_shows_economy_cap_copy() -> void: # Arrange var built: Dictionary = await _build_controller( NoopHttpTransport.new(), NoopHttpTransport.new() ) # Act built["controller"].call( "_on_issue_result_received", {"issued": false, "reasonCode": "economy_cap_exceeded"} ) # Assert assert_str(built["issue_label"].text).contains("economy cap exceeded") func test_issue_deny_shows_active_contract_exists_copy() -> void: # Arrange var built: Dictionary = await _build_controller( NoopHttpTransport.new(), NoopHttpTransport.new() ) # Act built["controller"].call( "_on_issue_result_received", {"issued": false, "reasonCode": "active_contract_exists"} ) # Assert assert_str(built["issue_label"].text).contains("active contract already issued") func test_reward_label_populates_on_active_to_completed_transition() -> void: # Arrange var built: Dictionary = await _build_controller( NoopHttpTransport.new(), NoopHttpTransport.new() ) built["controller"].call("_on_contracts_received", _active_snapshot()) # Act built["controller"].call("_on_contracts_received", _completed_snapshot()) # Assert assert_str(built["reward_label"].text).contains("Contract rewards:") assert_str(built["reward_label"].text).contains("Salvage +15 XP") func test_completed_get_clears_stale_issue_patch_on_active_label() -> void: # Arrange — simulate successful issue patch still held when encounter-complete GET arrives var built: Dictionary = await _build_controller( NoopHttpTransport.new(), NoopHttpTransport.new() ) var issue_row: Dictionary = (_active_snapshot()["contracts"] as Array)[0] as Dictionary built["controller"].set("_issue_contract_patch", issue_row.duplicate(true)) built["controller"].set("_tracked_instance_id", INSTANCE_ID) # Act built["controller"].call("_on_contracts_received", _completed_snapshot()) # Assert assert_str(built["active_label"].text).contains("completed") assert_str(built["active_label"].text).not_contains(": active —")