Files
TheTree_mock/tests/s2/test_inventory_logistics.gd
T

592 lines
25 KiB
GDScript

extends SceneTree
const InventoryServiceScript = preload("res://scripts/inventory/inventory_service.gd")
const LogisticsRequestScript = preload("res://scripts/inventory/logistics_request.gd")
const LogisticsDispatcherScript = preload("res://scripts/inventory/logistics_dispatcher.gd")
const InventoryReservationScript = preload("res://scripts/inventory/inventory_reservation.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: Inventory and Logistics")
_test_reservation_limits_and_idempotency()
_test_transfer_never_duplicates_stock()
_test_expiration_rules()
_test_orphan_repair_report()
_test_dispatch_priority_capacity_and_tie_break()
_test_dispatch_missing_carrier_preserves_reserved_load()
_test_atomic_transfer_batches_match_sequential_operations()
_test_idempotency_conflicts_are_preflighted()
_test_request_transitions()
_test_snapshot_round_trip_and_atomic_restore()
_test_strict_snapshot_migration_and_id_tombstones()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_reservation_limits_and_idempotency() -> void:
var service = InventoryServiceScript.new()
var added: Dictionary = service.add_stack(&"warehouse", &"meal", 10, 90, -1, 0, 2)
_expect(added[&"ok"], "stock can be added to an owner inventory")
var first: Dictionary = service.reserve_item(
&"warehouse", &"meal", 7, &"cafeteria", &"service", 10, 60, &"reserve-a"
)
_expect(first[&"ok"] and service.available_quantity(&"warehouse", &"meal", 10) == 3, "reservation reduces available but not physical quantity")
var replay: Dictionary = service.reserve_item(
&"warehouse", &"meal", 7, &"cafeteria", &"service", 10, 60, &"reserve-a"
)
_expect(replay[&"ok"] and replay[&"replay"] and service.get_reservations(true).size() == 1, "reservation operation id replays without creating stock claims")
var rejected: Dictionary = service.reserve_item(
&"warehouse", &"meal", 4, &"kitchen", &"service", 10, 60, &"reserve-b"
)
_expect(not rejected[&"ok"] and rejected[&"code"] == &"INSUFFICIENT_AVAILABLE", "over-reservation is rejected atomically")
var reservation_id: StringName = first[&"reservation_ids"][0]
var consumed: Dictionary = service.consume_reservation(reservation_id, &"consume-a", 20)
var quantity_after := service.total_quantity(&"warehouse", &"meal")
var consumed_again: Dictionary = service.consume_reservation(reservation_id, &"consume-b", 21)
_expect(consumed[&"ok"] and quantity_after == 3, "consumption removes exactly the reserved physical stock")
_expect(consumed_again[&"ok"] and consumed_again[&"replay"] and service.total_quantity(&"warehouse", &"meal") == quantity_after, "a consumed reservation cannot consume twice")
_expect(service.validate_invariants()[&"ok"], "reserve and consume preserve quantity minus reserved invariant")
func _test_transfer_never_duplicates_stock() -> void:
var service = InventoryServiceScript.new()
service.add_stack(&"source", &"material", 12, 75, 500, 3, 3)
var before := service.total_quantity(&"source", &"material") + service.total_quantity(&"destination", &"material")
var moved: Dictionary = service.transfer_available(
&"source", &"destination", &"material", 5, &"transfer-a", 20
)
var after := service.total_quantity(&"source", &"material") + service.total_quantity(&"destination", &"material")
_expect(moved[&"ok"] and before == after and after == 12, "owner transfer conserves global item quantity")
_expect(service.total_quantity(&"source", &"material") == 7 and service.total_quantity(&"destination", &"material") == 5, "transfer subtracts source before adding destination")
var replay: Dictionary = service.transfer_available(
&"source", &"destination", &"material", 5, &"transfer-a", 20
)
_expect(replay[&"replay"] and service.total_quantity(&"destination", &"material") == 5, "transfer replay cannot duplicate destination stock")
var reserved: Dictionary = service.reserve_item(
&"source", &"material", 4, &"delivery-1", &"logistics", 21, 100, &"pickup-reserve"
)
var pickup: Dictionary = service.transfer_reserved(
reserved[&"reservation_ids"], &"carrier-cargo", &"pickup-a", 22
)
_expect(pickup[&"ok"] and pickup[&"weight"] == 12, "reserved pickup transfers the exact reserved weight")
_expect(service.total_quantity(&"source", &"material") == 3 and service.total_quantity(&"carrier-cargo", &"material") == 4, "reserved pickup consumes reservation and creates cargo once")
_expect(service.validate_invariants()[&"ok"], "available and reserved remain valid after transfer")
func _test_expiration_rules() -> void:
var service = InventoryServiceScript.new()
var added: Dictionary = service.add_stack(&"cold-store", &"food", 8, 80, 30, 0, 1)
var reservation: Dictionary = service.reserve_item(
&"cold-store", &"food", 3, &"service-a", &"meal", 10, 50, &"expiry-reserve"
)
_expect(not service.is_stack_expired(added[&"stack_id"], 29), "stack remains valid before expires_at")
_expect(service.is_stack_expired(added[&"stack_id"], 30), "stack expires exactly at expires_at")
var report: Dictionary = service.process_expired(30)
_expect(report[&"discarded_quantity"] == 5 and service.total_quantity(&"cold-store", &"food") == 3, "expiration discards only unreserved quantity")
var consume: Dictionary = service.consume_reservation(reservation[&"reservation_ids"][0], &"expired-consume", 30)
_expect(not consume[&"ok"] and consume[&"code"] == &"STACK_EXPIRED", "reserved stack is checked again before consumption")
service.process_expired(50)
_expect(service.total_quantity(&"cold-store", &"food") == 0, "reservation expiry releases and discards remaining expired stock")
func _test_orphan_repair_report() -> void:
var service = InventoryServiceScript.new()
var added: Dictionary = service.add_stack(&"store", &"part", 5)
var reservation: Dictionary = service.reserve_item(
&"store", &"part", 2, &"repair-job", &"build", 0, -1, &"orphan-reserve"
)
var stacks: Dictionary = service._inventories[&"store"]
stacks.erase(added[&"stack_id"])
service._inventories[&"store"] = stacks
var report: Dictionary = service.audit_and_repair_reservations(5)
var repaired_reservation = service.get_reservation(reservation[&"reservation_ids"][0])
_expect(report[&"orphan_count"] == 1 and report[&"released_reservation_ids"].size() == 1, "orphan repair returns a structured repair report")
_expect(repaired_reservation.state == InventoryReservationScript.State.RELEASED, "orphan repair closes the dangling reservation")
func _test_dispatch_priority_capacity_and_tie_break() -> void:
var high = _reserved_request(&"request-high", 90, 6, 300)
var low = _reserved_request(&"request-low", 20, 6, 400)
var dispatcher = LogisticsDispatcherScript.new()
var carriers := {
&"carrier-b": {
&"position": Vector3i(1, 0, 1),
&"capacity": 10,
&"current_load": 0,
&"role": &"logistics",
&"path_minutes": {&"default": 5},
},
&"carrier-a": {
&"position": Vector3i(0, 0, 1),
&"capacity": 10,
&"current_load": 0,
&"role": &"logistics",
&"path_minutes": {&"default": 5},
},
}
var assignments: Array[Dictionary] = dispatcher.dispatch([low, high], carriers, 100)
_expect(assignments.size() == 2 and assignments[0][&"request_id"] == &"request-high", "dispatcher evaluates requests in deterministic priority order")
_expect(assignments[0][&"carrier_id"] == &"carrier-a", "equal scores use lexical carrier id as tie break")
_expect(assignments[1][&"carrier_id"] == &"carrier-b", "reserved load prevents assigning beyond one carrier capacity")
_expect(dispatcher.get_reserved_load(&"carrier-a") == 6 and dispatcher.get_reserved_load(&"carrier-b") == 6, "dispatcher records capacity reservations for later passes")
var reordered_dispatcher = LogisticsDispatcherScript.new()
var reordered_carriers := {
&"carrier-a": carriers[&"carrier-a"],
&"carrier-b": carriers[&"carrier-b"],
}
var reordered_assignments: Array[Dictionary] = reordered_dispatcher.dispatch([
_reserved_request(&"request-high", 90, 6, 300),
_reserved_request(&"request-low", 20, 6, 400),
], reordered_carriers, 100)
_expect(
reordered_assignments == assignments,
"one-pass carrier ordering remains deterministic across request and Dictionary insertion order"
)
func _test_dispatch_missing_carrier_preserves_reserved_load() -> void:
var dispatcher = LogisticsDispatcherScript.new()
var carrier := {
&"carrier-a": {
&"position": Vector3i.ZERO,
&"capacity": 10,
&"current_load": 0,
&"role": &"logistics",
&"path_minutes": 1,
},
}
var first = _reserved_request(&"request-carrier-present", 90, 6, 300)
_expect(
dispatcher.dispatch([first], carrier, 100).size() == 1,
"carrier accepts an assignment within capacity"
)
var second = _reserved_request(&"request-carrier-absent", 80, 6, 310)
_expect(
dispatcher.dispatch([second], {}, 101).is_empty()
and dispatcher.get_reserved_load(&"carrier-a") == 6,
"temporarily absent carrier keeps its accepted reserved load"
)
var absent_snapshot: Dictionary = dispatcher.create_snapshot()
var restored = LogisticsDispatcherScript.new()
_expect(
restored.restore_snapshot(absent_snapshot)
and restored.create_snapshot() == absent_snapshot,
"missing-carrier reservation snapshot restores exactly"
)
_expect(
restored.dispatch([second], carrier, 102).is_empty()
and restored.get_reserved_load(&"carrier-a") == 6,
"reappearing carrier cannot accept another six units into capacity ten"
)
_expect(
restored.release_assignment(&"carrier-a", 6) == 0
and restored.create_snapshot()[&"carrier_loads"].is_empty(),
"explicit assignment release removes the retained carrier load"
)
func _test_atomic_transfer_batches_match_sequential_operations() -> void:
var seed = InventoryServiceScript.new()
seed.add_stack(&"warehouse", &"supply", 4, 87, 600, 3, 2, &"batch-source")
var reservation_a: Dictionary = seed.reserve_item(
&"warehouse", &"supply", 2, &"request-a", &"delivery", 10, 100, &"batch-reserve-a"
)
var reservation_b: Dictionary = seed.reserve_item(
&"warehouse", &"supply", 2, &"request-b", &"delivery", 10, 100, &"batch-reserve-b"
)
var ids_a: Array[StringName] = []
var ids_b: Array[StringName] = []
ids_a.assign(reservation_a[&"reservation_ids"])
ids_b.assign(reservation_b[&"reservation_ids"])
var sequential = InventoryServiceScript.new()
var batched = InventoryServiceScript.new()
sequential.restore_snapshot(seed.create_snapshot())
batched.restore_snapshot(seed.create_snapshot())
sequential.transfer_reserved(ids_a, &"cargo-a", &"pickup-a", 20)
sequential.transfer_reserved(ids_b, &"cargo-b", &"pickup-b", 20)
var pickup_entries: Array[Dictionary] = [
{&"reservation_ids": ids_a, &"destination_inventory_id": &"cargo-a", &"operation_id": &"pickup-a"},
{&"reservation_ids": ids_b, &"destination_inventory_id": &"cargo-b", &"operation_id": &"pickup-b"},
]
var pickup_batch: Dictionary = batched.transfer_reserved_batch(pickup_entries, 20)
_expect(
pickup_batch[&"ok"] and pickup_batch[&"transfer_count"] == 2,
"reserved-stock batch applies every ordered pickup atomically"
)
_expect(
batched.create_snapshot() == sequential.create_snapshot(),
"reserved-stock batch is snapshot-equivalent to sequential public operations"
)
var before_replay := batched.create_snapshot()
var pickup_replay: Dictionary = batched.transfer_reserved_batch(pickup_entries, 20)
_expect(
pickup_replay[&"ok"]
and pickup_replay[&"results"][0][&"replay"]
and pickup_replay[&"results"][1][&"replay"]
and batched.create_snapshot() == before_replay,
"batch replay preserves per-operation idempotency without another transfer"
)
var pickup_snapshot := batched.create_snapshot()
sequential.transfer_available(&"cargo-a", &"destination", &"supply", 2, &"deliver-a", 21)
sequential.transfer_available(&"cargo-b", &"destination", &"supply", 2, &"deliver-b", 21)
var delivery_batch: Dictionary = batched.transfer_available_batch([
{
&"source_inventory_id": &"cargo-a",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"deliver-a",
},
{
&"source_inventory_id": &"cargo-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"deliver-b",
},
], 21)
_expect(
delivery_batch[&"ok"] and batched.create_snapshot() == sequential.create_snapshot(),
"available-stock batch preserves FEFO results, stack ids, and operation order"
)
var rollback = InventoryServiceScript.new()
rollback.restore_snapshot(pickup_snapshot)
var before_failure := rollback.create_snapshot()
var failed_batch: Dictionary = rollback.transfer_available_batch([
{
&"source_inventory_id": &"cargo-a",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"rollback-first",
},
{
&"source_inventory_id": &"missing-cargo",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 1,
&"operation_id": &"rollback-second",
},
], 22)
_expect(
not failed_batch[&"ok"] and failed_batch[&"code"] == &"ATOMIC_BATCH_FAILED",
"a failed line rejects the complete transfer batch"
)
_expect(
rollback.create_snapshot() == before_failure,
"failed batch rolls back stock, generated ids, and idempotency records exactly"
)
func _test_idempotency_conflicts_are_preflighted() -> void:
var sequential = InventoryServiceScript.new()
sequential.add_stack(&"source-a", &"supply", 4)
sequential.add_stack(&"source-b", &"supply", 4)
var first: Dictionary = sequential.transfer_available(
&"source-a", &"destination", &"supply", 2, &"shared-operation", 10
)
var after_first := sequential.create_snapshot()
var conflict: Dictionary = sequential.transfer_available(
&"source-b", &"destination", &"supply", 2, &"shared-operation", 10
)
_expect(first[&"ok"], "first normalized input binds its operation id")
_expect(
not conflict[&"ok"]
and conflict[&"code"] == &"IDEMPOTENCY_CONFLICT"
and sequential.create_snapshot() == after_first,
"sequential operation-id reuse with different input is rejected without side effects"
)
var action_conflict: Dictionary = sequential.reserve_item(
&"source-a", &"supply", 1, &"requester", &"delivery", 10, -1, &"shared-operation"
)
_expect(
not action_conflict[&"ok"]
and action_conflict[&"code"] == &"IDEMPOTENCY_CONFLICT"
and sequential.create_snapshot() == after_first,
"operation id is bound to its action as well as normalized input"
)
var duplicate_batch = InventoryServiceScript.new()
duplicate_batch.add_stack(&"source-a", &"supply", 4)
duplicate_batch.add_stack(&"source-b", &"supply", 4)
var before_duplicate := duplicate_batch.create_snapshot()
var duplicate_result: Dictionary = duplicate_batch.transfer_available_batch([
{
&"source_inventory_id": &"source-a",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"batch-shared",
},
{
&"source_inventory_id": &"source-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"batch-shared",
},
], 10)
_expect(
not duplicate_result[&"ok"]
and duplicate_result[&"code"] == &"IDEMPOTENCY_CONFLICT"
and duplicate_batch.create_snapshot() == before_duplicate,
"A-to-D then B-to-D duplicate operation id is rejected before the batch mutates"
)
var existing_conflict = InventoryServiceScript.new()
existing_conflict.add_stack(&"source-a", &"supply", 4)
existing_conflict.add_stack(&"source-b", &"supply", 4)
existing_conflict.transfer_available(
&"source-a", &"destination", &"supply", 1, &"already-bound", 11
)
var before_existing_conflict := existing_conflict.create_snapshot()
var existing_result: Dictionary = existing_conflict.transfer_available_batch([
{
&"source_inventory_id": &"source-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 1,
&"operation_id": &"would-be-first",
},
{
&"source_inventory_id": &"source-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 1,
&"operation_id": &"already-bound",
},
], 11)
_expect(
not existing_result[&"ok"]
and existing_result[&"code"] == &"IDEMPOTENCY_CONFLICT"
and existing_conflict.create_snapshot() == before_existing_conflict,
"existing operation-id conflict is found before an earlier batch row can apply"
)
func _test_request_transitions() -> void:
var request = _reserved_request(&"request-state", 50, 4, 200)
_expect(request.assign(&"carrier", 20), "RESERVED request accepts a carrier assignment")
_expect(request.transition_to(LogisticsRequestScript.State.PICKING_UP, "", 21), "ASSIGNED transitions to PICKING_UP")
_expect(request.transition_to(LogisticsRequestScript.State.DELIVERING, "", 22), "PICKING_UP transitions to DELIVERING")
_expect(request.transition_to(LogisticsRequestScript.State.COMPLETED, "", 23), "DELIVERING transitions to COMPLETED")
_expect(not request.transition_to(LogisticsRequestScript.State.OPEN, "", 24), "terminal logistics request cannot reopen")
func _test_snapshot_round_trip_and_atomic_restore() -> void:
var original = InventoryServiceScript.new()
original.add_stack(&"warehouse", &"supply", 10, 88, 700, 5, 2)
original.reserve_item(&"warehouse", &"supply", 4, &"facility", &"restock", 10, 80, &"snapshot-reserve")
var snapshot: Dictionary = original.create_snapshot()
var restored = InventoryServiceScript.new()
_expect(restored.restore_snapshot(snapshot), "valid inventory snapshot restores")
_expect(restored.create_snapshot() == snapshot, "inventory snapshot round trip is exact")
var before_invalid: Dictionary = restored.create_snapshot()
var invalid: Dictionary = snapshot.duplicate(true)
invalid[&"inventories"][0][&"stacks"][0][&"reserved_quantity"] = 99
_expect(not restored.restore_snapshot(invalid), "invalid inventory snapshot is rejected")
_expect(restored.create_snapshot() == before_invalid, "failed restore leaves live inventory state untouched")
var request = _reserved_request(&"request-save", 70, 3, 500)
var request_snapshot: Dictionary = request.create_snapshot()
var restored_request = LogisticsRequestScript.from_snapshot(request_snapshot)
_expect(restored_request != null and restored_request.create_snapshot() == request_snapshot, "logistics request round trip preserves state")
var dispatcher = LogisticsDispatcherScript.new()
dispatcher.dispatch([request], {
&"carrier": {
&"capacity": 10,
&"current_load": 0,
&"role": &"porter",
&"path_minutes": 2,
&"position": Vector3i.ZERO,
},
}, 40)
var dispatcher_snapshot: Dictionary = dispatcher.create_snapshot()
var restored_dispatcher = LogisticsDispatcherScript.new()
_expect(restored_dispatcher.restore_snapshot(dispatcher_snapshot), "dispatcher snapshot restores")
_expect(restored_dispatcher.create_snapshot() == dispatcher_snapshot, "dispatcher snapshot round trip is exact")
func _test_strict_snapshot_migration_and_id_tombstones() -> void:
var base = InventoryServiceScript.new()
base.add_stack(&"warehouse", &"supply", 3, 90, 100, 5, 1)
base.reserve_item(
&"warehouse", &"supply", 1, &"requester", &"delivery", 6, 90, &"legacy-reserve"
)
var snapshot: Dictionary = base.create_snapshot()
var guard = InventoryServiceScript.new()
_expect(guard.restore_snapshot(snapshot), "strict snapshot fixture restores before corruption checks")
var guard_snapshot := guard.create_snapshot()
var negative_minute := snapshot.duplicate(true)
negative_minute[&"current_minute"] = -1
var negative_created := snapshot.duplicate(true)
negative_created[&"inventories"][0][&"stacks"][0][&"created_at"] = -1
var negative_expiry := snapshot.duplicate(true)
negative_expiry[&"inventories"][0][&"stacks"][0][&"expires_at"] = -2
var invalid_terminal := snapshot.duplicate(true)
invalid_terminal[&"reservations"][0][&"terminal_at"] = 0
_expect(
not guard.restore_snapshot(negative_minute)
and not guard.restore_snapshot(negative_created)
and not guard.restore_snapshot(negative_expiry)
and not guard.restore_snapshot(invalid_terminal),
"restore rejects negative or state-inconsistent canonical timestamps instead of clamping"
)
_expect(
guard.create_snapshot() == guard_snapshot,
"strict timestamp rejection is atomic"
)
var low_stack_sequence := snapshot.duplicate(true)
low_stack_sequence[&"next_stack_sequence"] = 1
var low_reservation_sequence := snapshot.duplicate(true)
low_reservation_sequence[&"next_reservation_sequence"] = 1
_expect(
not guard.restore_snapshot(low_stack_sequence)
and not guard.restore_snapshot(low_reservation_sequence)
and guard.create_snapshot() == guard_snapshot,
"next sequences at or below existing canonical ids are atomically rejected"
)
var tombstone_source = InventoryServiceScript.new()
var first_stack: Dictionary = tombstone_source.add_stack(&"source", &"part", 1)
var reservation: Dictionary = tombstone_source.reserve_item(
&"source", &"part", 1, &"consumer", &"use", 0, -1, &"tombstone-reserve"
)
tombstone_source.consume_reservation(
reservation[&"reservation_ids"][0], &"tombstone-consume", 1
)
var tombstone_snapshot := tombstone_source.create_snapshot()
_expect(
tombstone_source.total_quantity(&"source", &"part") == 0
and tombstone_snapshot[&"known_stack_ids"].has(String(first_stack[&"stack_id"])),
"deleted generated stack id remains in persisted known-id history"
)
var corrupt_tombstone_sequence := tombstone_snapshot.duplicate(true)
corrupt_tombstone_sequence[&"next_stack_sequence"] = 1
var tombstone_restored = InventoryServiceScript.new()
_expect(
not tombstone_restored.restore_snapshot(corrupt_tombstone_sequence),
"tombstone prevents a corrupted sequence from reusing a deleted auto id"
)
_expect(
tombstone_restored.restore_snapshot(tombstone_snapshot)
and tombstone_restored.create_snapshot() == tombstone_snapshot
and tombstone_restored.add_stack(&"source", &"part", 1)[&"stack_id"] == &"stack_00000002",
"valid tombstone snapshot round-trips exactly and allocates the next unused id"
)
var legacy_deleted = InventoryServiceScript.new()
var migrated_deleted_ok := legacy_deleted.restore_snapshot(
_as_legacy_inventory_snapshot(tombstone_snapshot)
)
_expect(
migrated_deleted_ok
and legacy_deleted.create_snapshot()[&"known_stack_ids"].has("stack_00000001"),
"version-1 counter migration reconstructs tombstones for deleted generated ids"
)
var legacy_snapshot := _as_legacy_inventory_snapshot(snapshot)
var migrated = InventoryServiceScript.new()
var legacy_restored := migrated.restore_snapshot(legacy_snapshot)
var legacy_replay: Dictionary = migrated.reserve_item(
&"warehouse", &"supply", 1, &"requester", &"delivery", 6, 90, &"legacy-reserve"
)
_expect(
legacy_restored and legacy_replay[&"ok"] and legacy_replay[&"replay"],
"version-1 snapshot migrates while preserving legacy replay behavior"
)
var rebound_conflict: Dictionary = migrated.reserve_item(
&"warehouse", &"supply", 2, &"requester", &"delivery", 6, 90, &"legacy-reserve"
)
_expect(
not rebound_conflict[&"ok"] and rebound_conflict[&"code"] == &"IDEMPOTENCY_CONFLICT",
"first legacy replay upgrades the operation id to an input-bound record"
)
var migrated_snapshot: Dictionary = migrated.create_snapshot()
var migrated_roundtrip = InventoryServiceScript.new()
_expect(
migrated_snapshot[&"version"] == 2
and migrated_roundtrip.restore_snapshot(migrated_snapshot)
and migrated_roundtrip.create_snapshot() == migrated_snapshot,
"migrated version-2 snapshot is canonical and exact on subsequent restore"
)
var ambiguous_legacy := _as_legacy_inventory_snapshot(snapshot)
ambiguous_legacy[&"idempotency_results"].append({
&"operation_key": "transfer|legacy-reserve",
&"result": {&"ok": true, &"code": &"OK", &"replay": false},
})
var ambiguous = InventoryServiceScript.new()
var ambiguous_loaded := ambiguous.restore_snapshot(ambiguous_legacy)
var ambiguous_use: Dictionary = ambiguous.transfer_available(
&"warehouse", &"destination", &"supply", 1, &"legacy-reserve", 6
)
_expect(
ambiguous_loaded
and not ambiguous_use[&"ok"]
and ambiguous_use[&"code"] == &"IDEMPOTENCY_CONFLICT",
"legacy cross-action duplicate ids load compatibly and fail closed on replay"
)
func _as_legacy_inventory_snapshot(snapshot: Dictionary) -> Dictionary:
var legacy_snapshot := snapshot.duplicate(true)
legacy_snapshot[&"version"] = 1
legacy_snapshot.erase(&"known_stack_ids")
legacy_snapshot.erase(&"known_reservation_ids")
var legacy_results: Array[Dictionary] = []
for record: Dictionary in legacy_snapshot[&"idempotency_results"]:
legacy_results.append({
&"operation_key": "%s|%s" % [record[&"action"], record[&"operation_id"]],
&"result": (record[&"result"] as Dictionary).duplicate(true),
})
legacy_snapshot[&"idempotency_results"] = legacy_results
return legacy_snapshot
func _reserved_request(
request_id: StringName,
priority: int,
weight: int,
deadline: int
):
var request = LogisticsRequestScript.new().configure(
request_id,
&"warehouse",
&"facility",
{&"supply": weight},
priority,
10,
deadline,
weight
)
var reservations: Array[StringName] = [StringName("reservation-%s" % request_id)]
var stacks: Array[StringName] = [StringName("stack-%s" % request_id)]
request.mark_reserved(reservations, stacks, 11)
return request
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)