Connect paid food purchasing, kitchen production, and porter deliveries

This commit is contained in:
Takeshi
2026-09-09 09:46:33 +09:00
parent 29a660aed0
commit 627b200f00
42 changed files with 3787 additions and 84 deletions
+202
View File
@@ -0,0 +1,202 @@
extends SceneTree
## A real inn from the default 5,000 G / 50 materials. No inserted guests,
## inventory, employees, completed construction, or free resource commands.
var _checks := 0
var _failures := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _ids: Dictionary = {}
var _saw_loaded := false
var _saw_movement := false
var _saw_batch := false
var _save_checked := false
var _last_porter_cell := Vector3i.ZERO
var _save_path := "user://tests/normal_supply_%d/cargo" % OS.get_process_id()
var _main: Control
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 424242)
_clock.set_paused(true)
root.size = Vector2i(1440, 810)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
_main.set("manual_save_path", "user://tests/supply_normal_%d/manual" % OS.get_process_id())
root.add_child(_main)
await process_frame
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(_state.get_resource(&"gold") == 5000 and _state.get_resource(&"materials") == 50, "ordinary starting resources")
for row: Array in [[&"reception", Vector3i(-8, 0, 0)], [&"cafeteria", Vector3i(2, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
if not _command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}):
_finish()
return
await _advance(180)
_command(&"build_facility", {&"definition_id": &"standard_room", &"origin": Vector3i(-8, 2, 0)})
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(-4, 1, 0)})
await _advance(60)
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(-4, 2, 0)})
await _advance(10)
for x: int in [-5, -6]:
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(x, 2, 0)})
await _advance(10)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
_ids[facility.definition_id] = facility.instance_id
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"], [&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
if not _command(&"hire_staff_candidate", {&"candidate_id": assignment[0]}) or not _command(&"set_staff_work_assignment", {&"staff_id": assignment[0], &"facility_id": _ids[assignment[1]], &"template_id": &"DAY"}):
_finish()
return
_expect(_state.get_resource(&"materials") == 4, "all buildings and the staff door fit within the starting material budget")
var warehouse: FacilityInstance = _runtime.facility_service.get_instance(_ids[&"warehouse"])
var cash_before: int = _state.get_resource(&"gold")
_command(&"purchase_supplies", {&"warehouse_id": warehouse.instance_id, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(_state.get_resource(&"gold") == cash_before - 120 and _runtime.inventory_service.total_quantity(warehouse.local_inventory_id, SupplyCatalog.RAW_FOOD) == 40, "40 raw food costs exactly 120 G and arrives only in the warehouse")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
await _advance(120)
_expect(_saw_movement and _saw_loaded, "an employed porter walks the real route and carries purchased food")
_expect(_saw_batch, "the kitchen starts autonomous production without a guest request")
_expect(_save_checked, "a mid-delivery disk save restores ownership and replays exactly")
_expect(_completed_deliveries(SupplyCatalog.RAW_FOOD) > 0 and _completed_deliveries(SupplyCatalog.BASIC_MEAL) > 0, "both warehouse-to-kitchen and kitchen-to-cafeteria deliveries complete")
_expect(_sales(&"cafeteria") > 0, "natural guests pay for meals made from purchased food")
await _capture()
if OS.get_environment("THE_TREE_SUPPLY_PREVIEW") == "1":
_start_preview()
return
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE_ONLY") == "1":
_finish()
return
for day: int in range(1, 4):
await _advance(360 + 1440 * day - _clock.absolute_minute)
var summaries: Array = _runtime.operations_coordinator.get_day_summaries()
_expect(summaries.size() == day and bool(summaries[-1].get(&"balanced", false)), "business day %d closes with a reconciled ledger" % day)
_expect(_state.get_resource(&"gold") >= 0, "day %d retains cash after real payroll and purchases" % day)
if day < 3:
_command(&"purchase_supplies", {&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(_sales(&"standard_room") > 0, "the same inn earns lodging revenue")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "three-day supply history remains restorable")
var evidence := {"checks": _checks, "failures": _failures, "meal_sales": _sales(&"cafeteria"), "lodging_sales": _sales(&"standard_room"), "raw_deliveries": _completed_deliveries(SupplyCatalog.RAW_FOOD), "meal_deliveries": _completed_deliveries(SupplyCatalog.BASIC_MEAL), "cash": _state.get_resource(&"gold"), "closes": _runtime.operations_coordinator.get_day_summaries(), "supply": _runtime.get_supply_management_report()}
var evidence_path := ProjectSettings.globalize_path("res://artifacts/supply-chain/normal-play-report.json")
DirAccess.make_dir_recursive_absolute(evidence_path.get_base_dir())
var file := FileAccess.open(evidence_path, FileAccess.WRITE)
if file != null:
file.store_string(JSON.stringify(evidence, "\t"))
print("NORMAL_SUPPLY_REPORT ", JSON.stringify({"cash": evidence["cash"], "meal_sales": evidence["meal_sales"], "raw_deliveries": evidence["raw_deliveries"], "meal_deliveries": evidence["meal_deliveries"]}))
_finish()
func _advance(minutes: int) -> void:
for index: int in maxi(minutes, 0):
var before: int = _clock.absolute_minute
_clock.advance_minutes(1)
if _clock.absolute_minute != before + 1:
_expect(false, "clock must progress: %s" % _runtime.get_operations_report())
return
var porter: StaffRuntime = _runtime.staff_service.get_staff(&"staff_gina")
if porter != null:
_saw_movement = _saw_movement or porter.current_cell != _last_porter_cell
_last_porter_cell = porter.current_cell
if not porter.carried_inventory_id.is_empty():
_saw_loaded = true
if not _save_checked:
_check_loaded_save()
if _ids.has(&"kitchen"):
_saw_batch = _saw_batch or not _runtime.kitchen_production.get_report(_ids[&"kitchen"]).get(&"active_batch", {}).is_empty()
if index % 120 == 119:
print("SUPPLY_PROGRESS ", _clock.formatted_time(), " meals=", _sales(&"cafeteria"))
await process_frame
func _check_loaded_save() -> void:
_save_checked = true
var saved: Dictionary = _runtime.save_to_path(_save_path)
_expect(bool(saved.get(&"ok", false)), "mid-delivery save writes to an isolated slot")
var before: Dictionary = _runtime.create_snapshot()
var loaded: Dictionary = _runtime.load_from_path(_save_path)
_expect(bool(loaded.get(&"ok", false)), "mid-delivery disk restore: %s" % loaded)
var normalized: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(5)
var expected: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(normalized), "mid-delivery checkpoint rewinds")
_clock.advance_minutes(5)
_expect(_runtime.create_snapshot() == expected, "mid-delivery replay preserves exact inventory, tasks, positions, and ledger")
_expect(_runtime.restore_snapshot(before), "resume the observed minute after replay verification")
func _completed_deliveries(item: StringName) -> int:
var count := 0
for request: LogisticsRequest in _runtime.logistics_requests.values():
if String(request.request_id).begins_with("supply.") and request.state == LogisticsRequest.State.COMPLETED and request.item_requirements.has(item):
count += 1
return count
func _sales(definition_id: StringName) -> int:
var total := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == _ids.get(definition_id, &"missing"):
total += entry.amount
return total
func _command(type: StringName, payload: Dictionary) -> bool:
var result: CommandResult = _runtime.submit_command(type, payload)
_expect(result.is_success(), "%s %s: %s" % [type, payload, result.message])
return result.is_success()
func _capture() -> void:
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE") != "1":
return
await process_frame
_main.call("_activate_management_mode", &"product")
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
for tab: StringName in [&"purchase", &"kitchen", &"delivery"]:
panel.select_tab(tab)
await process_frame
await RenderingServer.frame_post_draw
var path := ProjectSettings.globalize_path("res://artifacts/supply-chain/%s.png" % tab)
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
root.get_texture().get_image().save_png(path)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error("FAIL: " + message)
func _finish() -> void:
SaveService.delete_save(_save_path)
print("Normal supply chain: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
func _start_preview() -> void:
if _failures > 0:
_finish()
return
SaveService.delete_save(_save_path)
root.title = "The Tree · 仕入れと厨房の試遊(保存なし)"
for node: Node in _main.find_children("*", "Button", true, false):
var button := node as Button
for connection: Dictionary in button.pressed.get_connections():
var callback: Callable = connection[&"callable"]
if callback.get_method() in [&"_save_manual", &"_load_manual"]:
button.disabled = true
button.tooltip_text = "この試遊は保存しません。"
_main.call("_activate_management_mode", &"product")
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
panel.select_tab(&"kitchen")
_main.call("_set_status", "通常予算から育てた宿の試遊です。Spaceで再開し、商品タブで仕入れ・調理・配送を確認できます(保存なし)。", true, true)
_clock.set_paused(true)
print("SUPPLY_PREVIEW_READY checks=%d failures=0" % _checks)
@@ -0,0 +1 @@
uid://bb4vjqk7okwwa
@@ -0,0 +1,315 @@
extends SceneTree
## Real construction, hired porter and purchased stock; failure injection is
## limited to route closures, capacity pressure and expired cargo.
var _checks := 0
var _failures := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _source: StringName
var _destination: StringName
var _loaded: Dictionary
var _reserved: Dictionary
const REQUEST := &"supply.bridge_test"
const PORTER := &"staff_gina"
class LateFailInventory extends InventoryService:
var fail_delivery := true
func transfer_available(source_inventory_id: StringName, destination_inventory_id: StringName,
item_id: StringName, quantity: int, operation_id: StringName = &"", absolute_minute: int = -1) -> Dictionary:
var result := super.transfer_available(source_inventory_id, destination_inventory_id, item_id, quantity, operation_id, absolute_minute)
if fail_delivery and String(source_inventory_id).begins_with("cargo:") and result[&"ok"]:
return {&"ok": false, &"code": &"INJECTED_LATE_FAILURE"}
return result
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("Supply bridge: real movement, cargo conservation and recovery")
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_expect(_runtime.initialize(_clock, _state, 653322)[&"valid"], "Runtime initializes")
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [[&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
_expect(_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}), "build %s" % row[0])
_clock.advance_minutes(180)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse": _source = facility.instance_id
if facility.definition_id == &"kitchen": _destination = facility.instance_id
if _source.is_empty() or _destination.is_empty():
_expect(false, "normal construction completes")
_finish()
return
_expect(_command(&"hire_staff_candidate", {&"candidate_id": PORTER}), "hire the catalogue porter")
_expect(_command(&"set_staff_work_assignment", {&"staff_id": PORTER, &"facility_id": _source, &"template_id": &"DAY"}), "assign porter to a repeating real shift")
_expect(_command(&"purchase_supplies", {&"warehouse_id": _source, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8}), "purchase the source stock through the ledger")
_expect(_command(&"create_logistics_request", {&"request_id": REQUEST, &"source_inventory_id": _inventory(_source), &"destination_inventory_id": _inventory(_destination), &"item_requirements": {SupplyCatalog.RAW_FOOD: 8}}), "create the reserved supply request")
_expect(_quantity(_inventory(_destination)) == 0, "request creation does not deliver anything")
_test_manual_dispatch_boundary()
_reserved = _runtime.create_snapshot()
_clock.advance_minutes(1)
_expect(_request().assigned_carrier_id == PORTER and _worker().current_cell.distance_to(Vector3i.ZERO) <= 1.0, "dispatcher selects the actual employee without teleporting")
var start := _worker().current_cell
_wait_for(LogisticsRequest.State.DELIVERING, 40)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _worker().current_cell != start, "porter walks to the source before pickup")
if _request().state != LogisticsRequest.State.DELIVERING:
_finish()
return
_expect(_quantity(_inventory(_source)) == 0 and _quantity(_cargo()) == 8 and _quantity(_inventory(_destination)) == 0, "pickup moves all eight units into owned cargo only")
_expect(_crosslinks(), "loaded Request, Task, reservations and cargo agree")
_loaded = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(_loaded), "mid-delivery Runtime snapshot restores")
_test_ownership_and_replay()
_test_capacity_wait()
_test_route_recovery()
_test_unavailable_and_expiry()
_test_restore_guards()
_test_pickup_retry()
_test_unavailable_before_pickup()
_test_partial_expiry_return()
_test_shift_boundary()
_test_late_delivery_failure()
_finish()
func _test_ownership_and_replay() -> void:
var owned := _owned_snapshot()
var manual: CommandResult = _runtime.submit_command(&"advance_logistics_request", {&"request_id": REQUEST})
_expect(not manual.is_success() and manual.details.get(&"reason") == &"SUPPLY_DRIVER_REQUIRED", "external advance cannot bypass real arrival")
_expect(_owned_snapshot() == owned, "rejected manual advance leaves inventory, Task and carrier ownership unchanged")
_expect(_runtime.restore_snapshot(_loaded), "rewind the rejected command audit before deterministic replay")
var task_id := _worker().current_task_id
_expect(not _runtime.staff_service.cancel_task(task_id).is_success(), "ordinary cancellation cannot orphan loaded cargo")
_expect(not _runtime.staff_service.remove_staff(PORTER).is_success(), "loaded porter cannot be removed")
_expect(not _runtime.staff_service.retire_staff(PORTER).is_success() and not _worker().is_retiring, "retirement rejects atomically while hauling")
_expect(not _runtime.staff_service.progress_task(task_id, 1, _clock.absolute_minute + 2).is_success(), "generic Task progress cannot bypass delivery")
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_request().state == LogisticsRequest.State.COMPLETED and _quantity(_inventory(_destination)) == 8, "arrival transfers the purchased stock into kitchen storage")
_expect(_quantity(_cargo()) == 0 and _worker().current_task_id.is_empty() and _worker().carried_inventory_id.is_empty(), "completion clears cargo and employee ownership together")
_expect(_runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "completion releases exact dispatcher weight")
var completed: Dictionary = _runtime.create_snapshot()
var end_minute: int = _clock.absolute_minute
_expect(_runtime.restore_snapshot(_loaded), "rewind to a loaded employee")
_clock.advance_minutes(end_minute - _clock.absolute_minute)
_expect(_runtime.create_snapshot() == completed, "loaded save replay produces identical inventory, Tasks, positions and accounts")
func _test_manual_dispatch_boundary() -> void:
var owned := _owned_snapshot()
var manual: CommandResult = _runtime.submit_command(&"dispatch_logistics", {&"carriers": {&"synthetic_porter": {&"capacity": 100, &"current_load": 0, &"path_minutes": 0, &"role": &"logistics"}}})
_expect(manual.is_success() and manual.details[&"assignments"].is_empty(), "external synthetic carrier cannot claim a managed supply request")
_expect(_owned_snapshot() == owned, "manual dispatch does not change reserved stock, Staff Task or request ownership")
func _owned_snapshot() -> Dictionary:
return {&"inventory": _runtime.inventory_service.create_snapshot(), &"staff": _runtime.staff_service.create_snapshot(), &"dispatcher": _runtime.logistics_dispatcher.create_snapshot(), &"request": _request().create_snapshot()}
func _test_capacity_wait() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore capacity scenario")
_runtime.inventory_service.add_stack(_inventory(_destination), SupplyCatalog.BASIC_MEAL, 40, 100, -1, _clock.absolute_minute)
_clock.advance_minutes(30)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _request().state_reason == "DESTINATION_FULL", "capacity becoming full after pickup pauses delivery")
_expect(_quantity(_cargo()) == 8 and _runtime.logistics_dispatcher.get_reserved_load(PORTER) == 8, "full destination retains all cargo and weight ownership")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "waiting loaded cargo is restorable")
_runtime.inventory_service.transfer_available(_inventory(_destination), &"test.capacity_sink", SupplyCatalog.BASIC_MEAL, 40, &"", _clock.absolute_minute)
_wait_for(LogisticsRequest.State.COMPLETED, 10)
_expect(_quantity(_inventory(_destination)) == 8 and _quantity(_cargo()) == 0, "freeing storage resumes once without losing or duplicating stock")
func _test_route_recovery() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore route scenario")
var target := _task().target_cell
_runtime.path_service.set_cell_temporary_block(target, true)
_clock.advance_minutes(40)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _quantity(_cargo()) == 8, "blocked delivery route never releases carried stock")
_expect(_worker().current_task_id == _task().task_id and _crosslinks(), "stuck recovery keeps employee and Task linked")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "route-blocked cargo saves and restores")
_runtime.path_service.set_cell_temporary_block(target, false)
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_request().state == LogisticsRequest.State.COMPLETED and _quantity(_inventory(_destination)) == 8, "restoring the real route resumes delivery")
func _test_unavailable_and_expiry() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore unavailable scenario")
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
var cell := _worker().current_cell
_clock.advance_minutes(3)
_expect(_worker().current_cell == cell and _quantity(_cargo()) == 8, "incapacitated porter does not move or lose cargo")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "incapacitated cargo owner is restorable")
_worker().health = 100
_worker().transition_to(StaffRuntime.State.MOVING_TO_TASK, &"TEST_RECOVERED", _clock.absolute_minute)
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_request().state == LogisticsRequest.State.COMPLETED, "employee recovery resumes the retained delivery")
_expect(_runtime.restore_snapshot(_loaded), "restore spoilage scenario")
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
var inventory_snapshot: Dictionary = _runtime.inventory_service.create_snapshot()
for owner: Dictionary in inventory_snapshot[&"inventories"]:
if owner[&"owner_inventory_id"] == String(_cargo()):
for stack: Dictionary in owner[&"stacks"]:
stack[&"expires_at"] = _clock.absolute_minute + 1
_expect(_runtime.inventory_service.restore_snapshot(inventory_snapshot), "inject a cargo expiry boundary")
_clock.advance_minutes(1)
_expect(_request().state == LogisticsRequest.State.CANCELLED and _quantity(_cargo()) == 0, "expired cargo enters inventory waste and closes the impossible delivery")
_expect(_worker().state == StaffRuntime.State.INCAPACITATED and _worker().current_task_id.is_empty(), "spoilage cleanup preserves incapacitation while releasing the Task")
_expect(_runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "spoilage releases weight without a ghost assignment")
func _test_restore_guards() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore crosslink scenario")
var stable: Dictionary = _runtime.create_snapshot()
var bad := stable.duplicate(true)
for task: Dictionary in bad[&"systems"][&"runtime"][&"staff"][&"tasks"]:
if task[&"task_type"] == String(SupplyLogisticsBridge.DELIVERY_TYPE):
task[&"target_cell"] = [99, 99, 1]
_expect(not _runtime.restore_snapshot(bad) and _runtime.create_snapshot() == stable, "restore rejects a remote delivery target atomically")
var worker := _worker()
worker.carried_inventory_id = &"cargo:somebody_else"
_expect(not _crosslinks(), "crosslink validator rejects stolen cargo ownership")
worker.carried_inventory_id = _cargo()
var task := _task()
var saved_type := task.task_type
task.task_type = SupplyLogisticsBridge.PICKUP_TYPE
_expect(not _crosslinks(), "crosslink validator rejects pickup Task paired with loaded request")
task.task_type = saved_type
_expect(_crosslinks(), "valid ownership survives rejected candidates")
func _test_pickup_retry() -> void:
_expect(_runtime.restore_snapshot(_reserved), "restore before pickup dispatch")
_clock.advance_minutes(1)
var old_task := _worker().current_task_id
var stopped: Dictionary = _runtime.supply_logistics.handle_path_stuck(old_task, _clock.absolute_minute)
_expect(stopped[&"ok"] and _request().state == LogisticsRequest.State.BLOCKED, "unloaded route failure blocks the request and cancels the approach")
_expect(_runtime.inventory_service.available_quantity(_inventory(_source), SupplyCatalog.RAW_FOOD) == 8 and _worker().current_task_id.is_empty(), "failed approach releases source reservation and employee together")
_expect(_runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "failed approach retains no dispatcher load")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "blocked unloaded request restores")
_wait_for(LogisticsRequest.State.DELIVERING, 40)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _worker().current_task_id != old_task, "retries reserve stock again and create a fresh attempt Task")
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_quantity(_inventory(_destination)) == 8 and _quantity(_cargo()) == 0, "retry delivers the same stock exactly once")
func _test_unavailable_before_pickup() -> void:
_expect(_runtime.restore_snapshot(_reserved), "restore unloaded health failure")
_clock.advance_minutes(1)
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
_clock.advance_minutes(1)
_expect(_request().state == LogisticsRequest.State.BLOCKED and _worker().current_task_id.is_empty(), "incapacitation before pickup releases approach ownership")
_expect(_worker().state == StaffRuntime.State.INCAPACITATED and _runtime.inventory_service.available_quantity(_inventory(_source), SupplyCatalog.RAW_FOOD) == 8, "unloaded health failure preserves health state and releases reserved food")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()) and _crosslinks(), "unloaded health failure saves without expired reservation links")
func _test_partial_expiry_return() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore partial-spoilage scenario")
var inventory: Dictionary = _runtime.inventory_service.create_snapshot()
for owner: Dictionary in inventory[&"inventories"]:
if owner[&"owner_inventory_id"] == String(_cargo()):
owner[&"stacks"][0][&"quantity"] = 4
_expect(_runtime.inventory_service.restore_snapshot(inventory), "split loaded cargo into two expiry lots")
_runtime.inventory_service.add_stack(_cargo(), SupplyCatalog.RAW_FOOD, 4, 100, _clock.absolute_minute + 1, _clock.absolute_minute)
_clock.advance_minutes(1)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _task().task_type == SupplyLogisticsBridge.RETURN_TYPE and _quantity(_cargo()) == 4, "partial expiry retargets the surviving cargo to its source")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "returning cargo saves its destination in the existing Task")
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
_clock.advance_minutes(1)
_expect(_crosslinks(), "unavailability reason cannot erase return destination")
_worker().health = 100
_worker().transition_to(StaffRuntime.State.MOVING_TO_TASK, &"RECOVERED", _clock.absolute_minute)
_wait_for(LogisticsRequest.State.CANCELLED, 40)
_expect(_request().state_reason == "CARGO_EXPIRED_RETURNED" and _quantity(_inventory(_source)) == 4 and _quantity(_inventory(_destination)) == 0, "return unloads only unspoiled stock at the original warehouse")
_expect(_quantity(_cargo()) == 0 and _runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "partial-expiry return releases all cargo ownership")
func _test_shift_boundary() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore end-of-shift scenario")
var target := _task().target_cell
_runtime.path_service.set_cell_temporary_block(target, true)
_clock.advance_minutes(1081 - _clock.absolute_minute)
_expect(_worker().is_overtime and not _worker().current_task_id.is_empty() and _quantity(_cargo()) == 8, "18:00 shift end retains loaded non-interruptible work")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "overtime delivery is restorable")
_runtime.path_service.set_cell_temporary_block(target, false)
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_clock.advance_minutes(1)
_expect(_worker().state == StaffRuntime.State.OFF_DUTY and _worker().current_task_id.is_empty() and _quantity(_inventory(_destination)) == 8, "employee ends the shift after safe unloading")
func _test_late_delivery_failure() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore delivery transaction failure")
var fault := LateFailInventory.new()
_expect(fault.restore_snapshot(_runtime.inventory_service.create_snapshot()), "install failure after inventory transfer has already mutated")
_runtime.inventory_service = fault
_clock.advance_minutes(30)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _request().state_reason == "DELIVERY_RETRY", "late inventory failure rolls the request back to owned delivery")
_expect(_quantity(_cargo()) == 8 and _quantity(_inventory(_destination)) == 0 and _runtime.logistics_dispatcher.get_reserved_load(PORTER) == 8, "failed delivery reverses all physical transfer and dispatcher changes")
_expect(_crosslinks() and not _worker().current_task_id.is_empty(), "failed delivery preserves the employee and Task pair")
fault.fail_delivery = false
_wait_for(LogisticsRequest.State.COMPLETED, 10)
_expect(_request().state == LogisticsRequest.State.COMPLETED and _quantity(_inventory(_destination)) == 8 and _quantity(_cargo()) == 0, "clearing transient failure retries the same operation exactly once")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "transaction retry leaves a restorable Runtime")
func _wait_for(state: int, minutes: int) -> void:
for index: int in minutes:
if _request().state == state: return
_clock.advance_minutes(1)
func _command(type: StringName, payload: Dictionary) -> bool:
var result: CommandResult = _runtime.submit_command(type, payload)
if not result.is_success(): print(result.message, result.details)
return result.is_success()
func _inventory(id: StringName) -> StringName:
return _runtime.facility_service.get_instance(id).local_inventory_id
func _request() -> LogisticsRequest:
return _runtime.logistics_requests[REQUEST]
func _worker() -> StaffRuntime:
return _runtime.staff_service.get_staff(PORTER)
func _task() -> StaffTask:
return _runtime.staff_service.get_task(_worker().current_task_id)
func _cargo() -> StringName:
return StringName("cargo:%s" % PORTER)
func _quantity(inventory_id: StringName) -> int:
return _runtime.inventory_service.total_quantity(inventory_id, SupplyCatalog.RAW_FOOD)
func _crosslinks() -> bool:
return SupplyLogisticsBridge.validate_crosslinks(_runtime.logistics_requests, _runtime.staff_service, _runtime.inventory_service, _runtime.facility_service)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _finish() -> void:
print("%s: %d checks / %d failures" % ["PASS" if _failures == 0 else "FAIL", _checks, _failures])
quit(_failures)
@@ -0,0 +1 @@
uid://dbj5dk0uqj0ay
@@ -0,0 +1,337 @@
extends SceneTree
## Runtime-level purchasing and replenishment. All physical stock is purchased
## and cooked normally; failure fixtures cover payment, closed routes and expiry.
class RejectingPaymentEconomy:
extends EconomyService
func transfer(_from: StringName, _to: StringName, _amount: int,
_minute: int = -1, _category: StringName = DEFAULT_CATEGORY,
_source: StringName = &"", _key: StringName = &"", _memo: StringName = &"",
_related: StringName = &"", _notify := true) -> CommandResult:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Injected payment refusal")
const PORTER := &"staff_gina"
const COOK := &"staff_chika"
const RETURN_REQUEST := &"supply.management_return"
var _checks := 0
var _failures := 0
var _completed_groups := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _ids: Dictionary = {}
var _built: Dictionary = {}
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_expect(_runtime.initialize(_clock, _state, 553221)[&"valid"], "Runtime initializes")
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [
[&"warehouse", Vector3i(-8, 0, 1)], [ &"kitchen", Vector3i(2, 0, 1)],
[&"cafeteria", Vector3i(2, 0, 0)], [&"staff_door", Vector3i(0, 0, 1)],
]:
_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]})
_clock.advance_minutes(180)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
_ids[facility.definition_id] = facility.instance_id
var completed := 0
for job: ConstructionJob in _runtime.construction_service.get_jobs():
if job.state == ConstructionJob.State.COMPLETED:
completed += 1
_expect(_ids.has(&"warehouse") and _ids.has(&"kitchen") and _ids.has(&"cafeteria") and completed == 4,
"normal starting budget completes three supply facilities and the staff door")
if not _ids.has(&"warehouse") or not _ids.has(&"kitchen") or not _ids.has(&"cafeteria"):
_finish()
return
_built = _runtime.create_snapshot()
_test_paid_purchase_and_rejection()
_test_disabled_kitchen_still_delivers()
_test_multiple_sources_choose_connected_nearest()
_test_returning_cargo_reserves_actual_space()
_expect(_completed_groups == 4, "all four regression scenarios reach their final assertions")
_finish()
func _test_paid_purchase_and_rejection() -> void:
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var quote: Dictionary = _runtime.preview_supply_purchase(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, 8)
_expect(bool(quote[&"ok"]), "unassigned completed warehouse accepts a paid purchase")
var result := _purchase(8)
_expect(result.is_success() and _quantity(&"warehouse", SupplyCatalog.RAW_FOOD) == 8,
"successful purchase creates exactly eight physical units")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash - int(quote[&"total_cost"])
and _state.get_resource(&"gold") == cash - int(quote[&"total_cost"]),
"ledger and player cash both reflect the exact purchase price")
var entries: Array = _runtime.economy_service.get_transaction_entries(StringName(result.details.get(&"transaction_id", &"")))
_expect(entries.size() == 1 and entries[0].amount == int(quote[&"total_cost"])
and entries[0].source_entity_id == _ids[&"warehouse"], "purchase records one expense attributed to its warehouse")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 0}, &"INVALID_QUANTITY")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 100}, &"STORAGE_FULL")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1000}, &"INSUFFICIENT_FUNDS")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.BASIC_MEAL, &"quantity": 1}, &"ITEM_NOT_PURCHASABLE")
_assert_purchase_rejected({&"warehouse_id": _ids[&"kitchen"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"NOT_WAREHOUSE")
_runtime.facility_service.set_operational_status(_ids[&"warehouse"], {&"operator_open": false}, _clock.absolute_minute)
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"FACILITY_CLOSED")
_runtime.facility_service.set_operational_status(_ids[&"warehouse"], {&"operator_open": true}, _clock.absolute_minute)
# Exercise the rollback after stock has been inserted, rather than only
# testing quote failures that reject before the first inventory write.
var original_economy: EconomyService = _runtime.economy_service
var rejecting := RejectingPaymentEconomy.new()
_expect(rejecting.restore_snapshot(original_economy.create_snapshot()), "payment-failure fixture retains all real balances")
_runtime.economy_service = rejecting
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"PAYMENT_FAILED")
_runtime.economy_service = original_economy
_completed_groups += 1
func _test_disabled_kitchen_still_delivers() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for production")
_hire_and_assign(PORTER, &"warehouse")
_hire_and_assign(COOK, &"kitchen")
_expect(_purchase(8).is_success(), "purchase ingredients for real production")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 4})
for minute: int in 100:
if _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) >= 4:
break
_clock.advance_minutes(1)
_expect(_quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) == 4, "paid food is transported and cooked into one real batch")
if _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) != 4:
return
_expect(_meal_requests().is_empty(), "production finishes before the following logistics tick creates a meal request")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": false, &"target_stock": 4})
# Preserve the legacy compatibility boundary: a kitchen with no explicit
# production plan must not suddenly become an automatic source.
_expect(_runtime.kitchen_production.remove_plan(_ids[&"kitchen"])[&"ok"], "completed disabled kitchen can model a legacy unconfigured source")
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_expect(_meal_requests().is_empty(), "unconfigured kitchen stock does not trigger unsolicited automatic delivery")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": false, &"target_stock": 4})
_clock.advance_minutes(1)
_expect(_meal_requests().size() == 1 and not _runtime.kitchen_production.get_plan(_ids[&"kitchen"])[&"enabled"],
"an explicitly disabled production plan still dispatches its completed meals")
_expect(_quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 0
and _runtime.inventory_service.available_quantity(_inventory(&"kitchen"), SupplyCatalog.BASIC_MEAL) == 0,
"meal request reserves stock without instantly moving it to the dining room")
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_expect(_meal_requests().size() == 1, "repeated replenishment cannot duplicate the already reserved meal shipment")
for minute: int in 100:
if _quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 4:
break
_clock.advance_minutes(1)
_expect(_quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 4
and _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) == 0,
"real porter completes the outgoing shipment while kitchen production remains off")
_completed_groups += 1
func _test_multiple_sources_choose_connected_nearest() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for competing suppliers")
_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(6, 0, 1)})
_clock.advance_minutes(180)
var near_id: StringName = &""
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse" and facility.instance_id != _ids[&"warehouse"]:
near_id = facility.instance_id
_expect(not near_id.is_empty(), "starting budget also completes a second, closer warehouse")
if near_id.is_empty():
return
_expect(_purchase(8).is_success(), "older distant supplier holds eight paid units")
_command(&"purchase_supplies", {&"warehouse_id": near_id, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8})
var near: FacilityInstance = _runtime.facility_service.get_instance(near_id)
var kitchen: FacilityInstance = _runtime.facility_service.get_instance(_ids[&"kitchen"])
var near_route: Dictionary = _runtime.supply_logistics.find_facility_access_path(Vector3i(6, 0, 1), kitchen)
var far_route: Dictionary = _runtime.supply_logistics.find_facility_access_path(Vector3i(-5, 0, 1), kitchen)
_expect(bool(near_route[&"reachable"]) and bool(far_route[&"reachable"])
and int(near_route[&"steps"]) < int(far_route[&"steps"])
and String(near_id) > String(_ids[&"warehouse"]),
"both actual routes connect, and the shorter route belongs to the later warehouse ID")
var stocked: Dictionary = _runtime.create_snapshot()
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
_clock.advance_minutes(1)
var requests := _raw_requests_for_kitchen()
_expect(requests.size() == 1 and int(requests[0].item_requirements[SupplyCatalog.RAW_FOOD]) == 4,
"two stocked suppliers create only one four-unit shipment in the first logistics tick")
_expect(not requests.is_empty() and requests[0].source_inventory_id == near.local_inventory_id,
"connected nearby supplier wins over the older, farther warehouse")
_expect(_runtime.inventory_service.available_quantity(near.local_inventory_id, SupplyCatalog.RAW_FOOD) == 4
and _runtime.inventory_service.available_quantity(_inventory(&"warehouse"), SupplyCatalog.RAW_FOOD) == 8
and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 4,
"exactly four units are reserved once and counted once as incoming")
_clock.advance_minutes(1)
requests = _raw_requests_for_kitchen()
_expect(requests.size() == 2 and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 8,
"next tick adds at most one batch to cover the remaining four-unit deficit")
_clock.advance_minutes(1)
_expect(_raw_requests_for_kitchen().size() == 2
and _runtime.inventory_service.available_quantity(_inventory(&"warehouse"), SupplyCatalog.RAW_FOOD) == 8,
"incoming stock at target stops extra requests and leaves other supplier stock untouched")
# Geometric proximity must not beat an actual connection. Closing every
# access cell of the near warehouse leaves the far route into the kitchen.
_expect(_runtime.restore_snapshot(stocked), "rewind to the two unreserved suppliers")
near = _runtime.facility_service.get_instance(near_id)
kitchen = _runtime.facility_service.get_instance(_ids[&"kitchen"])
for cell: Vector3i in near.occupied_cells:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
if _runtime.path_service.has_cell(cell + delta):
_runtime.path_service.set_cell_temporary_block(cell + delta, true)
far_route = _runtime.supply_logistics.find_facility_access_path(Vector3i(-5, 0, 1), kitchen)
near_route = _runtime.supply_logistics.find_facility_access_path(Vector3i(6, 0, 1), kitchen)
_expect(bool(far_route[&"reachable"]) and not bool(near_route[&"reachable"]),
"route closure really disconnects the nearby supplier while preserving the far route")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
_clock.advance_minutes(1)
requests = _raw_requests_for_kitchen()
_expect(requests.size() == 1 and requests[0].source_inventory_id == _inventory(&"warehouse")
and int(requests[0].item_requirements[SupplyCatalog.RAW_FOOD]) == 4,
"unreachable near stock cannot steal the single shipment from a connected supplier")
_completed_groups += 1
func _test_returning_cargo_reserves_actual_space() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for returning cargo")
_hire_and_assign(PORTER, &"warehouse")
_expect(_purchase(2).is_success() and _purchase(6).is_success(), "two paid stacks provide an eight-unit shipment")
_command(&"create_logistics_request", {
&"request_id": RETURN_REQUEST, &"source_inventory_id": _inventory(&"warehouse"),
&"destination_inventory_id": _inventory(&"kitchen"), &"item_requirements": {SupplyCatalog.RAW_FOOD: 8},
})
var request: LogisticsRequest = _runtime.logistics_requests[RETURN_REQUEST]
for minute: int in 60:
if request.state == LogisticsRequest.State.DELIVERING:
break
_clock.advance_minutes(1)
var cargo_id := StringName("cargo:%s" % PORTER)
_expect(request.state == LogisticsRequest.State.DELIVERING
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 8,
"actual pickup puts purchased goods in the employee's cargo")
if request.state != LogisticsRequest.State.DELIVERING:
return
var inventory_snapshot: Dictionary = _runtime.inventory_service.create_snapshot()
var injected := false
for inventory: Dictionary in inventory_snapshot[&"inventories"]:
if inventory[&"owner_inventory_id"] != String(cargo_id):
continue
for stack: Dictionary in inventory[&"stacks"]:
if int(stack[&"quantity"]) == 2:
stack[&"expires_at"] = _clock.absolute_minute + 1
injected = true
_expect(injected, "the original two-unit paid stack remains identifiable inside real cargo")
_expect(_runtime.inventory_service.restore_snapshot(inventory_snapshot), "inject expiry of only the two-unit paid stack")
_clock.advance_minutes(1)
_expect(request.state == LogisticsRequest.State.DELIVERING and request.state_reason == "RETURNING_EXPIRED_CARGO"
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 6,
"partial expiry starts a real return of the six surviving units")
if request.state != LogisticsRequest.State.DELIVERING or request.state_reason != "RETURNING_EXPIRED_CARGO":
return
for reason: String in ["RETURNING_EXPIRED_CARGO", "EXPIRED_CARGO_RETURN_BLOCKED", "CARRIER_UNAVAILABLE"]:
# Both bridge states retain the same real cargo. Capacity accounting
# must follow that cargo even if the return route becomes blocked.
request.state_reason = reason
var quote: Dictionary = _runtime.preview_supply_purchase(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, 94)
_expect(bool(quote[&"ok"]) and int(quote[&"max_quantity"]) == 94,
"%s reserves six actual returning units, not the original eight" % reason)
_expect(_incoming(&"warehouse", SupplyCatalog.RAW_FOOD) == 6 and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 0,
"%s counts returning goods only at the true receiving warehouse" % reason)
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 95}, &"STORAGE_FULL")
request.state_reason = "RETURNING_EXPIRED_CARGO"
_expect(_purchase(94).is_success(), "buyer may use the exact free space left after reserving the return")
for minute: int in 30:
if request.is_terminal():
break
_clock.advance_minutes(1)
_expect(request.state == LogisticsRequest.State.CANCELLED and request.state_reason == "CARGO_EXPIRED_RETURNED",
"capacity-aware purchase leaves enough room for the real return to complete")
_expect(_quantity(&"warehouse", SupplyCatalog.RAW_FOOD) == 100
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 0
and _incoming(&"warehouse", SupplyCatalog.RAW_FOOD) == 0,
"return fills warehouse exactly once and clears incoming cargo")
_expect(_runtime.inventory_service.validate_invariants()[&"ok"]
and SupplyLogisticsBridge.validate_crosslinks(_runtime.logistics_requests, _runtime.staff_service, _runtime.inventory_service, _runtime.facility_service),
"completed return preserves inventory and carrier ownership invariants")
_completed_groups += 1
func _assert_purchase_rejected(payload: Dictionary, reason: StringName) -> void:
var inventory_before: Dictionary = _runtime.inventory_service.create_snapshot()
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
var gold_before: int = _state.get_resource(&"gold")
var result: CommandResult = _runtime.submit_command(&"purchase_supplies", payload)
_expect(not result.is_success() and result.details.get(&"reason") == reason, "purchase rejects %s" % reason)
_expect(_runtime.inventory_service.create_snapshot() == inventory_before
and _runtime.economy_service.create_snapshot() == economy_before and _state.get_resource(&"gold") == gold_before,
"%s leaves full inventory, ledger and player cash unchanged" % reason)
func _command(type: StringName, payload: Dictionary) -> CommandResult:
var result: CommandResult = _runtime.submit_command(type, payload)
_expect(result.is_success(), "%s succeeds: %s" % [type, result.details])
return result
func _purchase(quantity: int) -> CommandResult:
return _runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": quantity})
func _hire_and_assign(staff_id: StringName, facility: StringName) -> void:
_command(&"hire_staff_candidate", {&"candidate_id": staff_id})
_command(&"set_staff_work_assignment", {&"staff_id": staff_id, &"facility_id": _ids[facility], &"template_id": &"DAY"})
func _inventory(definition: StringName) -> StringName:
return _runtime.facility_service.get_instance(_ids[definition]).local_inventory_id
func _quantity(definition: StringName, item: StringName) -> int:
return _runtime.inventory_service.total_quantity(_inventory(definition), item)
func _incoming(definition: StringName, item: StringName) -> int:
var report: Dictionary = _runtime.get_supply_management_report()
var group := &"warehouses" if definition == &"warehouse" else &"kitchens"
for row: Dictionary in report[group]:
if StringName(row[&"facility_id"]) != _ids[definition]:
continue
for stock: Dictionary in row[&"stock"]:
if StringName(stock[&"item_id"]) == item:
return int(stock[&"in_transit"])
return -1
func _meal_requests() -> Array[LogisticsRequest]:
var requests: Array[LogisticsRequest] = []
for request: LogisticsRequest in _runtime.logistics_requests.values():
if request.item_requirements.has(SupplyCatalog.BASIC_MEAL):
requests.append(request)
return requests
func _raw_requests_for_kitchen() -> Array[LogisticsRequest]:
var requests: Array[LogisticsRequest] = []
for request: LogisticsRequest in _runtime.logistics_requests.values():
if request.destination_inventory_id == _inventory(&"kitchen") and request.item_requirements.has(SupplyCatalog.RAW_FOOD):
requests.append(request)
return requests
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _finish() -> void:
print("Supply management service: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
@@ -0,0 +1 @@
uid://dmfdmeee0ev61
@@ -0,0 +1,92 @@
extends SceneTree
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P1: Runtime v7 supply save migration")
var source := _fixture(7301)
var first := _fixture(7302)
var second := _fixture(7303)
var source_runtime: Variant = source[&"runtime"]
var first_runtime: Variant = first[&"runtime"]
var second_runtime: Variant = second[&"runtime"]
source[&"clock"].advance_minutes(3)
var canonical: Dictionary = source_runtime.create_snapshot()
var canonical_data: Dictionary = canonical[&"systems"][&"runtime"]
_expect(canonical_data[&"runtime_schema_version"] == 7, "current Runtime emits schema v7")
_expect(canonical_data[&"kitchen_production"][&"current_minute"] == source[&"clock"].absolute_minute, "production clock follows Runtime clock without existing kitchens")
var legacy := canonical.duplicate(true)
var legacy_data: Dictionary = legacy[&"systems"][&"runtime"]
legacy_data[&"runtime_schema_version"] = 6
legacy_data.erase(&"kitchen_production")
_expect(legacy_data.has(&"facility_infrastructure") and legacy_data.has(&"route_projection"), "v6 fixture retains infrastructure introduced in v6")
_expect(first_runtime.restore_snapshot(legacy), "genuine v6 state restores into v7")
var migrated: Dictionary = first_runtime.create_snapshot()
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 7, "v6 migration re-emits current schema")
var kitchen: Dictionary = migrated[&"systems"][&"runtime"][&"kitchen_production"]
_expect(kitchen[&"plans"].is_empty() and kitchen[&"active_batches"].is_empty(), "legacy migration does not infer kitchen production plans or batches")
_expect(kitchen[&"current_minute"] == source[&"clock"].absolute_minute, "legacy production clock binds to the restored Runtime minute")
_expect(migrated == canonical, "v6 migration preserves non-supply state exactly")
_expect(second_runtime.restore_snapshot(legacy) and second_runtime.create_snapshot() == migrated, "v6 migration is deterministic across Runtime instances")
_expect(first_runtime.restore_snapshot(migrated) and first_runtime.create_snapshot() == migrated, "canonical v7 round trip is exact")
var missing_supply := canonical.duplicate(true)
missing_supply[&"systems"][&"runtime"].erase(&"kitchen_production")
_expect(_rejects_atomically(first_runtime, missing_supply), "v7 cannot omit its supply aggregate")
var relabelled := canonical.duplicate(true)
relabelled[&"systems"][&"runtime"][&"runtime_schema_version"] = 6
_expect(_rejects_atomically(first_runtime, relabelled), "relabelled v7 with retained kitchen state cannot masquerade as v6")
var bad_clock := canonical.duplicate(true)
bad_clock[&"systems"][&"runtime"][&"kitchen_production"][&"current_minute"] -= 1
_expect(_rejects_atomically(first_runtime, bad_clock), "production and Runtime clock disagreement is rejected")
var bad_version := canonical.duplicate(true)
bad_version[&"systems"][&"runtime"][&"kitchen_production"][&"version"] = 2
_expect(_rejects_atomically(first_runtime, bad_version), "unknown nested production schema is rejected")
var stock: Dictionary = source_runtime.inventory_service.add_stack(&"supplier.legacy", SupplyCatalog.RAW_FOOD, 4)
_expect(stock[&"ok"], "orphan migration fixture has raw food")
var hold: Dictionary = source_runtime.inventory_service.reserve_item(
&"supplier.legacy", SupplyCatalog.RAW_FOOD, 4,
&"kitchen.batch:00000001", SupplyCatalog.PRODUCTION_PURPOSE, source[&"clock"].absolute_minute
)
_expect(hold[&"ok"], "orphan fixture creates a production reservation")
var orphaned: Dictionary = source_runtime.create_snapshot()
orphaned[&"systems"][&"runtime"][&"runtime_schema_version"] = 6
orphaned[&"systems"][&"runtime"].erase(&"kitchen_production")
_expect(_rejects_atomically(first_runtime, orphaned), "legacy label cannot hide input holds for a discarded production batch")
for fixture: Dictionary in [source, first, second]:
fixture[&"runtime"].free()
fixture[&"clock"].free()
fixture[&"state"].free()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _fixture(seed: int) -> Dictionary:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
_expect(runtime.initialize(clock, state, seed)[&"valid"], "migration fixture initializes")
clock.set_paused(true)
return {&"clock": clock, &"state": state, &"runtime": runtime}
func _rejects_atomically(runtime: Variant, candidate: Dictionary) -> bool:
var before: Dictionary = runtime.create_snapshot()
return not runtime.restore_snapshot(candidate) and runtime.create_snapshot() == before
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
@@ -0,0 +1 @@
uid://t3p5aqdc4g5h