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)