Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
This commit is contained in:
@@ -27,6 +27,8 @@ func _run() -> void:
|
||||
_test_missing_actor_releases_lock_and_cost()
|
||||
_test_content_replacement_boundary()
|
||||
_test_transaction_policies()
|
||||
_test_failed_rollback_is_fatal()
|
||||
_test_failed_cost_release_retains_tokens()
|
||||
_test_event_snapshot_round_trip_and_atomicity()
|
||||
_test_event_recovery_lock_scope_and_strict_filter_parse()
|
||||
if _failures == 0:
|
||||
@@ -439,3 +441,135 @@ func _expect(condition: bool, label: String) -> void:
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" not ok %s" % label)
|
||||
|
||||
|
||||
func _test_failed_rollback_is_fatal() -> void:
|
||||
var cases: Array[Dictionary] = [
|
||||
{&"name":&"rollback",&"policy":EventDefinitionType.TransactionPolicy.ROLLBACK_ALL},
|
||||
{&"name":&"branch",&"policy":EventDefinitionType.TransactionPolicy.BRANCH_FAILURE},
|
||||
{&"name":&"automatic",&"policy":EventDefinitionType.TransactionPolicy.BRANCH_FAILURE,&"auto":true},
|
||||
{&"name":&"commit",&"policy":EventDefinitionType.TransactionPolicy.ROLLBACK_ALL,&"commit":true},
|
||||
{&"name":&"skip_self",&"policy":EventDefinitionType.TransactionPolicy.SKIP_FAILED,&"self":true},
|
||||
{&"name":&"rollback_self",&"policy":EventDefinitionType.TransactionPolicy.ROLLBACK_ALL,&"self":true},
|
||||
{&"name":&"branch_self",&"policy":EventDefinitionType.TransactionPolicy.BRANCH_FAILURE,&"self":true},
|
||||
]
|
||||
for row: Dictionary in cases:
|
||||
var calls: Array[StringName] = []
|
||||
var participant := {&"value":0,&"branch":0,&"released":0}
|
||||
var service := EventServiceType.new().configure_callbacks(
|
||||
Callable(),
|
||||
func(action: EventAction, phase: StringName, _context: Dictionary, token: Variant) -> Dictionary:
|
||||
if phase == &"validate": return {&"ok":true}
|
||||
if phase == &"rollback":
|
||||
calls.append(action.action_id)
|
||||
if action.action_id == &"second": return {&"ok":false,&"error":"explicit participant refusal"}
|
||||
participant[&"value"] = int(token)
|
||||
return {&"ok":true}
|
||||
if action.action_id == &"fallback": participant[&"branch"] += 1
|
||||
if bool(action.payload.get(&"fail",false)):
|
||||
if row.get(&"self",false): participant[&"value"] += 7
|
||||
return {&"ok":false,&"fatal":bool(row.get(&"self",false)),&"error":"explicit failed apply"}
|
||||
var previous: int = participant[&"value"]
|
||||
participant[&"value"] += int(action.payload.get(&"delta",0))
|
||||
return {&"ok":true,&"token":previous},
|
||||
func(_cost: Dictionary, _context: Dictionary) -> Dictionary: return {&"ok":true,&"token":&"reserved"},
|
||||
func(_token: Variant, _context: Dictionary) -> Dictionary:
|
||||
participant[&"released"] += 1
|
||||
return {&"ok":true},
|
||||
func(_token: Variant, _context: Dictionary) -> Dictionary: return {&"ok":not bool(row.get(&"commit",false)),&"error":"explicit commit refusal"}
|
||||
)
|
||||
var event_id := StringName("failed.rollback."+String(row[&"name"]))
|
||||
var actions: Array = [_delta_action(&"first",2),_delta_action(&"second",3)]
|
||||
if not row.get(&"commit",false): actions.append(_failing_action(&"fail"))
|
||||
var definition := _choice_definition(event_id,10,row[&"policy"],actions,{&"gold":1})
|
||||
definition.failure_actions = [_delta_action(&"fallback",4)]
|
||||
if row.get(&"auto",false):
|
||||
definition.foreground = false
|
||||
definition.choices = []
|
||||
definition.automatic_actions.assign(actions)
|
||||
_expect(service.register_definition(definition).is_success(),"rollback refusal definition registers")
|
||||
var instance_id := StringName(String(event_id)+".instance")
|
||||
var result: CommandResult
|
||||
if row.get(&"auto",false):
|
||||
_expect(service.enqueue_candidate(event_id,[],0,{&"event_instance_id":instance_id}).is_success(),"automatic rollback refusal enqueue")
|
||||
result = service.start_next_event(0)
|
||||
else: result = _run_choice(service,event_id,instance_id)
|
||||
_expect(not result.is_success() and bool(result.details.get(&"fatal",false)),"refused rollback or failed self restoration propagates fatal outcome")
|
||||
_expect(service.get_instance(instance_id).state == EventInstanceType.State.ERROR and service.pause_requested,"fatal event stops at ERROR with pause requested")
|
||||
_expect(participant[&"branch"] == 0,"no fallback executes on unverified rollback")
|
||||
if row[&"policy"] == EventDefinitionType.TransactionPolicy.SKIP_FAILED:
|
||||
_expect(calls.is_empty() and participant[&"value"] == 12,"skip policy stops on fatal self restoration without further actions")
|
||||
else:
|
||||
_expect(calls == [&"second",&"first"] and participant[&"value"] == 0,"all prior restorations attempted in reverse order despite refusal")
|
||||
if not row.get(&"auto",false):
|
||||
_expect(participant[&"released"] == 1,"fatal choice releases reservation once")
|
||||
var instance: EventInstance = service.get_instance(instance_id)
|
||||
var before: Dictionary = service.create_snapshot()
|
||||
var replay := service.submit_choice(instance_id,instance.choice_instance_id,&"accept",0)
|
||||
_expect(replay.is_replay and not replay.is_success() and bool(replay.details.get(&"fatal",false)),"fatal outcome is cached for choice replay")
|
||||
_expect(var_to_bytes(before) == var_to_bytes(service.create_snapshot()) and participant[&"released"] == 1,"fatal replay does not mutate event state or release twice")
|
||||
_expect(service.validate_invariants()[&"ok"],"fatal event lock and queue invariants remain valid")
|
||||
|
||||
|
||||
func _test_failed_cost_release_retains_tokens() -> void:
|
||||
for mode: StringName in [&"action", &"commit", &"cancel", &"missing", &"finish", &"no_releaser"]:
|
||||
var calls: Array[StringName] = []
|
||||
var state := {&"value": 0, &"available": true, &"refuse": true}
|
||||
var service := EventServiceType.new().configure_callbacks(
|
||||
func(_actor: StringName) -> bool: return state[&"available"],
|
||||
func(action: EventAction, phase: StringName, _context: Dictionary, token: Variant) -> Dictionary:
|
||||
if phase == &"apply":
|
||||
if bool(action.payload.get(&"fail", false)): return {&"ok": false, &"error": "explicit apply refusal"}
|
||||
var before := int(state[&"value"])
|
||||
state[&"value"] += int(action.payload.get(&"delta", 0))
|
||||
return {&"ok": true, &"token": before}
|
||||
if phase == &"rollback": state[&"value"] = int(token)
|
||||
return {&"ok": true},
|
||||
func(cost: Dictionary, _context: Dictionary) -> Dictionary: return {&"ok": true, &"token": cost.duplicate(true)},
|
||||
func(token: Variant, _context: Dictionary) -> Dictionary:
|
||||
calls.append(StringName(str(token.get(&"gold", 0))))
|
||||
return {&"ok": not state[&"refuse"], &"error": "explicit release refusal"},
|
||||
func(_token: Variant, _context: Dictionary) -> Dictionary: return {&"ok": mode != &"commit", &"error": "explicit commit refusal"}
|
||||
)
|
||||
var actions: Array = [_delta_action(&"primary", 4)]
|
||||
if mode in [&"action", &"no_releaser"]: actions.append(_failing_action(&"fail"))
|
||||
var definition := _choice_definition(StringName("release_" + mode), 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, actions, {&"gold": 100})
|
||||
definition.choices.append(EventChoiceType.new().configure(&"other", &"other", [], {&"gold": 200}, {}, []))
|
||||
_expect(service.register_definition(definition).is_success(), "%s release definition registers" % mode)
|
||||
service.enqueue_candidate(definition.event_id, [&"actor"], 0, {&"event_instance_id": &"release_instance"})
|
||||
var started := service.start_next_event(0)
|
||||
var instance := service.get_instance(&"release_instance")
|
||||
_expect(service.reserve_choice_cost(instance.event_instance_id, &"accept", 0).is_success(), "%s actual reservation created" % mode)
|
||||
if mode in [&"action", &"commit", &"cancel", &"missing", &"finish"]:
|
||||
_expect(service.reserve_choice_cost(instance.event_instance_id, &"other", 0).is_success(), "%s second reservation created" % mode)
|
||||
if mode == &"no_releaser": service._cost_releaser = Callable()
|
||||
var result: CommandResult
|
||||
if mode == &"cancel":
|
||||
result = service.cancel_event(instance.event_instance_id)
|
||||
elif mode == &"missing":
|
||||
state[&"available"] = false
|
||||
service.audit_actor_availability(1)
|
||||
else:
|
||||
result = service.submit_choice(instance.event_instance_id, started.details[&"choice_instance_id"], &"accept", 0)
|
||||
if result != null:
|
||||
_expect(not result.is_success() and result.details.get(&"fatal", false), "%s release failure cannot report success" % mode)
|
||||
_expect(instance.state == EventInstanceType.State.ERROR and service.pause_requested, "%s release failure stops as ERROR" % mode)
|
||||
_expect(service.get_actor_locks().is_empty() and service.get_foreground_instance() == null, "%s releases event ownership" % mode)
|
||||
var expected_ids: Array[StringName] = [&"accept"]
|
||||
if mode in [&"action", &"commit", &"cancel", &"missing"]: expected_ids.append(&"other")
|
||||
if mode == &"finish": expected_ids = [&"other"]
|
||||
_expect(instance.reserved_costs.keys().size() == expected_ids.size(), "%s retains every refused token only" % mode)
|
||||
for id: StringName in expected_ids:
|
||||
_expect(instance.reserved_costs.has(id), "%s retains token %s" % [mode, id])
|
||||
var expected_calls := 2 if mode in [&"action", &"commit", &"cancel", &"missing"] else 0 if mode == &"no_releaser" else 1
|
||||
_expect(calls.size() == expected_calls, "%s attempts each release once" % mode)
|
||||
_expect(state[&"value"] == (4 if mode == &"finish" else 0), "%s retains actual action outcome" % mode)
|
||||
var snapshot := service.create_snapshot()
|
||||
var restored := EventServiceType.new()
|
||||
restored.register_definition(definition)
|
||||
_expect(restored.restore_snapshot(snapshot) and restored.create_snapshot() == snapshot, "%s ERROR with unreleased tokens restores whole" % mode)
|
||||
_expect(restored.validate_invariants()[&"ok"], "%s retained ERROR token invariants pass" % mode)
|
||||
if mode not in [&"cancel", &"missing"]:
|
||||
var before := service.create_snapshot()
|
||||
var replay := service.submit_choice(instance.event_instance_id, started.details[&"choice_instance_id"], &"accept", 0)
|
||||
_expect(not replay.is_success() and replay.is_replay and service.create_snapshot() == before and calls.size() == expected_calls, "%s failure cache replay never refunds again" % mode)
|
||||
|
||||
@@ -17,6 +17,10 @@ func _run() -> void:
|
||||
_test_paid_choice_escrow_and_commit()
|
||||
_test_missing_actor_releases_reservation_and_lock()
|
||||
_test_waiting_choice_snapshot_exact_replay()
|
||||
_test_event_ledger_observer_boundaries()
|
||||
_test_paid_choice_attempt_lifecycle()
|
||||
_test_legacy_paid_choice_tokens()
|
||||
_test_paid_reservation_failure_boundaries()
|
||||
_cleanup()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
@@ -423,3 +427,195 @@ func _expect(condition: bool, label: String) -> void:
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
|
||||
|
||||
func _test_event_ledger_observer_boundaries() -> void:
|
||||
var cases: Array[Dictionary] = [
|
||||
{&"name":&"success",&"policy":&"ROLLBACK_ALL",&"amount":1000,&"cash":3000,&"count":2,&"ok":true},
|
||||
{&"name":&"rollback",&"policy":&"ROLLBACK_ALL",&"amount":3000,&"cash":5000,&"count":0,&"ok":false},
|
||||
{&"name":&"skip",&"policy":&"SKIP_FAILED",&"amount":3000,&"cash":2000,&"count":1,&"ok":true},
|
||||
{&"name":&"branch",&"policy":&"BRANCH_FAILURE",&"amount":3000,&"cash":4000,&"count":1,&"ok":true},
|
||||
{&"name":&"auto_success",&"policy":&"ROLLBACK_ALL",&"amount":1000,&"cash":3000,&"count":2,&"ok":true,&"auto":true},
|
||||
{&"name":&"auto_rollback",&"policy":&"ROLLBACK_ALL",&"amount":3000,&"cash":5000,&"count":0,&"ok":false,&"auto":true},
|
||||
{&"name":&"cost_success",&"policy":&"ROLLBACK_ALL",&"amount":1000,&"cash":2900,&"count":4,&"ok":true,&"cost":100},
|
||||
{&"name":&"cost_rollback",&"policy":&"ROLLBACK_ALL",&"amount":3000,&"cash":5000,&"count":2,&"ok":false,&"cost":1000},
|
||||
]
|
||||
for row: Dictionary in cases:
|
||||
_reset_runtime(981009)
|
||||
var name := String(row[&"name"])
|
||||
var event_id := StringName("observer."+name)
|
||||
var actions: Array[Dictionary] = []
|
||||
for id: StringName in [&"first", &"second"]:
|
||||
actions.append({&"action_id":id,&"type":&"ADD_LEDGER_ENTRY",&"payload":{&"from_account":&"PLAYER_CASH",&"to_account":_runtime.OPERATING_EXPENSE_ACCOUNT,&"amount":row[&"amount"]}})
|
||||
var automatic: bool = row.get(&"auto",false)
|
||||
var definition := {&"event_id":event_id,&"category":&"FLAVOR",&"priority":70,&"foreground":not automatic,&"transaction_policy":row[&"policy"],&"failure_actions":[{&"action_id":&"branch",&"type":&"ADD_LEDGER_ENTRY",&"payload":{&"from_account":&"PLAYER_CASH",&"to_account":_runtime.OPERATING_EXPENSE_ACCOUNT,&"amount":1000}}]}
|
||||
if automatic:
|
||||
definition[&"automatic_actions"]=actions
|
||||
else:
|
||||
definition[&"choices"]=[{&"choice_id":&"choose",&"text_key":&"observer.choose",&"cost":{&"gold":row.get(&"cost",0)},&"actions":actions}]
|
||||
_expect(_runtime.submit_command(&"register_event_definition",{&"definition":definition}).is_success(),name+" public registration")
|
||||
_expect(_runtime.submit_command(&"enqueue_event",{&"event_id":event_id,&"actor_ids":[&"player"]}).is_success(),name+" public enqueue")
|
||||
var notifications: Array[Dictionary]=[]
|
||||
_runtime.economy_service.transaction_posted.connect(func(id: StringName, entries: Array) -> void:
|
||||
var copied: Array[Dictionary]=[]
|
||||
for entry: LedgerEntry in entries:copied.append(entry.to_dictionary())
|
||||
notifications.append({&"id":id,&"entries":copied,&"cash":_runtime.economy_service.get_balance(&"PLAYER_CASH"),&"mirror":_state.get_resource(&"gold")}))
|
||||
_start_next_foreground_minute()
|
||||
var payload: Dictionary={}
|
||||
var result: CommandResult
|
||||
if not automatic:
|
||||
var foreground: EventInstance=_runtime.event_service.get_foreground_instance()
|
||||
_expect(foreground!=null,name+" real foreground choice")
|
||||
if foreground==null:continue
|
||||
payload={&"event_instance_id":foreground.event_instance_id,&"choice_instance_id":foreground.choice_instance_id,&"choice_id":&"choose"}
|
||||
result=_runtime.submit_command(&"submit_event_choice",payload)
|
||||
_expect(result.is_success()==bool(row[&"ok"]),name+" actual choice outcome")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH")==int(row[&"cash"])and _state.get_resource(&"gold")==int(row[&"cash"]),name+" final ledger/mirror exact")
|
||||
_expect(notifications.size()==int(row[&"count"]),name+" only surviving receipts notified once")
|
||||
var seen: Dictionary={}
|
||||
for notification: Dictionary in notifications:
|
||||
var id: StringName=notification[&"id"]
|
||||
var entries: Array[Dictionary]=[]
|
||||
for entry: LedgerEntry in _runtime.economy_service.get_transaction_entries(id):entries.append(entry.to_dictionary())
|
||||
_expect(not seen.has(id)and var_to_bytes(entries)==var_to_bytes(notification[&"entries"]),name+" notified receipt exists exactly in final ledger")
|
||||
_expect(notification[&"cash"]==int(row[&"cash"])and notification[&"mirror"]==int(row[&"cash"]),name+" observer sees only final authoritative state")
|
||||
seen[id]=true
|
||||
_expect(_runtime.economy_service.notification_scope_depth()==0,name+" all nested scopes closed")
|
||||
if not automatic:
|
||||
_clock.set_paused(true)
|
||||
var economy_before: Dictionary=_runtime.economy_service.create_snapshot()
|
||||
var count_before: int=notifications.size()
|
||||
var replay: CommandResult=_runtime.submit_command(&"submit_event_choice",payload)
|
||||
_expect(replay.is_replay and replay.is_success()==bool(row[&"ok"]),name+" cached choice replays its original outcome")
|
||||
_expect(var_to_bytes(economy_before)==var_to_bytes(_runtime.economy_service.create_snapshot())and notifications.size()==count_before,name+" replay neither posts nor notifies twice")
|
||||
|
||||
|
||||
func _lifecycle_event(name: StringName, fail_a: bool) -> EventInstance:
|
||||
var actions: Array[Dictionary] = []
|
||||
if fail_a:
|
||||
for id: StringName in [&"first", &"second"]:
|
||||
actions.append({&"action_id":id,&"type":&"ADD_LEDGER_ENTRY",&"payload":{&"from_account":&"PLAYER_CASH",&"to_account":_runtime.OPERATING_EXPENSE_ACCOUNT,&"amount":3000}})
|
||||
var definition := {&"event_id":name,&"category":&"FLAVOR",&"priority":70,&"foreground":true,&"transaction_policy":&"ROLLBACK_ALL",&"choices":[{&"choice_id":&"a",&"text_key":&"lifecycle.a",&"cost":{&"gold":100},&"actions":actions},{&"choice_id":&"b",&"text_key":&"lifecycle.b",&"cost":{&"gold":200},&"actions":[]}]}
|
||||
_expect(_runtime.submit_command(&"register_event_definition",{&"definition":definition}).is_success(),"lifecycle definition registers")
|
||||
_expect(_runtime.submit_command(&"enqueue_event",{&"event_id":name,&"actor_ids":[&"player"],&"options":{&"event_instance_id":name}}).is_success(),"lifecycle event queues")
|
||||
_clock.advance_minutes(1)
|
||||
var event: EventInstance = _runtime.event_service.get_foreground_instance()
|
||||
_expect(event != null and event.event_instance_id == name,"lifecycle actual foreground")
|
||||
return event
|
||||
|
||||
|
||||
func _lifecycle_reserve(event: EventInstance, choice: StringName, key: StringName) -> CommandResult:
|
||||
return _runtime.submit_command(&"reserve_event_choice_cost",{&"event_instance_id":event.event_instance_id,&"choice_id":choice},&"player",&"",100,-1,key)
|
||||
|
||||
|
||||
func _lifecycle_choose(event: EventInstance, choice: StringName, key: StringName) -> CommandResult:
|
||||
return _runtime.submit_command(&"submit_event_choice",{&"event_instance_id":event.event_instance_id,&"choice_instance_id":event.choice_instance_id,&"choice_id":choice},&"player",&"",100,-1,key)
|
||||
|
||||
|
||||
func _test_paid_choice_attempt_lifecycle() -> void:
|
||||
_reset_runtime(4_107)
|
||||
var event := _lifecycle_event(&"multi_cost",false)
|
||||
_expect(_lifecycle_reserve(event,&"a",&"multi.a").is_success(),"first paid choice reserves")
|
||||
_expect(_lifecycle_reserve(event,&"b",&"multi.b").is_success(),"distinct affordable choice reserves independently")
|
||||
var a: Dictionary = event.reserved_costs[&"a"][&"token"]
|
||||
var b: Dictionary = event.reserved_costs[&"b"][&"token"]
|
||||
_expect(a[&"reservation_id"] != b[&"reservation_id"],"actual paid reservations have distinct persistent identities")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4700 and _runtime.economy_service.get_balance(&"EVENT_ESCROW:multi_cost") == 300,"both paid reservations in real ledger")
|
||||
var before: Dictionary = _runtime.economy_service.create_snapshot()
|
||||
_expect(_lifecycle_reserve(event,&"a",&"multi.a.again").is_success(),"existing reservation remains replayable")
|
||||
_expect(var_to_bytes(before) == var_to_bytes(_runtime.economy_service.create_snapshot()),"existing reservation never double debits")
|
||||
var saved: Dictionary = _runtime.create_snapshot()
|
||||
_reset_runtime(4_108)
|
||||
_expect(_runtime.restore_snapshot(saved) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(saved),"entire multi-reservation snapshot restores exactly")
|
||||
event = _runtime.event_service.get_foreground_instance()
|
||||
_expect(_lifecycle_choose(event,&"b",&"multi.choose.b").is_success(),"restored B commits and unused A refunds")
|
||||
_expect(event.state == EventInstance.State.COMPLETED and event.reserved_costs.is_empty(),"restored paid event completes with no escrow tokens")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4800 and _runtime.economy_service.get_balance(&"EVENT_ESCROW:multi_cost") == 0,"selected200G only charged after refund100G")
|
||||
_expect(_runtime.id_generator.get_counter(&"event_cost") == 2,"persisted reservation ID counter restored")
|
||||
|
||||
_reset_runtime(4_109)
|
||||
event = _lifecycle_event(&"retry_cost",true)
|
||||
var old_choice := event.choice_instance_id
|
||||
var failed := _lifecycle_choose(event,&"a",&"retry.a")
|
||||
_expect(not failed.is_success() and event.state == EventInstance.State.WAITING_CHOICE,"ordinary failed action remains waiting after rollback")
|
||||
_expect(event.choice_instance_id != old_choice,"fully rolled back attempt gets a fresh active choice token")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 5000 and event.reserved_costs.is_empty(),"failed attempt returns actual funds")
|
||||
before = _runtime.create_snapshot()
|
||||
var replay: CommandResult = _runtime.event_service.submit_choice(event.event_instance_id,old_choice,&"a",_clock.absolute_minute)
|
||||
_expect(replay.is_replay and not replay.is_success() and var_to_bytes(before) == var_to_bytes(_runtime.create_snapshot()),"old failed attempt retains whole replay no-op")
|
||||
_expect(_lifecycle_reserve(event,&"a",&"retry.reserve").is_success(),"same affordable choice can reserve after a complete refund")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4900,"new retry reservation physically charges once")
|
||||
_expect(_lifecycle_choose(event,&"b",&"retry.choose.b").is_success(),"fresh different choice never replays previous failure")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4800 and event.state == EventInstance.State.COMPLETED,"retry B pays200G and refunds unused A")
|
||||
_expect(event.reserved_costs.is_empty() and _runtime.economy_service.get_balance(&"EVENT_ESCROW:retry_cost") == 0,"retry leaves no unpaid or unused reservations")
|
||||
_expect(_runtime.id_generator.get_counter(&"event_cost") == 3,"successful new reservations advance only their existing ID namespace")
|
||||
|
||||
|
||||
func _test_legacy_paid_choice_tokens() -> void:
|
||||
for mode: StringName in [&"commit", &"cancel"]:
|
||||
_reset_runtime(4_110)
|
||||
var name := StringName("legacy_cost_"+mode)
|
||||
var event := _lifecycle_event(name,false)
|
||||
# Reproduce the actual old three-field token and instance-level ledger key.
|
||||
_runtime.event_service.configure_callbacks(
|
||||
Callable(_runtime,"_event_actor_exists"),Callable(_runtime,"_execute_event_action_boundary"),
|
||||
func(cost: Dictionary, context: Dictionary) -> Dictionary:
|
||||
var id := StringName(context[&"event_instance_id"])
|
||||
var escrow := StringName("EVENT_ESCROW:"+String(id))
|
||||
_runtime.economy_service.register_account(escrow)
|
||||
var result: CommandResult = _runtime.economy_service.transfer(&"PLAYER_CASH",escrow,int(cost[&"gold"]),_clock.absolute_minute,&"EVENT_COST_RESERVED",id,StringName("event.cost.reserve."+String(id)))
|
||||
return {&"ok":result.is_success(),&"token":{&"event_instance_id":id,&"cost":cost.duplicate(true),&"escrow_account":escrow}},
|
||||
Callable(_runtime,"_release_event_cost"),Callable(_runtime,"_commit_event_cost"),Callable(_runtime,"_event_entity_exists"),Callable(_runtime,"_event_path_exists")
|
||||
)
|
||||
_expect(_lifecycle_reserve(event,&"a",&"legacy.reserve").is_success(),"actual legacy100G reservation")
|
||||
_expect(not event.reserved_costs[&"a"][&"token"].has(&"reservation_id"),"legacy token retains exactly old representation")
|
||||
var saved: Dictionary = _runtime.create_snapshot()
|
||||
_reset_runtime(4_111)
|
||||
_expect(_runtime.restore_snapshot(saved) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(saved),"legacy cost token whole snapshot restores unchanged")
|
||||
event = _runtime.event_service.get_foreground_instance()
|
||||
if mode == &"commit":
|
||||
_expect(_lifecycle_choose(event,&"a",&"legacy.choose").is_success(),"restored legacy cost commits via old key")
|
||||
else:
|
||||
_expect(_runtime.event_service.cancel_event(event.event_instance_id).is_success(),"restored legacy cost cancels via old refund key")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == (4900 if mode == &"commit" else 5000),"legacy cash exact after commit or refund")
|
||||
_expect(_runtime.economy_service.get_balance(StringName("EVENT_ESCROW:"+String(name))) == 0 and event.reserved_costs.is_empty(),"legacy escrow and token cleared once")
|
||||
_expect(_runtime.id_generator.get_counter(&"event_cost") == 0,"legacy restore never invents a new reservation ID")
|
||||
|
||||
|
||||
func _test_paid_reservation_failure_boundaries() -> void:
|
||||
_reset_runtime(4_112)
|
||||
var event := _lifecycle_event(&"reserve_failure",false)
|
||||
var receipts: Array[Dictionary] = []
|
||||
_runtime.economy_service.transaction_posted.connect(func(id: StringName, entries: Array) -> void:
|
||||
var copied: Array[Dictionary] = []
|
||||
for entry: LedgerEntry in entries: copied.append(entry.to_dictionary())
|
||||
receipts.append({&"id":id,&"entries":copied}))
|
||||
var choice: EventChoice = _runtime.event_service.get_definition(event.source_event_id).get_choice(&"a")
|
||||
choice.cost = {&"gold":6000}
|
||||
var ledger_before: Dictionary = _runtime.economy_service.create_snapshot()
|
||||
_expect(not _lifecycle_reserve(event,&"a",&"unfunded.gold").is_success(),"unaffordable gold reserve rejects")
|
||||
_expect(var_to_bytes(ledger_before) == var_to_bytes(_runtime.economy_service.create_snapshot()) and _state.get_resource(&"gold") == 5000,"gold reserve failure retains full ledger and mirror")
|
||||
_expect(_runtime.id_generator.get_counter(&"event_cost") == 0 and event.reserved_costs.is_empty(),"failed gold reserve consumes no cost identity or token")
|
||||
var mana_before: int = _state.get_resource(&"mana")
|
||||
choice.cost = {&"gold":100,&"mana":mana_before+1}
|
||||
_expect(not _lifecycle_reserve(event,&"a",&"unfunded.mixed").is_success(),"mixed resource reserve rejects after real gold transfer")
|
||||
_expect(var_to_bytes(ledger_before) == var_to_bytes(_runtime.economy_service.create_snapshot()),"mixed resource failure reverses the entire real ledger")
|
||||
_expect(_state.get_resource(&"gold") == 5000 and _state.get_resource(&"mana") == mana_before,"mixed cost failure retains all resources")
|
||||
_expect(receipts.is_empty(),"rolled-back public reserve never publishes a ghost receipt")
|
||||
_expect(_runtime.economy_service.notification_scope_depth() == 0 and _runtime.id_generator.get_counter(&"event_cost") == 0 and event.reserved_costs.is_empty(),"mixed reserve closes scopes and restores cost identity")
|
||||
|
||||
_reset_runtime(4_113)
|
||||
event = _lifecycle_event(&"commit_retry",false)
|
||||
var old_choice := event.choice_instance_id
|
||||
_runtime.event_service._cost_committer = func(_token: Variant, _context: Dictionary) -> Dictionary: return {&"ok":false,&"error":"explicit commit refusal"}
|
||||
var result := _lifecycle_choose(event,&"a",&"commit.refused")
|
||||
_expect(not result.is_success() and event.state == EventInstance.State.WAITING_CHOICE,"recoverable commit failure remains waiting")
|
||||
_expect(event.choice_instance_id != old_choice and event.reserved_costs.is_empty(),"fully refunded commit failure advances attempt and clears token")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 5000 and _state.get_resource(&"gold") == 5000,"commit refusal refunds real gold")
|
||||
var before: Dictionary = _runtime.create_snapshot()
|
||||
var replay: CommandResult = _runtime.event_service.submit_choice(event.event_instance_id,old_choice,&"a",_clock.absolute_minute)
|
||||
_expect(replay.is_replay and not replay.is_success() and var_to_bytes(before) == var_to_bytes(_runtime.create_snapshot()),"old commit failure remains exact replay noop")
|
||||
_runtime.event_service._cost_committer = Callable(_runtime,"_commit_event_cost")
|
||||
_expect(_lifecycle_choose(event,&"b",&"commit.alternative").is_success(),"alternative after refunded commit failure succeeds")
|
||||
_expect(event.state == EventInstance.State.COMPLETED and _runtime.economy_service.get_balance(&"PLAYER_CASH") == 4800,"commit retry finishes and charges B only")
|
||||
_expect(_runtime.economy_service.notification_scope_depth() == 0 and _runtime.id_generator.get_counter(&"event_cost") == 2,"commit retry closes scopes with persistent distinct identities")
|
||||
|
||||
Reference in New Issue
Block a user