extends SceneTree const EntryShell = preload("res://entry/entry_shell.gd") const SettingsStore = preload("res://entry/settings_store.gd") const DraftService = preload("res://entry/initial_company_draft_service.gd") const ContractService = preload("res://entry/guild_contract_service.gd") const DOMAIN_DRAFT := "user://dev3_hire_domain_draft.json" const DOMAIN_COMPANY := "user://dev3_hire_domain_company.json" const UI_DRAFT := "user://dev3_hire_ui_draft.json" const UI_COMPANY := "user://dev3_hire_ui_company.json" const SEED := "a1b2c3d4e5f60718273645aabbccddee" const CONTRACT_OP := "aaaaaaaa11111111bbbbbbbb22222222" const HIRE_OP_1 := "11111111222222223333333344444444" const HIRE_OP_2 := "55555555666666667777777788888888" const RETRY_OP := "99999999aaaaaaaabbbbbbbbcccccccc" var failures: Array[String] = [] var parsed_touches := 0 func _initialize() -> void: call_deferred("run_test") func run_test() -> void: root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND root.content_scale_size = Vector2i(844, 390) root.size = Vector2i(844, 390) _cleanup_all() verify_domain_transactions() await verify_ui_two_hires() _cleanup_all() if failures.is_empty(): print("ABG_DEV3_HIRE_OK receipts=2 atomic=true idempotent=operation+offer conflict=true price=true unavailable=true recovery=candidate company=2,3,4 line=4 coin=52,38,24 third=inspect_only restart=true parsed_touch=%d" % parsed_touches) quit(0) else: for failure in failures: push_error(failure) quit(1) func verify_domain_transactions() -> void: var company := _create_contract_company(DOMAIN_DRAFT, DOMAIN_COMPANY) if company.is_empty(): return var service := _contract_service(DOMAIN_COMPANY) var offers: Array = company.active_contract.market_board.recruit_offers var first_id := String(offers[0].source_offer_id) var second_id := String(offers[1].source_offer_id) expect_rejected_unchanged(service, func(): return service.hire_recruit("bad", first_id, 14), "HIRE_OPERATION_ID_INVALID", DOMAIN_COMPANY, "malformed operation") expect_rejected_unchanged(service, func(): return service.hire_recruit(HIRE_OP_1, "bad", 14), "HIRE_OFFER_ID_INVALID", DOMAIN_COMPANY, "malformed offer") expect_rejected_unchanged(service, func(): return service.hire_recruit(HIRE_OP_1, first_id, 15), "HIRE_PRICE_MISMATCH", DOMAIN_COMPANY, "stale price") var first: Dictionary = service.hire_recruit(HIRE_OP_1, first_id, 14) expect(first.ok and first.code == "hired", "first separate review commits one hire") if not first.ok: return expect(int(first.company.active_contract.coin) == 38 and first.company.owned_recruits.size() == 3, "first hire applies 14 Coin and Company 2 to 3") expect(int(first.receipt.line_capacity) == 4 and first.company.active_contract.available_recruit_offer_ids.size() == 2, "first hire leaves Line capacity four and removes only its offer") expect(first.company.owned_recruits[2].market_offer_id == first_id and not first.company.owned_recruits[2].recruit_hash.is_empty(), "first hire materializes one persistent market Recruit") var duplicate: Dictionary = service.hire_recruit(HIRE_OP_1, first_id, 14) expect(duplicate.ok and duplicate.code == "already_hired" and String(duplicate.receipt.receipt_hash) == String(first.receipt.receipt_hash), "same operation returns original receipt") var recovery_retry: Dictionary = service.hire_recruit(RETRY_OP, first_id, 14) expect(recovery_retry.ok and recovery_retry.code == "already_hired" and String(recovery_retry.receipt.receipt_hash) == String(first.receipt.receipt_hash), "new operation recovery retry resolves by offer intent") expect_rejected_unchanged(service, func(): return service.hire_recruit(HIRE_OP_1, second_id, 14), "HIRE_OPERATION_CONFLICT", DOMAIN_COMPANY, "operation intent conflict") var second: Dictionary = service.hire_recruit(HIRE_OP_2, second_id, 14) expect(second.ok and int(second.company.active_contract.coin) == 24 and second.company.owned_recruits.size() == 4, "second independent receipt reaches exact opening stop") expect(second.company.active_contract.hire_receipts.size() == 2 and String(second.company.active_contract.hire_receipts[0].receipt_hash) != String(second.company.active_contract.hire_receipts[1].receipt_hash), "two hires retain distinct receipts") var third_id := String(offers[2].source_offer_id) expect_rejected_unchanged(service, func(): return service.hire_recruit(RETRY_OP, third_id, 14), "OPENING_HIRES_COMPLETE", DOMAIN_COMPANY, "third opening hire") var expected: Dictionary = second.company.duplicate(true) _remove_company_files(DOMAIN_COMPANY) _write("%s.candidate" % DOMAIN_COMPANY, JSON.stringify(_canonical(expected))) var recovered: Dictionary = _draft_service(DOMAIN_DRAFT, DOMAIN_COMPANY).load_company() expect(recovered.ok and recovered.code == "recovered_candidate", "interrupted applied candidate recovers atomically") expect(recovered.company.owned_recruits.size() == 4 and int(recovered.company.active_contract.coin) == 24 and recovered.company.active_contract.hire_receipts.size() == 2, "candidate recovery preserves both hires and exact resources") var restarted := _contract_service(DOMAIN_COMPANY) var restart_retry: Dictionary = restarted.hire_recruit(HIRE_OP_2, second_id, 14) expect(restart_retry.ok and restart_retry.code == "already_hired" and String(restart_retry.receipt.receipt_hash) == String(second.receipt.receipt_hash), "restart retry returns second original receipt without charge") func verify_ui_two_hires() -> void: var company := _create_contract_company(UI_DRAFT, UI_COMPANY) if company.is_empty(): return var settings := SettingsStore.new().defaults() settings.setup_completed = true settings.locale = "en" settings.text_scale = 1.3 var shell = EntryShell.new() shell.auto_start = false shell.initial_company_service.path = UI_DRAFT shell.initial_company_service.company_path = UI_COMPANY root.add_child(shell) await _frames(3) shell.settings = settings shell.show_market(company) await _frames(4) var offer_ids: Array[String] = [] for offer in company.active_contract.market_board.recruit_offers: offer_ids.append(String(offer.source_offer_id)) await _touch(shell.find_child("MarketOffer%s" % offer_ids[0], true, false)) await _touch(shell.find_child("MarketReviewRecruit", true, false)) expect(shell.find_child("RecruitHire", true, false) != null and "2 → 3" in shell.find_child("RecruitReviewImpact", true, false).text, "first review exposes exact individual confirmation") await _touch(shell.find_child("RecruitHire", true, false)) expect(shell.current_screen == "market" and shell.find_child("MarketCompanyStatus", true, false).text == "Company 3 | Line capacity 4", "first touch hire updates visible Company separately from Line") expect(shell.find_child("MarketCoinStatus", true, false).text == "38 COIN" and shell.find_child("MarketReceiptNotice", true, false) != null, "first touch hire shows 38 Coin and receipt feedback") await _touch(shell.find_child("MarketOffer%s" % offer_ids[1], true, false)) await _touch(shell.find_child("MarketReviewRecruit", true, false)) expect("3 → 4" in shell.find_child("RecruitReviewImpact", true, false).text, "second review names its own Company consequence") await _touch(shell.find_child("RecruitHire", true, false)) expect(shell.current_screen == "market" and shell.find_child("MarketCompanyStatus", true, false).text == "Company 4 | Line capacity 4", "second touch hire reaches exact opening Company stop") expect(shell.find_child("MarketCoinStatus", true, false).text == "24 COIN", "second touch hire leaves exact 24 Coin") await _touch(shell.find_child("MarketOffer%s" % offer_ids[2], true, false)) await _touch(shell.find_child("MarketReviewRecruit", true, false)) expect(shell.find_child("RecruitHire", true, false) == null and shell.find_child("RecruitHireState", true, false) != null, "third offer remains inspectable but opening-phase commit is truthfully unavailable") var final_state: Dictionary = _draft_service(UI_DRAFT, UI_COMPANY).load_company().company expect(final_state.owned_recruits.size() == 4 and final_state.active_contract.hire_receipts.size() == 2 and int(final_state.active_contract.line_capacity) == 4, "UI path persists four owned Recruits, two receipts and Line four") shell.queue_free() await _frames(3) func _create_contract_company(draft_path: String, company_path: String) -> Dictionary: var service := _draft_service(draft_path, company_path) var draft: Dictionary = service.begin_or_load(SEED) if not draft.ok: expect(false, "fixture creates Initial Company draft") return {} service.set_selection([String(draft.draft.offers[0].offer_id), String(draft.draft.offers[2].offer_id)]) var committed: Dictionary = service.commit_company() if not committed.ok: expect(false, "fixture commits Initial Company") return {} var contract := _contract_service(company_path) var created: Dictionary = contract.create_contract(CONTRACT_OP, "CTR-R1-01", "standard") expect(created.ok, "fixture creates first Contract") return created.company if created.ok else {} func _draft_service(draft_path: String, company_path: String) -> RefCounted: var service := DraftService.new() service.path = draft_path service.company_path = company_path return service func _contract_service(company_path: String) -> RefCounted: var service := ContractService.new() service.company_path = company_path return service func expect_rejected_unchanged(service: RefCounted, call: Callable, code: String, company_path: String, label: String) -> void: var before: Dictionary = _draft_service(DOMAIN_DRAFT, company_path).load_company().company var result: Dictionary = call.call() var after: Dictionary = _draft_service(DOMAIN_DRAFT, company_path).load_company().company expect(not result.ok and result.code == code and String(after.company_hash) == String(before.company_hash), "%s rejects with %s and changes nothing" % [label, code]) func _touch(control: Control) -> void: if control == null: expect(false, "touch target exists") return var center := root.get_final_transform() * control.get_global_rect().get_center() var press := InputEventScreenTouch.new() press.position = center press.pressed = true press.index = 0 Input.parse_input_event(press) await _frames(2) var release := InputEventScreenTouch.new() release.position = center release.pressed = false release.index = 0 Input.parse_input_event(release) parsed_touches += 1 await _frames(5) func _write(file_path: String, text: String) -> void: var file := FileAccess.open(file_path, FileAccess.WRITE) file.store_string(text) file.close() func _remove_company_files(company_path: String) -> void: for suffix in ["", ".bak", ".candidate", ".bak.candidate", ".corrupt"]: var file_path := "%s%s" % [company_path, suffix] if FileAccess.file_exists(file_path): DirAccess.remove_absolute(file_path) func _cleanup_all() -> void: for base in [DOMAIN_DRAFT, DOMAIN_COMPANY, UI_DRAFT, UI_COMPANY]: for suffix in ["", ".bak", ".candidate", ".bak.candidate", ".corrupt"]: var file_path := "%s%s" % [base, suffix] if FileAccess.file_exists(file_path): DirAccess.remove_absolute(file_path) func _frames(count: int) -> void: for frame in range(count): await process_frame func _canonical(value): if value is float and is_equal_approx(value, round(value)): return int(round(value)) if value is Dictionary: var result := {} var keys: Array = value.keys() keys.sort_custom(func(left, right): return String(left) < String(right)) for key in keys: result[key] = _canonical(value[key]) return result if value is Array: var result: Array = [] for item in value: result.append(_canonical(item)) return result return value func expect(condition: bool, message: String) -> void: if not condition: failures.append("DEV-3 Hire: %s" % message)