Files
TheTree_mock/tests/s4/test_relationship_event_service.gd
T
Takeshi b84bade045 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.
2026-10-11 15:58:00 +09:00

576 lines
31 KiB
GDScript

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)