extends GutTest class RecordingCommitter: extends TransactionalActionEffectCommitter var call_count := 0 var operation_count := 0 var should_fail := false func _commit_whole_plan( plan: ActionEffectPlan, _commit_context: Dictionary ) -> ActionEffectCommitResult: call_count += 1 operation_count = plan.get_operation_count() if should_fail: return ( ActionEffectCommitResult . rollback_safe_failure( &"authority_rejected", "Atomic authority rejected the plan", [&"transaction_started", &"transaction_rolled_back"], ) ) return ActionEffectCommitResult.committed( [101, 104], [&"transaction_started", &"whole_plan_committed"] ) class InvalidResultCommitter: extends TransactionalActionEffectCommitter var call_count := 0 func _commit_whole_plan( _plan: ActionEffectPlan, _commit_context: Dictionary ) -> ActionEffectCommitResult: call_count += 1 return ActionEffectCommitResult.new() func test_all_builtin_strategies_emit_exact_primitive_records_in_authored_order() -> void: var definition := _composite_definition() var planner := ActionEffectPlanner.new() var player_plan := planner.plan(definition, _player_ref()) var person_plan := planner.plan(definition, _person_ref()) assert_not_null(player_plan) assert_not_null(person_plan) assert_true(player_plan.is_valid(), "%s" % [player_plan.get_errors()]) assert_eq( player_plan.to_dictionary(), person_plan.to_dictionary(), "Player and person actors should share one effect-planning path", ) assert_eq( _operation_kinds(player_plan), [ ActionEffect.KIND_METRIC_DELTA, ActionEffect.KIND_NEED_DELTA, ActionEffect.KIND_INVENTORY_TRANSFER, ActionEffect.KIND_DAMAGE, ActionEffect.KIND_HEALING, ActionEffect.KIND_RELATIONSHIP_DELTA, ActionEffect.KIND_SCHEDULE_ACTION, ActionEffect.KIND_SITE_CONDITION, ], ) var operations := player_plan.get_operations() for operation_index in operations.size(): assert_eq(operations[operation_index]["authored_order"], operation_index) assert_eq(operations[0]["payload"]["subject_key"], "safety") assert_eq(operations[0]["payload"]["delta"], 2.5) assert_eq(operations[1]["payload"]["subject_key"], "hunger") assert_eq(operations[1]["payload"]["delta"], -10.0) assert_eq( operations[2]["payload"], { "item_id": "food", "amount": 2.0, "source_role": "actor", "destination_role": "target", "parameters": {"exact": true}, }, ) assert_eq(operations[3]["payload"]["amount"], 12.0) assert_eq(operations[4]["payload"]["amount"], 5.0) assert_eq(operations[5]["payload"]["subject_key"], "trust") assert_eq(operations[6]["payload"]["scheduled_action_id"], "rest") assert_eq(operations[7]["payload"]["subject_key"], "fire_lit") assert_has(player_plan.get_reason_trace(), &"authored_order_preserved") assert_has(player_plan.get_reason_trace(), &"plan_validated") func test_plan_is_defensively_copied_and_round_trips_as_primitive_data() -> void: var definition := _composite_definition() var plan := ActionEffectPlanner.new().plan(definition, _player_ref()) var saved := plan.to_dictionary() assert_true(ActionEffectPlan.is_primitive_record(saved)) var restored := ActionEffectPlan.from_dictionary(saved) assert_not_null(restored) assert_eq(restored.to_dictionary(), saved) var caller_operations := plan.get_operations() caller_operations[0]["payload"]["delta"] = 999.0 definition.effects[0].parameters["scope"] = "mutated" definition.effects[0].value = -500.0 assert_eq( plan.to_dictionary(), saved, "Plan values should not alias callers or authored resources" ) assert_null( ( ActionEffectPlan . from_dictionary( { "schema_version": ActionEffectPlan.SCHEMA_VERSION, "action_id": "bad", "operations": saved["operations"], "reason_trace": [], "reflection_expression": "state.call(method)", } ) ), "Unknown record fields should fail instead of becoming a reflection DSL", ) var malformed := saved.duplicate(true) malformed["operations"][0]["operation_id"] = 17 assert_null( ActionEffectPlan.from_dictionary(malformed), "Malformed primitive field types should fail without coercion", ) func test_missing_unknown_and_duplicate_strategies_fail_closed() -> void: var definition := _composite_definition() var planner := ActionEffectPlanner.new() assert_true(planner.unregister_strategy(ActionEffect.KIND_DAMAGE)) assert_null(planner.plan(definition, _player_ref())) assert_true(_has_error(planner.get_last_errors(), "missing strategy for effect kind 'damage'")) assert_has(planner.get_last_reason_trace(), &"planning_failed_closed") assert_true(planner.register_builtin_strategy(ActionEffect.KIND_DAMAGE)) assert_not_null(planner.plan(definition, _player_ref())) assert_false(planner.register_builtin_strategy(&"unknown_effect_kind")) var unknown := _single_effect_definition( _effect(&"unknown", &"unknown_effect_kind", &"field", 1.0) ) assert_null(planner.plan(unknown, _player_ref())) assert_true(_has_error(planner.get_last_errors(), "effect_kind 'unknown_effect_kind'")) var duplicate := _single_effect_definition( _effect(&"duplicate", ActionEffect.KIND_METRIC_DELTA, &"safety", 1.0) ) duplicate.effects.append(_effect(&"duplicate", ActionEffect.KIND_NEED_DELTA, &"energy", -1.0)) assert_null(planner.plan(duplicate, _player_ref())) assert_true(_has_error(planner.get_last_errors(), "duplicate effect_id 'duplicate'")) assert_null(planner.plan(definition, WorldEntityRef.create(&"world", &"one"))) assert_true(_has_error(planner.get_last_errors(), "actor must be a player or person")) func test_plan_validation_rejects_duplicate_ids_order_changes_and_hidden_fields() -> void: var valid_plan := ActionEffectPlanner.new().plan(_composite_definition(), _player_ref()) var operations := valid_plan.get_operations() operations[1]["operation_id"] = operations[0]["operation_id"] var duplicate_operation := ActionEffectPlan.new(&"composite", operations, [&"test"]) assert_false(duplicate_operation.is_valid()) assert_true(_has_error(duplicate_operation.get_errors(), "duplicate operation_id")) operations = valid_plan.get_operations() operations[1]["effect_id"] = operations[0]["effect_id"] var duplicate_effect := ActionEffectPlan.new(&"composite", operations, [&"test"]) assert_false(duplicate_effect.is_valid()) assert_true(_has_error(duplicate_effect.get_errors(), "duplicate effect_id")) operations = valid_plan.get_operations() operations[2]["authored_order"] = 4 var reordered := ActionEffectPlan.new(&"composite", operations, [&"test"]) assert_false(reordered.is_valid()) assert_true(_has_error(reordered.get_errors(), "does not preserve authored_order")) operations = valid_plan.get_operations() operations[0]["payload"]["dynamic_method"] = "apply_anything" var hidden_field := ActionEffectPlan.new(&"composite", operations, [&"test"]) assert_false(hidden_field.is_valid()) assert_true(_has_error(hidden_field.get_errors(), "payload has an invalid field set")) func test_runtime_objects_callables_and_node_paths_are_rejected_from_parameters() -> void: var runtime_node := Node.new() var forbidden_values: Array = [ runtime_node, Resource.new(), Callable(self, "_player_ref"), NodePath("../Authority"), ] for forbidden in forbidden_values: var effect := _effect( &"unsafe", ActionEffect.KIND_METRIC_DELTA, &"safety", 1.0, {"unsafe": forbidden} ) var planner := ActionEffectPlanner.new() assert_null(planner.plan(_single_effect_definition(effect), _player_ref())) assert_true( _has_error(planner.get_last_errors(), "parameters"), "Forbidden parameter should fail: %s" % type_string(typeof(forbidden)), ) runtime_node.free() func test_transactional_committer_invokes_one_whole_plan_and_reports_exact_outcome() -> void: var plan := ActionEffectPlanner.new().plan(_composite_definition(), _person_ref()) var committer := RecordingCommitter.new() var committed := committer.commit(plan, {"command_id": "command_1", "revision": 7}) assert_true(committed.is_valid()) assert_true(committed.did_commit()) assert_false(committed.was_rolled_back()) assert_eq(committed.get_event_ids(), [101, 104]) assert_eq(committer.call_count, 1) assert_eq(committer.operation_count, plan.get_operation_count()) committer.should_fail = true var rejected := committer.commit(plan, {"command_id": "command_2", "revision": 8}) assert_true(rejected.is_valid()) assert_false(rejected.did_commit()) assert_true(rejected.was_rolled_back()) assert_true(rejected.get_event_ids().is_empty()) assert_eq(rejected.get_reason_code(), &"authority_rejected") assert_eq(committer.call_count, 2, "One commit request should make one atomic authority call") var runtime_node := Node.new() var invalid_context := committer.commit(plan, {"authority_node": runtime_node}) assert_eq(invalid_context.get_reason_code(), &"invalid_commit_context") assert_true(invalid_context.was_rolled_back()) assert_eq(committer.call_count, 2, "Invalid context must fail before the authority callback") runtime_node.free() var invalid_committer := InvalidResultCommitter.new() var invalid_result := invalid_committer.commit(plan) assert_eq(invalid_committer.call_count, 1) assert_eq(invalid_result.get_reason_code(), &"invalid_commit_result") assert_true(invalid_result.was_rolled_back()) assert_false(ActionEffectCommitResult.committed([4, 4]).is_valid()) assert_true(TransactionalActionEffectCommitter.new().commit(plan).was_rolled_back()) func _composite_definition() -> ActionDefinition: var definition := ActionDefinition.new() definition.action_id = &"composite_action" definition.display_name = "Composite action" definition.target_type = SimulationIds.TARGET_ACTIVITY var inventory := _effect( &"03_transfer", ActionEffect.KIND_INVENTORY_TRANSFER, &"", 0.0, {"exact": true} ) inventory.item_id = SimulationIds.RESOURCE_FOOD inventory.amount = 2.0 inventory.source_role = &"actor" inventory.destination_role = &"target" var damage := _effect(&"04_damage", ActionEffect.KIND_DAMAGE) damage.amount = 12.0 damage.parameters = {"damage_type": "physical"} var healing := _effect(&"05_healing", ActionEffect.KIND_HEALING) healing.amount = 5.0 var schedule := _effect(&"07_schedule", ActionEffect.KIND_SCHEDULE_ACTION) schedule.scheduled_action_id = SimulationIds.ACTION_REST schedule.parameters = {"delay_ticks": 4} definition.effects = [ _effect( &"01_metric", ActionEffect.KIND_METRIC_DELTA, &"safety", 2.5, {"scope": "village"}, ), _effect(&"02_need", ActionEffect.KIND_NEED_DELTA, &"hunger", -10.0), inventory, damage, healing, _effect( &"06_relationship", ActionEffect.KIND_RELATIONSHIP_DELTA, &"trust", 0.25, {"other_role": "target"}, ), schedule, _effect(&"08_site", ActionEffect.KIND_SITE_CONDITION, &"fire_lit", 1.0), ] return definition func _single_effect_definition(effect: ActionEffect) -> ActionDefinition: var definition := ActionDefinition.new() definition.action_id = &"test_action" definition.display_name = "Test action" definition.target_type = SimulationIds.TARGET_ACTIVITY definition.effects = [effect] return definition func _effect( effect_id: StringName, effect_kind: StringName, subject_key: StringName = &"", value: float = 0.0, parameters: Dictionary = {} ) -> ActionEffect: var effect := ActionEffect.new() effect.effect_id = effect_id effect.effect_kind = effect_kind effect.subject_key = subject_key effect.value = value effect.parameters = parameters return effect func _player_ref() -> WorldEntityRef: return WorldEntityRef.create(SimulationIds.ENTITY_PLAYER, &"player") func _person_ref() -> WorldEntityRef: return WorldEntityRef.create(SimulationIds.ENTITY_PERSON, &"7") func _operation_kinds(plan: ActionEffectPlan) -> Array[StringName]: var kinds: Array[StringName] = [] for operation in plan.get_operations(): kinds.append(StringName(operation["operation_kind"])) return kinds func _has_error(errors: Array[String], fragment: String) -> bool: for error in errors: if error.contains(fragment): return true return false