Files
aetherbound-guild/runtime/tests/dev3/hire_transaction_test.gd
T

228 lines
12 KiB
GDScript

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)