Files
aetherbound-guild/runtime/tests/dev4/formation_risk_foundation_test.gd
T

255 lines
14 KiB
GDScript

extends SceneTree
const DraftService = preload("res://entry/initial_company_draft_service.gd")
const ContractService = preload("res://entry/guild_contract_service.gd")
const FormationService = preload("res://entry/formation_risk_service.gd")
const DRAFT_PATH := "user://dev4_foundation_draft.json"
const COMPANY_PATH := "user://dev4_foundation_company.json"
const SEED := "1032547698badcfeefcdab8967452301"
const CONTRACT_OP := "10101010202020203030303040404040"
const HIRE_OP_1 := "11112222333344445555666677778888"
const HIRE_OP_2 := "88887777666655554444333322221111"
const ORDER_OP := "abcdefabcdefabcdefabcdefabcdefab"
const ORDER_OP_2 := "1234567890abcdef1234567890abcdef"
const READY_OP := "fedcbafedcbafedcbafedcbafedcbafe"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
_cleanup()
var company := _create_contract_company()
if not company.is_empty():
verify_precondition(company)
company = _hire_opening_pair(company)
if not company.is_empty():
verify_foundation(company)
verify_order_and_readiness()
verify_restart_and_recovery()
_cleanup()
if failures.is_empty():
print("ABG_DEV4_FOUNDATION_OK schema=v2_in_place roster=4 auto_order=deterministic orientation=rear_left_front_right front=0 preview=swap,reset save=atomic,idempotent readiness=atomic,idempotent risk=steady,pressured,elite rewards=20,27,34 knockout=returns_next_stage battle_receipts=0 resources=24,line4 recovery=candidate,backup")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_precondition(company: Dictionary) -> void:
var before_hash := String(company.company_hash)
var result: Dictionary = _formation_service().begin_formation()
var after: Dictionary = _draft_service().load_company()
expect(not result.ok and result.code == "FORMATION_OPENING_HIRES_REQUIRED", "formation is unavailable before both opening hires")
expect(after.ok and String(after.company.company_hash) == before_hash and not after.company.active_contract.has("formation"), "rejected early entry changes no Company state")
func verify_foundation(company: Dictionary) -> void:
var preserved := {
"company_id": String(company.company_id),
"recruit_hashes": _recruit_hashes(company),
"coin": int(company.active_contract.coin),
"board_hash": String(company.active_contract.market_board.board_hash),
"hire_receipts": Array(company.active_contract.hire_receipts).duplicate(true),
"company_revision": int(company.company_revision),
}
var first: Dictionary = _formation_service().begin_formation()
expect(first.ok and first.code == "formation_created", "eligible Company creates one in-place formation foundation")
if not first.ok:
return
var formation: Dictionary = first.formation
expect(int(first.company.company_version) == 2 and int(first.company.company_revision) == preserved.company_revision, "formation extends schema v2 without falsifying economic revision")
expect(String(first.company.company_id) == preserved.company_id and _recruit_hashes(first.company) == preserved.recruit_hashes, "foundation preserves all four Recruit identities and hashes")
expect(int(first.company.active_contract.coin) == 24, "foundation preserves exact 24 Coin")
expect(String(first.company.active_contract.market_board.board_hash) == String(preserved.board_hash), "foundation preserves the Market board hash")
expect(_hash_array(first.company.active_contract.hire_receipts) == _hash_array(preserved.hire_receipts), "foundation preserves both hire receipts byte-for-byte")
expect(formation.base_front_to_rear_ids.size() == 4 and formation.base_front_to_rear_ids == formation.draft_front_to_rear_ids, "automatic order contains all four members exactly once")
expect(formation.orientation == "rear_left_front_right" and int(formation.front_position) == 0 and int(formation.relations[0].position) == 0, "stored line semantics fix rear-left and exposed front-right position zero")
expect(bool(formation.relations[0].first_threat_target) and not bool(formation.relations[3].first_threat_target), "first known threat targets the exposed front member")
expect(formation.risk_options.size() == 3, "foundation projects exactly three risk options")
var tiers: Array[String] = []
var rewards: Array[int] = []
for risk in range(3):
var option: Dictionary = formation.risk_options[risk]
tiers.append(String(option.tier_id))
rewards.append(int(option.reward_coin))
expect(String(option.risk_option_id) == String(first.company.active_contract.market_board.battle_offers[risk].offer_id), "risk %d retains the stable Market offer ID" % risk)
expect(String(option.knockout_rule_id) == "returns_next_stage" and int(option.known_threat.target_position) == 0, "risk %d names next-stage return and its front threat" % risk)
expect(tiers == ["steady", "pressured", "elite"] and rewards == [20, 27, 34], "risk tiers expose distinct exact first-round rewards")
var again: Dictionary = _formation_service().begin_formation()
expect(again.ok and again.code == "formation_loaded" and String(again.formation.formation_hash) == String(formation.formation_hash), "restart does not regenerate the formation or risk options")
func verify_order_and_readiness() -> void:
var service := _formation_service()
var loaded: Dictionary = service.begin_formation()
if not loaded.ok:
return
var base: Array = Array(loaded.formation.base_front_to_rear_ids).duplicate()
var swapped := base.duplicate()
var temporary = swapped[0]
swapped[0] = swapped[3]
swapped[3] = temporary
var before_hash := String(loaded.company.company_hash)
var preview: Dictionary = service.preview_order(swapped)
expect(preview.ok and preview.front_to_rear_ids == swapped and bool(preview.relations[0].first_threat_target), "swap preview moves the exposed target relation without committing")
expect(String(_draft_service().load_company().company.company_hash) == before_hash, "preview changes no persisted state")
var reset: Dictionary = service.reset_order()
expect(reset.ok and reset.front_to_rear_ids == base, "Reset restores deterministic automatic order")
expect_rejected_unchanged(func(): return service.preview_order([base[0], base[1], base[2]]), "FORMATION_COUNT_INVALID", "short order")
expect_rejected_unchanged(func(): return service.preview_order([base[0], base[0], base[2], base[3]]), "FORMATION_RECRUIT_DUPLICATE", "duplicate Recruit")
expect_rejected_unchanged(func(): return service.preview_order([base[0], base[1], base[2], "00000000000000000000000000000000"]), "FORMATION_RECRUIT_UNKNOWN", "unknown Recruit")
expect_rejected_unchanged(func(): return service.save_order("bad", swapped), "FORMATION_OPERATION_ID_INVALID", "malformed order operation")
var saved: Dictionary = service.save_order(ORDER_OP, swapped)
expect(saved.ok and saved.code == "order_saved" and saved.company.active_contract.formation.saved_front_to_rear_ids == swapped, "Save Order atomically persists the exact swapped order")
if not saved.ok:
return
expect(int(saved.company.active_contract.formation.order_revision) == 1 and saved.company.active_contract.formation.order_receipts.size() == 1, "first save creates one order revision and receipt")
var duplicate: Dictionary = service.save_order(ORDER_OP, swapped)
var recovered_retry: Dictionary = service.save_order(ORDER_OP_2, swapped)
expect(duplicate.ok and duplicate.code == "order_already_saved" and String(duplicate.receipt.receipt_hash) == String(saved.receipt.receipt_hash), "duplicate order operation returns the first receipt")
expect(recovered_retry.ok and recovered_retry.code == "order_already_saved" and String(recovered_retry.receipt.receipt_hash) == String(saved.receipt.receipt_hash), "new operation retry resolves the already-saved order intent")
var conflicting := base.duplicate()
expect_rejected_unchanged(func(): return service.save_order(ORDER_OP, conflicting), "FORMATION_OPERATION_CONFLICT", "reused order operation with changed intent")
expect_rejected_unchanged(func(): return service.accept_readiness(READY_OP, "0".repeat(64)), "READINESS_ORDER_STALE", "stale readiness order")
var order_hash := String(saved.receipt.order_hash)
var ready: Dictionary = service.accept_readiness(READY_OP, order_hash)
expect(ready.ok and ready.code == "readiness_accepted", "readiness accepts the frozen saved order once")
if not ready.ok:
return
expect(ready.snapshot.front_to_rear_ids == swapped and ready.snapshot.risk_option_ids.size() == 3 and ready.company.active_contract.formation.status == "ready", "prepared snapshot freezes members, order and three risk IDs")
expect(not ready.company.active_contract.has("battle_receipts") and not ready.company.active_contract.has("battle_checkpoint"), "DEV-4 foundation creates no battle receipt or simulation checkpoint")
var ready_retry: Dictionary = service.accept_readiness(READY_OP, order_hash)
expect(ready_retry.ok and ready_retry.code == "readiness_already_accepted" and String(ready_retry.receipt.receipt_hash) == String(ready.receipt.receipt_hash), "duplicate readiness returns the original receipt")
func verify_restart_and_recovery() -> void:
var expected: Dictionary = _draft_service().load_company()
if not expected.ok:
expect(false, "final foundation state loads before recovery checks")
return
var expected_hash := String(expected.company.company_hash)
var expected_formation_hash := String(expected.company.active_contract.formation.formation_hash)
_write(COMPANY_PATH, "{broken primary")
var backup: Dictionary = _formation_service().load_company()
expect(backup.ok and backup.code == "recovered_backup", "valid backup repairs a corrupt foundation primary")
expect(String(backup.company.active_contract.formation.formation_hash) == expected_formation_hash, "backup recovery preserves formation, readiness and risk state")
var recovered_company: Dictionary = backup.company.duplicate(true)
_remove_company_files()
_write("%s.candidate" % COMPANY_PATH, JSON.stringify(_canonical(recovered_company)))
var candidate: Dictionary = _formation_service().load_company()
expect(candidate.ok and candidate.code == "recovered_candidate", "verified interrupted candidate promotes on restart")
expect(String(candidate.company.company_hash) == expected_hash and String(candidate.company.active_contract.formation.formation_hash) == expected_formation_hash, "candidate recovery is byte-stable at the DEV-4 foundation boundary")
func _create_contract_company() -> Dictionary:
var draft: Dictionary = _draft_service().begin_or_load(SEED)
if not draft.ok:
expect(false, "fixture creates Initial Company draft")
return {}
var selected: Dictionary = _draft_service().set_selection([String(draft.draft.offers[0].offer_id), String(draft.draft.offers[2].offer_id)])
if not selected.ok:
expect(false, "fixture selects opening pair")
return {}
var committed: Dictionary = _draft_service().commit_company()
if not committed.ok:
expect(false, "fixture commits opening Company")
return {}
var created: Dictionary = _contract_service().create_contract(CONTRACT_OP, "CTR-R1-01", "standard")
expect(created.ok, "fixture creates first Contract")
return created.company if created.ok else {}
func _hire_opening_pair(company: Dictionary) -> Dictionary:
var offers: Array = company.active_contract.market_board.recruit_offers
var first: Dictionary = _contract_service().hire_recruit(HIRE_OP_1, String(offers[0].source_offer_id), 14)
var second: Dictionary = _contract_service().hire_recruit(HIRE_OP_2, String(offers[1].source_offer_id), 14) if first.ok else {"ok": false}
expect(first.ok and second.ok, "fixture completes two independent opening hires")
return second.company if second.ok else {}
func expect_rejected_unchanged(call: Callable, code: String, label: String) -> void:
var before: Dictionary = _draft_service().load_company()
var result: Dictionary = call.call()
var after: Dictionary = _draft_service().load_company()
expect(before.ok and after.ok and not result.ok and result.code == code and String(after.company.company_hash) == String(before.company.company_hash), "%s rejects with %s and changes nothing" % [label, code])
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 _formation_service() -> RefCounted:
var service := FormationService.new()
service.company_path = COMPANY_PATH
return service
func _recruit_hashes(company: Dictionary) -> Array[String]:
var hashes: Array[String] = []
for recruit in company.owned_recruits:
hashes.append(String(recruit.recruit_hash))
return hashes
func _hash_array(value: Array) -> String:
return JSON.stringify(_canonical(value)).sha256_text()
func _cleanup() -> void:
for path in [DRAFT_PATH, "%s.bak" % DRAFT_PATH, "%s.candidate" % DRAFT_PATH, "%s.bak.candidate" % DRAFT_PATH, COMPANY_PATH, "%s.bak" % COMPANY_PATH, "%s.candidate" % COMPANY_PATH, "%s.bak.candidate" % COMPANY_PATH, "%s.corrupt" % COMPANY_PATH]:
if FileAccess.file_exists(path):
DirAccess.remove_absolute(path)
func _remove_company_files() -> void:
for path in [COMPANY_PATH, "%s.bak" % COMPANY_PATH, "%s.candidate" % COMPANY_PATH, "%s.bak.candidate" % COMPANY_PATH, "%s.corrupt" % COMPANY_PATH]:
if FileAccess.file_exists(path):
DirAccess.remove_absolute(path)
func _write(path: String, content: String) -> void:
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
expect(false, "fixture writes %s" % path)
return
file.store_string(content)
file.close()
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-4 foundation: %s" % message)