extends SceneTree const RelationshipStateType = preload("res://scripts/relationship/relationship_state.gd") const RelationshipServiceType = preload("res://scripts/relationship/relationship_service.gd") const EventConditionType = preload("res://scripts/events/event_condition.gd") const EventActionType = preload("res://scripts/events/event_action.gd") const EventChoiceType = preload("res://scripts/events/event_choice.gd") const EventDefinitionType = preload("res://scripts/events/event_definition.gd") const EventInstanceType = preload("res://scripts/events/event_instance.gd") const EventServiceType = preload("res://scripts/events/event_service.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("S4: Relationship and Event Service") _test_relationship_pair_metrics_and_thresholds() _test_shield_penalty_and_route_confirmation() _test_relationship_snapshot_atomicity() _test_condition_tree() _test_foreground_queue_and_choice_idempotency() _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: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) func _test_relationship_pair_metrics_and_thresholds() -> void: var service := RelationshipServiceType.new() var created = service.create_relationship( &"rel_main", &"zoe", &"alex", { &"trust": 20, &"respect": 30, &"affection": 45, &"fear": 5, &"dependence": 10, &"shield_max": 40, &"shield_current": 35, } ) _expect(created.is_success(), "relationship registers") var state := service.get_relationship(&"rel_main") _expect(state.actor_a_id == &"alex" and state.actor_b_id == &"zoe", "actor pair normalizes lexically") _expect(service.find_relationship(&"alex", &"zoe") == state, "normalized pair lookup is direction independent") _expect(not service.create_relationship(&"rel_duplicate", &"alex", &"zoe").is_success(), "duplicate normalized actor pair is rejected") _expect(state.get_metric(&"trust") == 20 and state.get_metric(&"respect") == 30, "five canonical metrics are retained") var gated = service.apply_value_change(&"rel_main", &"affection", 10, 100) _expect(gated.is_success() and state.get_metric(&"affection") == 49, "background change stops before gated threshold 50") _expect(gated.details[&"gated_threshold"] == 50, "gated change exposes its required threshold Event") var candidates := service.get_threshold_candidates() _expect(candidates.size() == 1 and candidates[0][&"requires_unlock_event"], "threshold crossing registers a candidate without immediate playback") var unlocked = service.apply_value_change( &"rel_main", &"affection", 1, 101, {&"important": true, &"allow_threshold_crossing": true} ) _expect(unlocked.is_success() and state.get_metric(&"affection") == 50, "explicit important Event may cross the gated threshold") service.apply_value_change(&"rel_main", &"trust", 50, 102) _expect(state.get_metric(&"trust") == 30, "automatic relationship drift is limited to +10 per day") service.apply_value_change(&"rel_main", &"trust", 50, 1_500) _expect(state.get_metric(&"trust") == 40, "daily automatic limit resets on a new day") func _test_shield_penalty_and_route_confirmation() -> void: var service := RelationshipServiceType.new() service.create_relationship( &"rel_shield", &"owner", &"target", {&"shield_max": 100, &"shield_current": 100} ) var state := service.get_relationship(&"rel_shield") var damages: Array[int] = [] var penalties: Array[int] = [] for minute: int in range(4): var result = service.apply_shield_attack( &"rel_shield", &"repeat_action", minute, {&"base_damage": 20, &"max_damage": 20, &"daily_limit": 4} ) damages.append(int(result.details[&"damage"])) penalties.append(int(result.details[&"penalty_percent"])) _expect(penalties == [0, 25, 50, 75], "repeat Action applies 0/25/50/75 percent penalty stages") _expect(damages == [20, 15, 10, 5], "repeat penalty changes deterministic Shield damage") _expect(not service.apply_shield_attack(&"rel_shield", &"repeat_action", 4, {&"base_damage": 20}).is_success(), "daily Action use limit is enforced") service.apply_shield_attack(&"rel_shield", &"fresh_action", 5, {&"base_damage": 500, &"max_damage": 500}) _expect(state.shield_current == 0, "Shield never falls below zero") var next_day = service.apply_shield_attack(&"rel_shield", &"repeat_action", 1_440, {&"base_damage": 20, &"max_damage": 20}) _expect(next_day.is_success() and next_day.details[&"penalty_percent"] == 0, "repeat count resets at the day boundary") var direct = service.request_route_lock( &"rel_shield", RelationshipStateType.RouteLock.EXCLUSIVE_LOCKED, &"route_a" ) _expect(not direct.is_success() and state.route_lock == RelationshipStateType.RouteLock.NONE, "exclusive route cannot lock without confirmation") _expect(service.request_route_lock(&"rel_shield", RelationshipStateType.RouteLock.EXCLUSIVE_PENDING, &"route_a").is_success(), "exclusive route first enters pending state") _expect(not service.confirm_route_lock(&"rel_shield", &"route_b").is_success(), "mismatched route confirmation is rejected") _expect(service.confirm_route_lock(&"rel_shield", &"route_a").is_success(), "matching explicit confirmation locks the route") func _test_relationship_snapshot_atomicity() -> void: var service := RelationshipServiceType.new() service.create_relationship(&"rel_save", &"b", &"a", {&"trust": 12, &"shield_max": 50, &"shield_current": 25}) service.apply_value_change(&"rel_save", &"trust", 5, 20) var snapshot: Dictionary = service.create_snapshot() var restored := RelationshipServiceType.new() _expect(restored.restore_snapshot(snapshot), "valid Relationship snapshot restores") _expect(restored.create_snapshot() == snapshot, "Relationship snapshot round trips exactly") var before: Dictionary = restored.create_snapshot() var corrupt := snapshot.duplicate(true) corrupt[&"version"] = 999 _expect(not restored.restore_snapshot(corrupt), "unsupported Relationship snapshot is rejected") _expect(restored.create_snapshot() == before, "failed Relationship restore is atomic") func _test_condition_tree() -> void: var condition := EventConditionType.new().configure( EventConditionType.Operator.ALL, [], null, [ EventConditionType.new().configure(EventConditionType.Operator.GTE, "relationship.trust", 50), EventConditionType.new().configure(EventConditionType.Operator.HAS_FLAG, "flags", &"met_before"), EventConditionType.new().configure(EventConditionType.Operator.TIME_IN_WINDOW, "absolute_minute", [1_200, 300]), EventConditionType.new().configure(EventConditionType.Operator.ENTITY_EXISTS, "actor_id"), ] ) var context := { &"relationship": {&"trust": 60}, &"flags": {&"met_before": true}, &"absolute_minute": 1_300, &"actor_id": &"actor_a", } var callbacks := {&"entity_exists": func(actor_id: Variant) -> bool: return actor_id == &"actor_a"} _expect(condition.is_valid() and condition.evaluate(context, callbacks), "nested data Condition evaluates comparisons, flag, overnight time and entity callback") var clone := EventConditionType.from_variant(condition.create_snapshot()) _expect(clone != null and clone.evaluate(context, callbacks), "Condition snapshot preserves deterministic evaluation") context[&"relationship"][&"trust"] = 49 _expect(not clone.evaluate(context, callbacks), "ALL condition fails when one child changes") func _test_foreground_queue_and_choice_idempotency() -> void: var actor_live := {&"actor_a": true, &"actor_b": true} var applied := {&"count": 0} var service := EventServiceType.new().configure_callbacks( func(actor_id: StringName) -> bool: return bool(actor_live.get(actor_id, false)), func(action: EventAction, phase: StringName, _context: Dictionary, token: Variant) -> Dictionary: if phase == &"validate": return {&"ok": true} if phase == &"apply": var old_value := int(applied[&"count"]) applied[&"count"] = old_value + int(action.payload.get(&"delta", 0)) return {&"ok": true, &"token": old_value} if phase == &"rollback": applied[&"count"] = int(token) return {&"ok": true} ) service.register_definition(_choice_definition(&"low", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [_delta_action(&"low_add", 1)])) service.register_definition(_choice_definition(&"high", 50, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [_delta_action(&"high_add", 3)])) service.enqueue_candidate(&"low", [&"actor_b"], 0, {&"event_instance_id": &"event_low"}) service.enqueue_candidate(&"high", [&"actor_a"], 0, {&"event_instance_id": &"event_high"}) _expect(_instance_ids(service.get_queued_instances()) == [&"event_high", &"event_low"], "Event queue sorts priority descending then deterministic instance ID") var started = service.start_next_event(0) _expect(started.is_success() and started.details[&"event_instance_id"] == &"event_high", "highest priority foreground Event starts first") _expect(service.get_actor_locks() == {&"actor_a": &"event_high"}, "foreground Event locks all actors by stable ID") _expect(not service.start_next_event(0).is_success(), "only one foreground Event may run at a time") var choice_token: StringName = started.details[&"choice_instance_id"] var first = service.submit_choice(&"event_high", choice_token, &"accept", 0) var second = service.submit_choice(&"event_high", choice_token, &"accept", 0) _expect(first.is_success() and second.is_success() and second.is_replay, "duplicate Choice submission returns cached replay") _expect(applied[&"count"] == 3, "duplicate Choice never applies Actions twice") _expect(service.get_actor_locks().is_empty(), "completed Event releases actor locks") _expect(service.start_next_event(1).is_success(), "next foreground Event starts after lock release") func _test_missing_actor_releases_lock_and_cost() -> void: var actor_live := {&"actor_a": true} var released: Array = [] var committed: Array = [] var service := EventServiceType.new().configure_callbacks( func(actor_id: StringName) -> bool: return bool(actor_live.get(actor_id, false)), func(_action: EventAction, _phase: StringName, _context: Dictionary, _token: Variant) -> Dictionary: return {&"ok": true}, func(cost: Dictionary, _context: Dictionary) -> Dictionary: return {&"ok": true, &"token": {&"reserved_gold": int(cost.get(&"gold", 0))}}, func(token: Variant, _context: Dictionary) -> bool: released.append(token) return true, func(token: Variant, _context: Dictionary) -> bool: committed.append(token) return true ) var definition := _choice_definition(&"missing_actor", 20, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [_delta_action(&"noop", 0)], {&"gold": 25}) service.register_definition(definition) service.enqueue_candidate(&"missing_actor", [&"actor_a"], 10, {&"event_instance_id": &"event_missing"}) var started = service.start_next_event(10) _expect(started.is_success(), "missing-actor fixture starts while actor exists") _expect(service.reserve_choice_cost(&"event_missing", &"accept", 10).is_success(), "Choice cost reserves while UI is waiting") actor_live[&"actor_a"] = false var audit := service.audit_actor_availability(11) _expect(audit.size() == 1 and service.get_instance(&"event_missing").state == EventInstanceType.State.CANCELLED, "actor disappearance cancels Event by default policy") _expect(service.get_actor_locks().is_empty(), "actor disappearance always releases foreground lock") _expect(released.size() == 1 and committed.is_empty(), "actor disappearance releases reserved Cost without committing it") func _test_content_replacement_boundary() -> void: var service := EventServiceType.new() service.register_definition(EventDefinitionType.new().configure(&"safe_business", { &"content_rating": &"GENERAL", &"content_tags": [&"business_conflict"], })) service.register_definition(EventDefinitionType.new().configure(&"restricted_with_replacement", { &"content_rating": &"RESTRICTED", &"content_tags": [&"restricted_route"], &"filter_behavior": &"REPLACE", &"replacement_event_id": &"safe_business", })) service.register_definition(EventDefinitionType.new().configure(&"restricted_missing", { &"content_rating": &"RESTRICTED", &"content_tags": [&"restricted_route"], &"filter_behavior": &"REPLACE", &"replacement_event_id": &"missing_safe_event", })) service.configure_content_filter( EventDefinitionType.FilterBehavior.REPLACE, EventDefinitionType.ContentRating.GENERAL, [&"restricted_route"] ) var replaced := service.enqueue_candidate(&"restricted_with_replacement", [], 0, {&"event_instance_id": &"event_replaced"}) _expect(replaced.is_success() and replaced.details[&"resolved_event_id"] == &"safe_business", "restricted Event resolves only to configured safe replacement ID") var disabled := service.enqueue_candidate(&"restricted_missing", [], 0) _expect(not disabled.is_success() and disabled.details[&"reason"] == &"REPLACEMENT_MISSING", "missing replacement safely disables candidate") _expect(not service.validate_definitions()[&"ok"], "definition audit reports missing replacement boundary") func _test_transaction_policies() -> void: var rollback_state := {&"value": 0} var rollback_service: Variant = _transaction_service(rollback_state) rollback_service.register_definition(_choice_definition( &"rollback_event", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [_delta_action(&"add_first", 5), _failing_action(&"fail_second")] )) var rollback_result = _run_choice(rollback_service, &"rollback_event", &"rollback_instance") _expect(not rollback_result.is_success() and rollback_state[&"value"] == 0, "ROLLBACK_ALL reverses prior Action after later failure") var skip_state := {&"value": 0} var skip_service: Variant = _transaction_service(skip_state) skip_service.register_definition(_choice_definition( &"skip_event", 10, EventDefinitionType.TransactionPolicy.SKIP_FAILED, [_delta_action(&"add_one", 2), _failing_action(&"skip_me"), _delta_action(&"add_two", 3)] )) var skip_result = _run_choice(skip_service, &"skip_event", &"skip_instance") _expect(skip_result.is_success() and skip_state[&"value"] == 5, "SKIP_FAILED keeps successful Actions and skips only failed Action") _expect(skip_result.details[&"skipped_action_ids"] == [&"skip_me"], "SKIP_FAILED reports deterministic skipped Action IDs") var branch_state := {&"value": 0} var branch_service: Variant = _transaction_service(branch_state) var branch_definition := _choice_definition( &"branch_event", 10, EventDefinitionType.TransactionPolicy.BRANCH_FAILURE, [_delta_action(&"primary_add", 9), _failing_action(&"primary_fail")] ) branch_definition.failure_actions = [_delta_action(&"failure_branch_add", 4)] branch_service.register_definition(branch_definition) var branch_result = _run_choice(branch_service, &"branch_event", &"branch_instance") _expect(branch_result.is_success() and branch_state[&"value"] == 4, "BRANCH_FAILURE rolls back primary Actions then applies failure branch") _expect(branch_result.details[&"branched_failure"], "BRANCH_FAILURE result marks selected failure branch") func _test_event_snapshot_round_trip_and_atomicity() -> void: var service := EventServiceType.new() service.register_definition(_choice_definition(&"save_event", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [])) service.enqueue_candidate(&"save_event", [&"actor_a"], 100, {&"event_instance_id": &"save_instance"}) service.start_next_event(100) var snapshot: Dictionary = service.create_snapshot() var restored := EventServiceType.new() restored.register_definition(_choice_definition(&"save_event", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [])) _expect(restored.restore_snapshot(snapshot), "Event queue, Choice token and Actor Locks restore") _expect(restored.create_snapshot() == snapshot, "active foreground Event snapshot round trips exactly") _expect(restored.validate_invariants()[&"ok"], "restored EventService satisfies Actor Lock invariants") var before: Dictionary = restored.create_snapshot() var corrupt := snapshot.duplicate(true) corrupt[&"actor_locks"] = {&"wrong_actor": &"save_instance"} _expect(not restored.restore_snapshot(corrupt), "orphaned Actor Lock snapshot is rejected") _expect(restored.create_snapshot() == before, "failed Event restore is atomic") func _test_event_recovery_lock_scope_and_strict_filter_parse() -> void: var definition := _choice_definition( &"recover_event", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [] ) var source := EventServiceType.new() source.register_definition(definition) source.enqueue_candidate(&"recover_event", [&"actor_a"], 10, {&"event_instance_id": &"recover_instance"}) source.start_next_event(10) var terminal_snapshot: Dictionary = source.create_snapshot() terminal_snapshot[&"instances"][0][&"state"] = "COMPLETED" terminal_snapshot[&"active_deduplication_keys"] = {} var recovered := EventServiceType.new() recovered.register_definition(definition) _expect(recovered.restore_snapshot_with_lock_recovery(terminal_snapshot), "recovery accepts the terminal foreground's exact Actor Lock set") _expect(recovered.get_actor_locks().is_empty() and recovered.get_foreground_instance() == null, "terminal recovery releases only the matching foreground pointer and lock") _expect(recovered.last_recovery_repair_report[&"released_actor_ids"] == [&"actor_a"], "terminal recovery report names the exact released Actor Lock") var live_before: Dictionary = recovered.create_snapshot() var mixed_corruption: Dictionary = terminal_snapshot.duplicate(true) mixed_corruption[&"actor_locks"][&"unrelated_actor"] = "recover_instance" _expect(not recovered.restore_snapshot_with_lock_recovery(mixed_corruption), "valid foreground lock mixed with unrelated lock corruption is hard-rejected") _expect(recovered.create_snapshot() == live_before, "unrelated lock rejection preserves the live Event state atomically") var missing_foreground: Dictionary = terminal_snapshot.duplicate(true) missing_foreground[&"instances"] = [] var missing_recovered := EventServiceType.new() missing_recovered.register_definition(definition) _expect(missing_recovered.restore_snapshot_with_lock_recovery(missing_foreground), "missing foreground recovery releases locks that still name only the missing foreground ID") _expect(missing_recovered.last_recovery_repair_report[&"reason"] == &"MISSING_FOREGROUND" and missing_recovered.last_recovery_repair_report[&"released_actor_ids"] == [&"actor_a"], "missing foreground report preserves the exact released Actor IDs and reason") var unknown_mode: Dictionary = live_before.duplicate(true) unknown_mode[&"filter_mode"] = "UNKNOWN_FILTER" _expect(not recovered.restore_snapshot(unknown_mode), "unknown serialized Content Filter behavior is rejected instead of falling back") _expect(recovered.create_snapshot() == live_before, "unknown filter rejection preserves the live Event state") var out_of_range_rating: Dictionary = live_before.duplicate(true) out_of_range_rating[&"maximum_content_rating"] = 99 _expect(not recovered.restore_snapshot(out_of_range_rating), "out-of-range serialized Content Rating is rejected instead of clamped") _expect(recovered.create_snapshot() == live_before, "out-of-range rating rejection preserves the live Event state") var serialized_definition: Dictionary = definition.create_snapshot() serialized_definition[&"filter_behavior"] = "UNKNOWN_FILTER" _expect(EventDefinitionType.from_variant(serialized_definition) == null, "serialized EventDefinition rejects an unknown FilterBehavior") serialized_definition = definition.create_snapshot() serialized_definition[&"content_rating"] = 99 _expect(EventDefinitionType.from_variant(serialized_definition) == null, "serialized EventDefinition rejects an out-of-range ContentRating") var lenient_input := EventDefinitionType.new().configure(&"settings_boundary", {&"filter_behavior": "UNKNOWN_FILTER"}) _expect(lenient_input.filter_behavior == EventDefinitionType.FilterBehavior.NONE, "non-snapshot configuration keeps its explicit lenient fallback boundary") func _transaction_service(state: Dictionary) -> Variant: return EventServiceType.new().configure_callbacks( Callable(), func(action: EventAction, phase: StringName, _context: Dictionary, token: Variant) -> Dictionary: if phase == &"validate": return {&"ok": true} if phase == &"apply": if bool(action.payload.get(&"fail", false)): return {&"ok": false, &"error": "requested failure"} var old_value := int(state[&"value"]) state[&"value"] = old_value + int(action.payload.get(&"delta", 0)) return {&"ok": true, &"token": old_value} if phase == &"rollback": state[&"value"] = int(token) return {&"ok": true} ) func _run_choice(service: Variant, event_id: StringName, instance_id: StringName) -> CommandResult: service.enqueue_candidate(event_id, [], 0, {&"event_instance_id": instance_id}) var started = service.start_next_event(0) return service.submit_choice(instance_id, started.details[&"choice_instance_id"], &"accept", 0) func _choice_definition( event_id: StringName, priority: int, policy: EventDefinition.TransactionPolicy, actions: Array, cost: Dictionary = {} ) -> EventDefinition: var choice := EventChoiceType.new().configure( &"accept", StringName("choice.%s.accept" % event_id), [], cost, {}, actions ) return EventDefinitionType.new().configure(event_id, { &"category": &"RELATION", &"priority": priority, &"foreground": true, &"transaction_policy": policy, &"choices": [choice], }) func _delta_action(action_id: StringName, delta: int) -> EventAction: return EventActionType.new().configure( action_id, EventActionType.Type.ADD_LEDGER_ENTRY, {&"delta": delta} ) func _failing_action(action_id: StringName) -> EventAction: return EventActionType.new().configure( action_id, EventActionType.Type.ADD_LEDGER_ENTRY, {&"fail": true} ) func _instance_ids(instances: Array[EventInstance]) -> Array[StringName]: var result: Array[StringName] = [] for instance: EventInstance in instances: result.append(instance.event_instance_id) return result func _expect(condition: bool, label: String) -> void: _checks += 1 if condition: print(" ok %s" % label) 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)