Files
TheTree_mock/tests/s0/test_command_transaction.gd
T

204 lines
9.0 KiB
GDScript

extends SceneTree
const CommandType = preload("res://scripts/core/command.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
const CommandQueueType = preload("res://scripts/core/command_queue.gd")
const TransactionType = preload("res://scripts/core/state_transaction.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S0: Command Queue and State Transaction")
_test_deterministic_queue_order()
_test_idempotency_and_result_retention()
_test_snapshot_preserves_accepted_idempotency_history()
_test_legacy_snapshot_migrates_accepted_metadata()
_test_transaction_commit()
_test_validation_is_atomic()
_test_apply_failure_rolls_back()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_deterministic_queue_order() -> void:
var queue = CommandQueueType.new()
_expect(queue.enqueue(_command(3, 11, 100, &"three")).is_success(), "command 3 accepted")
_expect(queue.enqueue(_command(2, 10, 10, &"two")).is_success(), "command 2 accepted")
_expect(queue.enqueue(_command(1, 10, 10, &"one")).is_success(), "command 1 accepted")
_expect(queue.enqueue(_command(4, 10, 90, &"four")).is_success(), "command 4 accepted")
var ready: Array = queue.drain_ready(10)
var ids: Array[int] = []
for command in ready:
ids.append(command.command_id)
_expect(ids == [4, 1, 2], "ready order is tick, descending priority, command id")
_expect(queue.next_execute_tick() == 11, "future command remains queued")
func _test_idempotency_and_result_retention() -> void:
var queue = CommandQueueType.new()
var original = _command(10, 1, 0, &"same-operation")
_expect(queue.enqueue(original).is_success(), "idempotent command accepted once")
var duplicate = _command(11, 1, 0, &"same-operation")
var duplicate_result = queue.enqueue(duplicate)
_expect(
duplicate_result.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY,
"duplicate idempotency key rejected"
)
queue.drain_ready(1)
queue.complete(10, CommandResultType.succeeded(10, {"value": 42}))
var retained = queue.get_result_by_idempotency_key(&"same-operation")
_expect(retained != null and retained.details["value"] == 42, "processed result retained by key")
_expect(retained.is_replay, "retained lookup is marked as replay")
func _test_snapshot_preserves_accepted_idempotency_history() -> void:
var queue = CommandQueueType.new()
_expect(queue.enqueue(_command(1, 0, 10, &"processed-key")).is_success(), "snapshot fixture accepts keyed command")
_expect(queue.enqueue(_command(2, 0, 5, &"")).is_success(), "snapshot fixture accepts keyless command")
_expect(queue.enqueue(_command(3, 5, 1, &"pending-key")).is_success(), "snapshot fixture accepts pending keyed command")
var completed: Array = queue.execute_ready(0, func(command: SimulationCommand) -> CommandResult:
return CommandResultType.succeeded(command.command_id, {"completed": true})
)
_expect(completed.size() == 2, "snapshot fixture completes its ready commands")
var snapshot: Dictionary = queue.create_snapshot()
_expect(int(snapshot["version"]) == CommandQueueType.SNAPSHOT_VERSION, "Command Queue writes the current snapshot version")
_expect(snapshot["accepted"] == [
{"command_id": 1, "idempotency_key": "processed-key"},
{"command_id": 2, "idempotency_key": ""},
{"command_id": 3, "idempotency_key": "pending-key"},
], "snapshot retains accepted metadata after Command inputs leave the queue")
var restored = CommandQueueType.new()
_expect(restored.restore_snapshot(snapshot), "strict current Command Queue snapshot restores")
_expect(restored.create_snapshot() == snapshot, "current Command Queue snapshot round trip is exact")
var metadata: Array[Dictionary] = restored.get_accepted_command_metadata()
metadata[0]["idempotency_key"] = "changed"
metadata.clear()
_expect(restored.get_accepted_command_metadata() == snapshot["accepted"], "accepted metadata query is a defensive deep copy")
var live_before: Dictionary = restored.create_snapshot()
var missing_map: Dictionary = snapshot.duplicate(true)
missing_map["idempotency"].erase("processed-key")
_expect(not restored.restore_snapshot(missing_map), "processed idempotency mapping deletion is rejected")
_expect(restored.create_snapshot() == live_before, "mapping deletion rejection is restore-atomic")
var changed_metadata: Dictionary = snapshot.duplicate(true)
changed_metadata["accepted"][0]["idempotency_key"] = ""
_expect(not restored.restore_snapshot(changed_metadata), "accepted metadata cannot disagree with the reverse map")
_expect(restored.create_snapshot() == live_before, "metadata mismatch rejection is restore-atomic")
var missing_metadata: Dictionary = snapshot.duplicate(true)
missing_metadata["accepted"].remove_at(0)
_expect(not restored.restore_snapshot(missing_metadata), "every known Command requires one accepted metadata row")
var unordered_metadata: Dictionary = snapshot.duplicate(true)
var first: Dictionary = unordered_metadata["accepted"][0]
unordered_metadata["accepted"][0] = unordered_metadata["accepted"][1]
unordered_metadata["accepted"][1] = first
_expect(not restored.restore_snapshot(unordered_metadata), "accepted metadata must be ordered by Command ID")
var unknown_field: Dictionary = snapshot.duplicate(true)
unknown_field["extra"] = true
_expect(not restored.restore_snapshot(unknown_field), "current snapshot rejects unknown root fields")
var string_version: Dictionary = snapshot.duplicate(true)
string_version["version"] = "2"
_expect(not restored.restore_snapshot(string_version), "current snapshot rejects a coerced version")
var float_result_id: Dictionary = snapshot.duplicate(true)
float_result_id["processed"][0]["command_id"] = 1.0
_expect(not restored.restore_snapshot(float_result_id), "current snapshot rejects a coerced processed Command ID")
_expect(restored.create_snapshot() == live_before, "all malformed current snapshots preserve live queue state")
func _test_legacy_snapshot_migrates_accepted_metadata() -> void:
var source = CommandQueueType.new()
source.enqueue(_command(7, 0, 1, &"legacy-processed"))
source.drain_ready(0)
source.complete(7, CommandResultType.succeeded(7))
source.enqueue(_command(9, 20, 1, &"legacy-pending"))
var legacy: Dictionary = source.create_snapshot()
legacy.erase("version")
legacy.erase("accepted")
var restored = CommandQueueType.new()
_expect(restored.restore_snapshot(legacy), "legacy unversioned Command Queue snapshot restores")
var migrated: Dictionary = restored.create_snapshot()
_expect(int(migrated["version"]) == CommandQueueType.SNAPSHOT_VERSION, "legacy restore writes the current version next time")
_expect(migrated["accepted"] == [
{"command_id": 7, "idempotency_key": "legacy-processed"},
{"command_id": 9, "idempotency_key": "legacy-pending"},
], "legacy reverse map reconstructs processed accepted idempotency metadata")
_expect(restored.get_result_by_idempotency_key(&"legacy-processed") != null, "legacy processed key remains exactly-once after migration")
func _test_transaction_commit() -> void:
var state := {"gold": 100, "stock": 3}
var transaction = TransactionType.new()
transaction.stage_dictionary_increment(state, "gold", -40, 0)
transaction.stage_dictionary_increment(state, "stock", -1, 0)
_expect(state == {"gold": 100, "stock": 3}, "staging does not mutate state")
var result = transaction.commit()
_expect(result.is_success(), "valid transaction commits")
_expect(state == {"gold": 60, "stock": 2}, "all staged changes commit together")
func _test_validation_is_atomic() -> void:
var state := {"gold": 100, "stock": 3}
var transaction = TransactionType.new()
transaction.stage_dictionary_set(state, "gold", 60)
transaction.stage_dictionary_set(state, "stock", 2)
transaction.add_validator(func() -> bool: return false)
var result = transaction.commit()
_expect(result.code == CommandResultType.Code.VALIDATION_FAILED, "validation failure reported")
_expect(state == {"gold": 100, "stock": 3}, "validation failure applies no changes")
func _test_apply_failure_rolls_back() -> void:
var state := {"gold": 100, "stock": 3}
var transaction = TransactionType.new()
transaction.stage_dictionary_set(state, "gold", 60)
transaction.stage_custom(
"failing stock update",
func() -> bool: return true,
func() -> bool:
state["stock"] = 2
return false,
func() -> void: state["stock"] = 3
)
var result = transaction.commit()
_expect(
result.code == CommandResultType.Code.TRANSACTION_APPLY_FAILED,
"apply failure reported"
)
_expect(state == {"gold": 100, "stock": 3}, "applied changes roll back in reverse order")
func _command(
command_id: int,
execute_tick: int,
priority: int,
idempotency_key: StringName
):
return CommandType.new(
command_id,
0,
execute_tick,
priority,
&"Test",
&"actor",
&"target",
{},
idempotency_key
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)