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

214 lines
11 KiB
GDScript

extends SceneTree
const DraftService = preload("res://entry/initial_company_draft_service.gd")
const ContractService = preload("res://entry/guild_contract_service.gd")
const DRAFT_PATH := "user://dev3_contract_foundation_draft.json"
const COMPANY_PATH := "user://dev3_contract_foundation_company.json"
const SEED := "2468ace013579bdfeca86420fdb97531"
const OPERATION_ID := "abcdef0123456789abcdef0123456789"
const OTHER_OPERATION_ID := "0123456789abcdef0123456789abcdef"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
_cleanup()
verify_v1_migration()
verify_contract_creation_and_restart()
verify_retry_conflict_and_validation()
verify_backup_and_candidate_recovery()
_cleanup()
if failures.is_empty():
print("ABG_DEV3_CONTRACT_OK migration=v1_to_v2 company=preserved contract=atomic retry=idempotent conflict=stable coin=52 line=4 board=3,4,3 prices=14+14 receipts=true restart=true backup=true candidate=true")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_v1_migration() -> void:
var created := _create_company()
if created.is_empty():
return
var original_id := String(created.company_id)
var original_recruits := _recruit_hashes(created)
var legacy := created.duplicate(true)
legacy.company_version = 1
for field in ["company_revision", "next_contract_ordinal", "active_contract", "contract_receipts"]:
legacy.erase(field)
_refresh_hash(legacy, "company_hash")
_remove_company_files()
_write(COMPANY_PATH, JSON.stringify(_canonical(legacy)))
var loaded: Dictionary = _draft_service().load_company()
expect(loaded.ok and loaded.code == "migrated_v1", "a valid DEV-2 Company migrates exactly once")
if not loaded.ok:
return
expect(int(loaded.company.company_version) == 2 and int(loaded.company.company_revision) == 0, "migration establishes v2 revision zero")
expect(String(loaded.company.company_id) == original_id and _recruit_hashes(loaded.company) == original_recruits, "migration preserves Company and Recruit identity byte facts")
expect(loaded.company.active_contract.is_empty() and loaded.company.contract_receipts.is_empty() and int(loaded.company.next_contract_ordinal) == 0, "migration adds no Contract or transaction")
var restarted: Dictionary = _draft_service().load_company()
expect(restarted.ok and restarted.code == "loaded" and String(restarted.company.company_hash) == String(loaded.company.company_hash), "restart reads the migrated authoritative Company without a second migration")
func verify_contract_creation_and_restart() -> void:
var service := _contract_service()
var created: Dictionary = service.create_contract(OPERATION_ID, "CTR-R1-01", "standard")
expect(created.ok and created.code == "created", "explicit confirm creates the first Contract")
if not created.ok:
return
var company: Dictionary = created.company
var contract: Dictionary = created.contract
expect(int(company.company_revision) == 1 and int(company.next_contract_ordinal) == 1, "Contract creation advances Company revision and ordinal once")
expect(company.owned_recruits.size() == 2 and _draft_service().validate_company(company).ok, "Contract creation preserves the opening two and validates the one-file state")
expect(int(contract.coin) == 52 and int(contract.line_capacity) == 4 and contract.status == "market", "first Contract begins at the exact Market resources")
var board: Dictionary = contract.market_board
expect(board.recruit_offers.size() == 3 and board.equipment_offers.size() == 4 and board.battle_offers.size() == 3, "first Market persists the exact 3/4/3 offer shape")
var first_two_cost := 0
for index in range(board.recruit_offers.size()):
var offer: Dictionary = board.recruit_offers[index]
expect(int(offer.offer_level) == 1 and String(offer.profession_id).begins_with("PF-B"), "Recruit offer %d is a level-1 base Profession" % index)
expect(int(offer.list_price) == 14, "Recruit offer %d has the protected first-Market price" % index)
if index < 2:
first_two_cost += int(offer.list_price)
expect(first_two_cost == 28 and int(contract.coin) - first_two_cost == 24, "two separate protected hires total 28 and leave 24 Coin")
expect(contract.available_recruit_offer_ids.size() == 3 and contract.available_equipment_offer_ids.size() == 4 and contract.available_battle_offer_ids.size() == 3, "all generated offers begin available")
expect(contract.hire_receipts.is_empty() and company.contract_receipts == [contract.creation_receipt], "creation records one Contract receipt and no fake hire")
var restarted: Dictionary = _contract_service().load_company()
expect(restarted.ok and String(restarted.company.active_contract.market_board.board_hash) == String(board.board_hash), "restart preserves the exact first Market board")
expect(String(restarted.company.active_contract.creation_receipt.receipt_hash) == String(created.receipt.receipt_hash), "restart preserves the exact creation receipt")
func verify_retry_conflict_and_validation() -> void:
var service := _contract_service()
var before: Dictionary = service.load_company().company
var duplicate: Dictionary = service.create_contract(OPERATION_ID, "CTR-R1-01", "standard")
expect(duplicate.ok and duplicate.code == "already_created" and String(duplicate.receipt.receipt_hash) == String(before.active_contract.creation_receipt.receipt_hash), "duplicate confirm returns the original receipt")
var recovery_retry: Dictionary = service.create_contract(OTHER_OPERATION_ID, "CTR-R1-01", "standard")
expect(recovery_retry.ok and recovery_retry.code == "already_created" and String(recovery_retry.receipt.receipt_hash) == String(before.active_contract.creation_receipt.receipt_hash), "a recovered new operation ID resolves to the same Contract")
expect_rejected_unchanged(func(): return service.create_contract(OPERATION_ID, "CTR-R1-01", "wayfinder"), "CONTRACT_OPERATION_CONFLICT", "reused operation ID with changed difficulty")
expect_rejected_unchanged(func(): return service.create_contract(OTHER_OPERATION_ID, "CTR-R1-02", "standard"), "CONTRACT_LOCKED", "locked Contract")
expect_rejected_unchanged(func(): return service.create_contract(OTHER_OPERATION_ID, "CTR-R1-01", "unknown"), "CONTRACT_DIFFICULTY_INVALID", "unknown difficulty")
expect_rejected_unchanged(func(): return service.create_contract("bad-id", "CTR-R1-01", "standard"), "CONTRACT_OPERATION_ID_INVALID", "malformed operation ID")
var invalid: Dictionary = before.duplicate(true)
invalid.active_contract.market_board.recruit_offers.pop_back()
_refresh_hash(invalid.active_contract.market_board, "board_hash")
_refresh_hash(invalid.active_contract, "contract_hash")
_refresh_hash(invalid, "company_hash")
expect(_draft_service().validate_company(invalid).code == "CONTRACT_RECRUIT_OFFER_COUNT_INVALID", "hash-consistent incomplete Market board is rejected")
func verify_backup_and_candidate_recovery() -> void:
var expected: Dictionary = _contract_service().load_company().company
_write(COMPANY_PATH, "{broken primary")
var backup: Dictionary = _contract_service().load_company()
expect(backup.ok and backup.code == "recovered_backup", "valid backup repairs a corrupt Contract primary")
expect(String(backup.company.active_contract.contract_hash) == String(expected.active_contract.contract_hash), "backup recovery preserves the active Contract")
_remove_company_files()
_write("%s.candidate" % COMPANY_PATH, JSON.stringify(_canonical(expected)))
var candidate: Dictionary = _contract_service().load_company()
expect(candidate.ok and candidate.code == "recovered_candidate", "verified interrupted candidate promotes on restart")
expect(String(candidate.company.active_contract.market_board.board_hash) == String(expected.active_contract.market_board.board_hash), "candidate recovery preserves the fixed Market")
expect(FileAccess.file_exists(COMPANY_PATH) and not FileAccess.file_exists("%s.candidate" % COMPANY_PATH), "candidate recovery leaves one authoritative primary")
func _create_company() -> Dictionary:
var service := _draft_service()
var draft: Dictionary = service.begin_or_load(SEED)
if not draft.ok:
expect(false, "fixture creates the Initial Company draft")
return {}
var ids := [String(draft.draft.offers[0].offer_id), String(draft.draft.offers[1].offer_id)]
var selected: Dictionary = service.set_selection(ids)
if not selected.ok:
expect(false, "fixture selects exactly two Recruits")
return {}
var committed: Dictionary = service.commit_company()
expect(committed.ok, "fixture commits the persistent Company")
return committed.company if committed.ok else {}
func _draft_service() -> RefCounted:
var service := DraftService.new()
service.path = DRAFT_PATH
service.company_path = COMPANY_PATH
return service
func _contract_service() -> RefCounted:
var service := ContractService.new()
service.company_path = COMPANY_PATH
return service
func _recruit_hashes(company: Dictionary) -> Array[String]:
var result: Array[String] = []
for recruit in company.owned_recruits:
result.append(String(recruit.recruit_hash))
return result
func expect_rejected_unchanged(call: Callable, code: String, label: String) -> void:
var before: Dictionary = _contract_service().load_company().company
var result: Dictionary = call.call()
var after: Dictionary = _contract_service().load_company().company
expect(not result.ok and result.code == code and after == before, "%s rejects with %s and changes nothing" % [label, code])
func _refresh_hash(value: Dictionary, field: String) -> void:
value.erase(field)
value[field] = _hash(value)
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() -> 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() -> void:
for base in [DRAFT_PATH, COMPANY_PATH]:
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 _hash(value) -> String:
return JSON.stringify(_canonical(value)).sha256_text()
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 Contract foundation: %s" % message)