228 lines
12 KiB
GDScript
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)
|