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:
@@ -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