test: add M45 contract return safety coverage

This commit is contained in:
2026-08-26 14:01:26 +08:00
parent 14735c773d
commit ecf0b8a6fd
@@ -0,0 +1,297 @@
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 FormationService = preload("res://entry/formation_risk_service.gd")
const DRAFT_PATH := "user://m45_return_safety_draft.json"
const COMPANY_PATH := "user://m45_return_safety_company.json"
const SEED := "13579bdf2468ace0fedcba9876543210"
const CONTRACT_OP := "abcdefabcdefabcdefabcdefabcdefab"
const RETURN_OP := "deadbeefdeadbeefdeadbeefdeadbeef"
const CONFLICT_OP := "0123456789abcdef0123456789abcdef"
const HIRE_OP_1 := "11112222333344445555666677778888"
const HIRE_OP_2 := "88887777666655554444333322221111"
var failures: Array[String] = []
var parsed_touches := 0
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
_cleanup()
var market := _create_market_company()
expect(not market.is_empty(), "fixture creates a valid pre-return Market Company")
if not market.is_empty():
verify_legacy_market_compatibility(market)
await verify_cancel_no_mutation()
_cleanup()
var returned_fixture := _create_expanded_company()
if not returned_fixture.is_empty():
verify_return_receipt_and_retry()
verify_receipt_validation(returned_fixture)
_cleanup()
var locked_fixture := _create_expanded_company()
if not locked_fixture.is_empty():
verify_formation_lock()
_cleanup()
if failures.is_empty():
print("ABG_M45_CONTRACT_RETURN_SAFETY_OK cancel_no_mutation=true receipt_identity=true receipt_validation=true confirm_once=true duplicate_retry=true conflict_reject=true restart_persistence=true formation_lock=true legacy_market_compat=true parsed_touch=%d" % parsed_touches)
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_legacy_market_compatibility(company: Dictionary) -> void:
var legacy := company.duplicate(true)
legacy.active_contract.erase("contract_return_receipts")
_rehash_company(legacy)
var validation: Dictionary = _draft_service().validate_company(legacy)
expect(validation.ok, "pre-M45 Market save without contract_return_receipts remains valid")
var saved: Dictionary = _draft_service().save_company(legacy)
expect(saved.ok, "pre-M45 Market save can be written without an empty receipt array")
var loaded: Dictionary = _draft_service().load_company()
expect(loaded.ok and not loaded.company.active_contract.has("contract_return_receipts"), "legacy Market reload preserves optional-field compatibility")
func verify_cancel_no_mutation() -> void:
_cleanup()
var company := _create_market_company()
expect(not company.is_empty(), "cancel fixture creates a Market Company")
if company.is_empty():
return
var before: Dictionary = _draft_service().load_company()
var shell := _shell()
shell.show_market(company)
await _frames(3)
var return_button := shell.find_child("MarketReturnContract", true, false) as Button
expect(return_button != null and not return_button.disabled, "cancel fixture exposes an enabled return command")
if return_button == null:
shell.queue_free()
return
await _touch(return_button)
var cancel := shell.find_child("ContractReturnCancel", true, false) as Button
expect(shell.current_screen == "contract_return_review" and cancel != null, "return command opens the existing review")
if cancel != null:
await _touch(cancel)
var after: Dictionary = _draft_service().load_company()
expect(shell.current_screen == "market", "Cancel returns to the same Market")
expect(before.ok and after.ok and String(after.company.company_hash) == String(before.company.company_hash), "Cancel changes no persisted Company bytes")
if after.ok:
expect(String(after.company.active_contract.status) == "market" and Array(after.company.active_contract.get("contract_return_receipts", [])).is_empty(), "Cancel creates no return receipt")
shell.queue_free()
await _timer_seconds(0.1)
func verify_return_receipt_and_retry() -> void:
var service := _contract_service()
var before: Dictionary = service.load_company()
if not before.ok:
expect(false, "return fixture loads before confirmation")
return
var before_ids := _owned_ids(before.company)
var before_coin := int(before.company.active_contract.coin)
var before_revision := int(before.company.company_revision)
var applied: Dictionary = service.return_contract(RETURN_OP)
expect(applied.ok and applied.code == "contract_returned", "Confirm commits Contract Return once")
if not applied.ok:
return
var returned: Dictionary = applied.company
var receipts: Array = Array(returned.active_contract.get("contract_return_receipts", []))
expect(String(returned.active_contract.status) == "returned", "Confirm marks the Contract returned")
expect(receipts.size() == 1, "Confirm writes exactly one return receipt")
expect(_owned_ids(returned) == before_ids and int(returned.active_contract.coin) == before_coin, "Confirm retains Company identities and Coin")
expect(int(returned.company_revision) == before_revision + 1, "Confirm advances Company revision exactly once")
if receipts.is_empty():
return
var receipt: Dictionary = receipts[0]
expect(String(receipt.company_id) == String(returned.company_id) and String(receipt.run_id) == String(returned.active_contract.run_id) and String(receipt.contract_id) == String(returned.active_contract.contract_id), "Receipt identity names Company, run and Contract")
expect(String(receipt.destination) == "guild" and int(receipt.retained_coin) == before_coin and receipt.retained_recruit_ids == before_ids, "Receipt records retained values and Guild destination")
expect(String(receipt.operation_id) == RETURN_OP and String(receipt.intent_hash).length() == 64 and String(receipt.receipt_hash).length() == 64, "Receipt carries operation, intent and content hashes")
var restarted: Dictionary = _draft_service().load_company()
expect(restarted.ok and String(restarted.company.company_hash) == String(returned.company_hash), "Restart preserves returned Company state")
var duplicate: Dictionary = _contract_service().return_contract(RETURN_OP)
expect(duplicate.ok and duplicate.code == "contract_return_already_applied" and String(duplicate.receipt.get("receipt_hash", "")) == String(receipt.get("receipt_hash", "")), "Same operation returns the original receipt")
var after_duplicate: Dictionary = _draft_service().load_company()
expect(after_duplicate.ok and String(after_duplicate.company.company_hash) == String(returned.company_hash) and Array(after_duplicate.company.active_contract.contract_return_receipts).size() == 1, "Duplicate retry creates no second receipt or revision")
var conflict: Dictionary = _contract_service().return_contract(CONFLICT_OP)
expect(not conflict.ok and conflict.code == "CONTRACT_RETURN_ALREADY_APPLIED", "Different operation is rejected after return")
var after_conflict: Dictionary = _draft_service().load_company()
expect(after_conflict.ok and String(after_conflict.company.company_hash) == String(returned.company_hash), "Conflicting retry leaves state unchanged")
func verify_receipt_validation(_market_company: Dictionary) -> void:
_cleanup()
var fixture := _create_market_company()
var applied: Dictionary = _contract_service().return_contract(RETURN_OP)
expect(not fixture.is_empty() and applied.ok, "validator fixture commits a valid return")
if not applied.ok:
return
var valid: Dictionary = applied.company
var cases := [
{"field": "operation_id", "value": "bad", "code": "CONTRACT_RETURN_RECEIPT_ID_INVALID"},
{"field": "intent_hash", "value": "0".repeat(64), "code": "CONTRACT_RETURN_RECEIPT_INTENT_INVALID"},
{"field": "retained_recruit_ids", "value": ["00000000000000000000000000000000"], "code": "CONTRACT_RETURN_RECEIPT_ROSTER_INVALID"},
{"field": "destination", "value": "market", "code": "CONTRACT_RETURN_RECEIPT_STATE_INVALID"},
{"field": "receipt_hash", "value": "0".repeat(64), "code": "CONTRACT_RETURN_RECEIPT_HASH_INVALID"},
]
for item in cases:
var tampered := valid.duplicate(true)
tampered.active_contract.contract_return_receipts[0][String(item.field)] = item.value
_rehash_company(tampered)
var result: Dictionary = _draft_service().validate_company(tampered)
expect(not result.ok and String(result.code) == String(item.code), "Receipt tamper %s is rejected with %s" % [String(item.field), String(item.code)])
func verify_formation_lock() -> void:
var formation := FormationService.new()
formation.company_path = COMPANY_PATH
var begun: Dictionary = formation.begin_formation()
expect(begun.ok, "Formation fixture opens before Contract Return")
if not begun.ok:
return
var before: Dictionary = _draft_service().load_company()
var blocked: Dictionary = _contract_service().return_contract(CONFLICT_OP)
var after: Dictionary = _draft_service().load_company()
expect(not blocked.ok and blocked.code == "CONTRACT_RETURN_FORMATION_LOCKED", "Formation lock directly blocks Contract Return")
expect(before.ok and after.ok and String(after.company.company_hash) == String(before.company.company_hash), "Formation-locked return changes no Company state")
if after.ok:
expect(Array(after.company.active_contract.get("contract_return_receipts", [])).is_empty() and String(after.company.active_contract.status) == "market", "Formation lock creates no receipt and keeps Market status")
func _create_market_company() -> Dictionary:
var draft_service := _draft_service()
var draft: Dictionary = draft_service.begin_or_load(SEED)
if not draft.ok:
return {}
var selected: Dictionary = draft_service.set_selection([String(draft.draft.offers[0].offer_id), String(draft.draft.offers[1].offer_id)])
var committed: Dictionary = draft_service.commit_company()
if not selected.ok or not committed.ok:
return {}
return _contract_service().create_contract(CONTRACT_OP, "CTR-R1-01", "standard").get("company", {})
func _create_expanded_company() -> Dictionary:
var company := _create_market_company()
if company.is_empty():
return {}
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)
return second.company if first.ok and second.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 _shell() -> Control:
root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
root.size = Vector2i(844, 390)
var shell := EntryShell.new()
shell.auto_start = false
shell.initial_company_service.path = DRAFT_PATH
shell.initial_company_service.company_path = COMPANY_PATH
root.add_child(shell)
var settings := SettingsStore.new().defaults()
settings.setup_completed = true
settings.locale = "en"
settings.text_scale = 1.0
shell.settings = settings
shell.setup_draft = settings.duplicate(true)
return shell
func _owned_ids(company: Dictionary) -> Array[String]:
var ids: Array[String] = []
for recruit in company.get("owned_recruits", []):
ids.append(String(recruit.get("recruit_id", "")))
return ids
func _rehash_company(company: Dictionary) -> void:
var contract: Dictionary = company.active_contract
contract.erase("contract_hash")
contract.contract_hash = _hash(contract)
company.active_contract = contract
company.erase("company_hash")
company.company_hash = _hash(company)
func _touch(control: Control) -> void:
if control == null:
expect(false, "touch target exists")
return
var center := 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(4)
func _frames(count: int) -> void:
for _index in range(count):
await process_frame
func _timer_seconds(seconds: float) -> void:
await create_timer(seconds).timeout
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 expect(condition: bool, message: String) -> void:
if not condition:
failures.append("M45 Contract Return safety: %s" % message)
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