Add supply auto-purchase and preserve save recovery state
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
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_auto_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_auto_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_stairs", &"origin": Vector3i(-4, 1, 0)})
|
||||
await _advance(60)
|
||||
_command(&"build_facility", {&"definition_id": &"guest_stairs", &"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")
|
||||
_main.call("_activate_management_mode", &"product")
|
||||
var auto_panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
|
||||
_expect(auto_panel != null, "normal product UI exposes automatic paid ordering")
|
||||
if auto_panel == null: _finish(); return
|
||||
auto_panel.auto_purchase_enabled.button_pressed = true
|
||||
auto_panel.auto_purchase_target.value = 20 # Explicit player-input fixture; not a default balance value.
|
||||
auto_panel.auto_purchase_button.pressed.emit()
|
||||
_expect(_runtime.auto_purchase_plans.get_plan(warehouse.instance_id, SupplyCatalog.RAW_FOOD)[&"enabled"], "normal product controls arm player-selected target20")
|
||||
_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)
|
||||
_expect(int(summaries[-1].get(&"arrears_total", -1)) == 0, "day %d pays all real wages without arrears" % day)
|
||||
_check_auto_financial_close(summaries[-1])
|
||||
_expect(_auto_order_count() >= 2, "at least two real shortages cause paid automatic orders without another manual restock")
|
||||
_expect(_sales(&"standard_room") > 0, "the same inn earns lodging revenue")
|
||||
_expect(_sales(&"reception") == 0, "three days do not depend on artificial reception 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(), "upkeep": _runtime.upkeep_settlement.get_report(), "maintenance_expense": _runtime.economy_service.get_balance(FacilityUpkeepSettlement.EXPENSE)}
|
||||
var evidence_dir := "res://artifacts/supply-auto-chain" if OS.get_environment("THE_TREE_BUSINESS_ACCEPTANCE") == "1" else "res://artifacts/supply-auto-chain"
|
||||
if not OS.get_environment("THE_TREE_SUPPLY_EVIDENCE_DIR").is_empty():
|
||||
evidence_dir = OS.get_environment("THE_TREE_SUPPLY_EVIDENCE_DIR")
|
||||
var evidence_path := ProjectSettings.globalize_path(evidence_dir.path_join("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_AUTO_PURCHASE_REPORT ", JSON.stringify({"cash": evidence["cash"], "meal_sales": evidence["meal_sales"], "raw_deliveries": evidence["raw_deliveries"], "meal_deliveries": evidence["meal_deliveries"]}))
|
||||
_finish()
|
||||
|
||||
|
||||
func _auto_order_count() -> int:
|
||||
var count := 0
|
||||
for row: Dictionary in _runtime.economy_service.create_snapshot()[&"idempotency"]:
|
||||
if String(row[&"key"]).begins_with("supply.auto."): count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _check_auto_financial_close(summary: Dictionary) -> void:
|
||||
var day := int(summary[&"day"])
|
||||
var start: int = _runtime.operations_coordinator.get_financial_period_start(day)
|
||||
var end := int(summary[&"closed_at"])
|
||||
var purchase_expense := 0
|
||||
var signing_expense := 0
|
||||
var auto_expense := 0
|
||||
var cash_in := 0
|
||||
var cash_out := 0
|
||||
for entry: LedgerEntry in _runtime.economy_service.get_entries():
|
||||
if entry.category != &"INITIALIZATION":
|
||||
if entry.debit_account == &"PLAYER_CASH": cash_in += entry.amount
|
||||
if entry.credit_account == &"PLAYER_CASH": cash_out += entry.amount
|
||||
if entry.absolute_minute < start or entry.absolute_minute >= end: continue
|
||||
if entry.category == &"STAFF_SIGNING_COST": signing_expense += entry.amount
|
||||
if entry.category == &"OPERATING_EXPENSE":
|
||||
purchase_expense += entry.amount
|
||||
if entry.source_entity_id == _ids[&"warehouse"] and _runtime.economy_service.get_transaction_id_for_idempotency_key(
|
||||
SupplyAutoPurchasePlans.order_key(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, entry.absolute_minute)) == entry.transaction_id:
|
||||
auto_expense += entry.amount
|
||||
var receipt: Dictionary = SupplyAutoPurchasePlans.receipt_from_ledger(_runtime.economy_service, _ids[&"warehouse"], SupplyCatalog.RAW_FOOD, entry.absolute_minute, entry.transaction_id)
|
||||
_expect(not receipt.is_empty() and receipt[&"quantity"] * 3 == entry.amount and receipt[&"unit_price"] == 3 and receipt[&"total_cost"] == entry.amount, "actual automatic receipt retains current normal3G quote and unchanged cost/source/category")
|
||||
_expect(summary[&"category_totals"][&"cost_of_goods"] == purchase_expense + signing_expense, "day%d actual purchase+contract cost exactly reconciles daily cost_of_goods" % day)
|
||||
_expect(int(summary[&"category_totals"][&"cost_of_goods"]) >= auto_expense, "automatic payment is included in operating expense once")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 5000 + cash_in - cash_out, "real initial cash+all ledger flows exactly equals player cash")
|
||||
var warehouse_row: Dictionary = summary[&"facility_profit_loss"].get(_ids[&"warehouse"], {})
|
||||
_expect(not warehouse_row.is_empty() and int(warehouse_row.get(&"expenses", 0)) >= auto_expense, "actual warehouse P&L retains automatic expense attribution")
|
||||
|
||||
|
||||
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-auto-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 auto purchase 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 · 仕入れと厨房の試遊(保存なし)"
|
||||
var load_menu := _main.find_child("SaveLoadMenu", true, false) as MenuButton
|
||||
if load_menu != null:
|
||||
load_menu.disabled = true
|
||||
load_menu.tooltip_text = "この試遊は保存・読込を行いません。"
|
||||
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 = "この試遊は保存しません。"
|
||||
if OS.get_environment("THE_TREE_BUSINESS_PREVIEW") == "1":
|
||||
root.title = "The Tree · 料金と営業の試遊(保存なし)"
|
||||
_main.call("_activate_management_mode", &"business")
|
||||
var business := _main.find_child("BusinessManagementPanel", true, false)
|
||||
business.call("select_facility", _ids[&"cafeteria"])
|
||||
_main.call("_set_status", "料金と営業状態を変更できます。Spaceで再開すると、客の行動に反映されます(保存なし)。", true, true)
|
||||
else:
|
||||
_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://b35pphufmkii8
|
||||
@@ -0,0 +1,365 @@
|
||||
extends SceneTree
|
||||
|
||||
## Real command/ledger boundaries; failure injections are disclosed separately.
|
||||
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, "Payment refusal fixture")
|
||||
|
||||
class RejectingNotificationEconomy:
|
||||
extends EconomyService
|
||||
func commit_transaction_notification(_transaction: StringName) -> bool:
|
||||
return false
|
||||
|
||||
class ProbedInventory:
|
||||
extends InventoryService
|
||||
var transaction_boundary: Dictionary = {}
|
||||
func add_stack(owner: StringName, item: StringName, quantity: int,
|
||||
quality: int = DEFAULT_QUALITY, expiry: int = -1, created: int = -1,
|
||||
weight: int = DEFAULT_UNIT_WEIGHT, stack_id: StringName = &"") -> Dictionary:
|
||||
transaction_boundary = create_snapshot()
|
||||
return super.add_stack(owner, item, quantity, quality, expiry, created, weight, stack_id)
|
||||
|
||||
class PartialFailInventory:
|
||||
extends ProbedInventory
|
||||
func add_stack(owner: StringName, item: StringName, quantity: int,
|
||||
quality: int = DEFAULT_QUALITY, expiry: int = -1, created: int = -1,
|
||||
weight: int = DEFAULT_UNIT_WEIGHT, stack_id: StringName = &"") -> Dictionary:
|
||||
super.add_stack(owner, item, quantity, quality, expiry, created, weight, stack_id)
|
||||
return {&"ok": false, &"code": &"INJECTED_AFTER_STOCK"}
|
||||
|
||||
var _runtime: Variant
|
||||
var _clock: Variant
|
||||
var _state: Variant
|
||||
var _warehouse: StringName
|
||||
var _built: Dictionary
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
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.set_process(false)
|
||||
_clock.reset(1, 6, 0)
|
||||
_state.reset()
|
||||
_expect(_runtime.initialize(_clock, _state, 420_501)[&"valid"], "canonical Runtime initializes")
|
||||
_clock.set_paused(true)
|
||||
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
_expect(_runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "ordinary paid warehouse build")
|
||||
_clock.advance_minutes(180)
|
||||
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
if facility.definition_id == &"warehouse": _warehouse = facility.instance_id
|
||||
_expect(not _warehouse.is_empty(), "ordinary construction yields a warehouse")
|
||||
if _warehouse.is_empty(): _finish(); return
|
||||
_built = _runtime.create_snapshot()
|
||||
_test_receipt_codec()
|
||||
_test_default_manual_and_paid_order()
|
||||
_test_invalid_input_and_corrupt_save()
|
||||
_test_atomic_failures_and_same_minute_retry()
|
||||
_test_hold_disable_and_removal()
|
||||
_test_actual_budget_power_reservation_and_terminal()
|
||||
_test_multiple_warehouse_order()
|
||||
_test_legacy13_and_composed_rollback()
|
||||
_finish()
|
||||
|
||||
func _plan(enabled: bool, target: int, key: StringName = &"") -> CommandResult:
|
||||
return _runtime.submit_command(&"set_supply_auto_purchase", {&"warehouse_id": _warehouse,
|
||||
&"item_id": SupplyCatalog.RAW_FOOD, &"enabled": enabled, &"target_stock": target}, &"player", _warehouse, 75, -1, key)
|
||||
|
||||
func _stock() -> int:
|
||||
var facility: FacilityInstance = _runtime.facility_service.get_instance(_warehouse)
|
||||
return _runtime.inventory_service.total_quantity(facility.local_inventory_id, SupplyCatalog.RAW_FOOD)
|
||||
|
||||
func _test_receipt_codec() -> void:
|
||||
var memo := SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.RAW_FOOD, 6, 3, 18)
|
||||
var decoded := SupplyAutoPurchasePlans.decode_receipt_memo(memo)
|
||||
_expect(decoded == {&"item_id": String(SupplyCatalog.RAW_FOOD), &"quantity": 6, &"unit_price": 3, &"total_cost": 18}, "canonical versioned actual quote codec is lossless")
|
||||
decoded[&"quantity"] = 3
|
||||
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(memo)[&"quantity"] == 6, "decoded receipt is independently owned")
|
||||
var punctuation: StringName = &"item:part.日本語"
|
||||
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(SupplyAutoPurchasePlans.receipt_memo(punctuation, 6, 3, 18))[&"item_id"] == String(punctuation), "length encoding handles punctuation and Unicode without ambiguity")
|
||||
var prefix: String = SupplyAutoPurchasePlans.RECEIPT_PREFIX + "19:ingredient/raw_food:"
|
||||
for malformed: String in ["", "supply_purchase", prefix + "3:6:18:extra", prefix + "6:03:18", prefix + "6:+3:18", prefix + "6:3.0:18", prefix + "6:3e0:18", prefix + "6:-3:18", prefix + "6:0:0", prefix + "6:3:019", prefix + "6:3:19", prefix + "10001:3:30003", prefix + "1:9223372036854775808:9223372036854775808", prefix + "2:9223372036854775807:9223372036854775807", "supply_purchase.auto.v2:19:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:019:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:18:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:9223372036854775808:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:1: :6:3:18"]:
|
||||
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(StringName(malformed)).is_empty(), "malformed/noncanonical/overflow quote memo is rejected: " + malformed)
|
||||
var max_memo: StringName = &"supply_purchase.auto.v1:1:x:1:9223372036854775807:9223372036854775807"
|
||||
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(max_memo).get(&"unit_price", 0) == 9223372036854775807, "signed64 canonical maximum parses without floating conversion or overflow")
|
||||
|
||||
func _test_default_manual_and_paid_order() -> void:
|
||||
_expect(_runtime.auto_purchase_plans.get_plans().is_empty(), "new game has no inferred automatic intent")
|
||||
var before: Dictionary = _runtime.create_snapshot()
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 0 and _runtime.economy_service.create_snapshot() == before[&"systems"][&"runtime"][&"economy"], "default disabled tick spends and stocks nothing")
|
||||
var manual: CommandResult = _runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 4})
|
||||
_expect(manual.is_success() and int(manual.details.get(&"total_cost", -1)) == 12 and _stock() == 4, "manual contract still charges4 food exactly12G")
|
||||
var transaction_id := StringName(manual.details.get(&"transaction_id", &""))
|
||||
var entries: Array = _runtime.economy_service.get_transaction_entries(transaction_id)
|
||||
_expect(entries.size() == 1 and entries[0].category == &"OPERATING_EXPENSE" and entries[0].memo_key == &"supply_purchase", "manual category/memo remain unchanged")
|
||||
_expect(_runtime.economy_service.get_transaction_id_for_idempotency_key(StringName("supply.purchase.%d" % manual.command_id)) == transaction_id, "manual ledger key still uses accepted commandID")
|
||||
var configured: CommandResult = _plan(true, 10, &"auto.test.intent")
|
||||
_expect(configured.is_success() and _stock() == 4, "player intent does not immediately stock or pay")
|
||||
var armed: Dictionary = _runtime.create_snapshot()
|
||||
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
|
||||
var notifications: Array[Dictionary] = []
|
||||
_runtime.economy_service.transaction_posted.connect(func(id: StringName, ledger_rows: Array) -> void:
|
||||
var rows: Array[Dictionary] = []
|
||||
for entry: LedgerEntry in ledger_rows: rows.append(entry.to_dictionary())
|
||||
notifications.append({&"transaction_id": id, &"entries": rows}))
|
||||
_clock.advance_minutes(1)
|
||||
var paid: Dictionary = _runtime.create_snapshot()
|
||||
var row: Dictionary = _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)
|
||||
_expect(_stock() == 10 and _runtime.economy_service.get_balance(&"PLAYER_CASH") == cash - 18, "ordinary tick buys the exact deficit6 at existing3G")
|
||||
_expect(_state.get_resource(&"gold") == cash - 18 and row[&"last_result"] == &"PURCHASED" and row[&"last_attempt_minute"] == _clock.absolute_minute, "cash mirror and saved receipt record the real purchase")
|
||||
var receipt := SupplyAutoPurchasePlans.receipt_from_ledger(_runtime.economy_service, _warehouse, SupplyCatalog.RAW_FOOD, _clock.absolute_minute, StringName(row[&"last_transaction_id"]))
|
||||
_expect(receipt == {&"item_id": String(SupplyCatalog.RAW_FOOD), &"quantity": 6, &"unit_price": 3, &"total_cost": 18}, "independent actual ledger memo anchors item/quantity/historic price/total")
|
||||
_expect(notifications.size() == 1 and notifications[0][&"transaction_id"] == StringName(row[&"last_transaction_id"]) and notifications[0][&"entries"].size() == 1 and notifications[0][&"entries"][0][&"memo_key"] == String(SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.RAW_FOOD, 6, 3, 18)), "normal successful automatic purchase delivers one actual quoted ledger notification")
|
||||
_expect((_runtime.economy_service.get("_pending_transaction_notifications") as Dictionary).is_empty(), "normal automatic notification is committed without pending residue")
|
||||
_expect(paid[&"systems"][&"runtime"][&"next_command_id"] == armed[&"systems"][&"runtime"][&"next_command_id"] and var_to_bytes(paid[&"systems"][&"runtime"][&"command_queue"]) == var_to_bytes(armed[&"systems"][&"runtime"][&"command_queue"]), "automatic order allocates no fake player command or queue history")
|
||||
_expect(var_to_bytes(paid[&"systems"][&"runtime"][&"rng_streams"]) == var_to_bytes(armed[&"systems"][&"runtime"][&"rng_streams"]), "automatic purchase makes no RNG draw")
|
||||
_expect(_runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute).is_empty() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "same-minute reentry is exact no-op")
|
||||
_expect(notifications.size() == 1, "same-minute automatic reentry does not duplicate actual notification")
|
||||
_expect(_runtime.restore_snapshot(armed), "whole pre-order snapshot restores")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "saved pre-order branch replays all typed state/order/events/IDs exactly")
|
||||
_expect(_runtime.restore_snapshot(paid) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "paid order/current14 receipt roundtrip is exact")
|
||||
var duplicate_before: Dictionary = _runtime.create_snapshot()
|
||||
var duplicate: CommandResult = _plan(true, 10, &"auto.test.intent")
|
||||
_expect(not duplicate.is_success() and duplicate.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY and not duplicate.is_replay and _stock() == 10, "already accepted intent key is rejected by the canonical command queue")
|
||||
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(duplicate_before), "duplicate intent refusal changes no plan, inventory, ledger, IDs, queue or Runtime state")
|
||||
var owned: Dictionary = _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)
|
||||
owned[&"enabled"] = false
|
||||
_expect(_runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"], "returned plan is owned")
|
||||
|
||||
func _test_invalid_input_and_corrupt_save() -> void:
|
||||
_expect(_runtime.restore_snapshot(_built), "reset to paid real warehouse fixture")
|
||||
var before: Dictionary = _runtime.create_snapshot()
|
||||
for value: Variant in [true, -1, 101, 1.5, "4", NAN, INF]:
|
||||
var result: CommandResult = _runtime.supply_management.set_auto_purchase(_runtime,
|
||||
SimulationCommand.new(999, _clock.absolute_minute, _clock.absolute_minute, 75,
|
||||
&"set_supply_auto_purchase", &"player", _warehouse,
|
||||
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": value}))
|
||||
_expect(not result.is_success() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(before), "invalidtarget rejects atomically: %s" % value)
|
||||
for patch: Dictionary in [
|
||||
{&"warehouse_id": &"missing", &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 1},
|
||||
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.BASIC_MEAL, &"enabled": true, &"target_stock": 1},
|
||||
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": "true", &"target_stock": 1},
|
||||
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 0},
|
||||
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 1, &"unit_price": 0}]:
|
||||
var result: CommandResult = _runtime.supply_management.set_auto_purchase(_runtime, SimulationCommand.new(999,0,0,75,&"set_supply_auto_purchase",&"player",_warehouse,patch))
|
||||
_expect(not result.is_success() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(before), "forged/wrongintent changes no state")
|
||||
_expect(_plan(true, 6).is_success(), "configure a canonical saved plan")
|
||||
var canonical: Dictionary = _runtime.create_snapshot()
|
||||
for mutation: String in ["missing", "version", "duplicate", "unknown", "target", "bool", "clock", "receipt", "price", "extra"]:
|
||||
var bad: Dictionary = canonical.duplicate(true)
|
||||
var data: Dictionary = bad[&"systems"][&"runtime"]
|
||||
var plans: Dictionary = data[&"auto_purchase_plans"]
|
||||
var row: Dictionary = plans[&"plans"][0]
|
||||
match mutation:
|
||||
"missing": data.erase(&"auto_purchase_plans")
|
||||
"version": plans[&"version"] = 2
|
||||
"duplicate": (plans[&"plans"] as Array).append(row.duplicate(true))
|
||||
"unknown": row[&"warehouse_id"] = "missing"
|
||||
"target": row[&"target_stock"] = 101
|
||||
"bool": row[&"enabled"] = 1
|
||||
"clock": row[&"last_attempt_minute"] = _clock.absolute_minute + 1
|
||||
"receipt": row[&"last_transaction_id"] = "txn.foreign"
|
||||
"price": row[&"last_unit_price"] = 0.0
|
||||
"extra": row[&"unit_price"] = 0
|
||||
_expect(not _runtime.restore_snapshot(bad) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(canonical), "corrupt14 plan rejected without mutation: " + mutation)
|
||||
|
||||
func _test_atomic_failures_and_same_minute_retry() -> void:
|
||||
for kind: String in ["payment", "notification", "partial_stock"]:
|
||||
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "configure independent failure fixture: " + kind)
|
||||
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
|
||||
var inventory_before: Dictionary = _runtime.inventory_service.create_snapshot()
|
||||
var probe: ProbedInventory = PartialFailInventory.new() if kind == "partial_stock" else ProbedInventory.new()
|
||||
_expect(probe.restore_snapshot(inventory_before), "Inventory probe starts from full actual snapshot: " + kind)
|
||||
_runtime.inventory_service = probe
|
||||
if kind != "partial_stock":
|
||||
var rejecting: EconomyService = RejectingPaymentEconomy.new() if kind == "payment" else RejectingNotificationEconomy.new()
|
||||
_expect(rejecting.restore_snapshot(economy_before), "injected Economy starts from actual snapshot")
|
||||
_runtime.economy_service = rejecting
|
||||
_runtime.call("_connect_economy_signals")
|
||||
var notifications: Array[StringName] = []
|
||||
_runtime.economy_service.transaction_posted.connect(func(id: StringName, _entries: Array) -> void: notifications.append(id))
|
||||
var pending_before: Dictionary = (_runtime.economy_service.get("_pending_transaction_notifications") as Dictionary).duplicate(true)
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 0 and var_to_bytes(_runtime.economy_service.create_snapshot()) == var_to_bytes(economy_before), "failed order keeps actual cash and all ledger bytes unchanged: " + kind)
|
||||
_expect(not probe.transaction_boundary.is_empty() and var_to_bytes(_runtime.inventory_service.create_snapshot()) == var_to_bytes(probe.transaction_boundary), "transaction-boundary rollback restores full Inventory IDs/reservations/history/order, allowing the preceding ordinary clock advancement: " + kind)
|
||||
_expect(probe.transaction_boundary.get(&"current_minute", -1) == _clock.absolute_minute and _clock.absolute_minute > int(inventory_before[&"current_minute"]), "Inventory probe observes the actual next-minute transaction boundary: " + kind)
|
||||
_expect(notifications.is_empty() and var_to_bytes(_runtime.economy_service.get("_pending_transaction_notifications")) == var_to_bytes(pending_before), "failed automatic transaction emits no notification and leaves the observed pending set exact: " + kind)
|
||||
var failed: Dictionary = _runtime.create_snapshot()
|
||||
var row: Dictionary = _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)
|
||||
_expect(row[&"last_attempt_minute"] == _clock.absolute_minute and row[&"last_result"] != &"PURCHASED", "actual failure stays recorded in same minute: " + kind)
|
||||
_expect(_runtime.restore_snapshot(failed), "recorded failure restores into regular services: " + kind)
|
||||
_expect(_runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute).is_empty() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(failed), "same-minute regular retry cannot hide the actual failure: " + kind)
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 6 and _runtime.economy_service.get_balance(&"PLAYER_CASH") == int(_built[&"game_state"][&"resources"][&"gold"]) - 18, "next ordinary tick retries exactly once: " + kind)
|
||||
|
||||
func _test_hold_disable_and_removal() -> void:
|
||||
_expect(_runtime.restore_snapshot(_built), "hold fixture restores")
|
||||
_expect(_plan(true, 6).is_success(), "enable before normal closure")
|
||||
_expect(_runtime.submit_command(&"set_facility_policy", {&"facility_id": _warehouse, &"patch": {&"business_open": false}}).is_success(), "normal closure precedes auto tick")
|
||||
var cash: int = _state.get_resource(&"gold")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 0 and _state.get_resource(&"gold") == cash, "closed warehouse does not stock/pay")
|
||||
_expect(_plan(false, 0).is_success(), "setting disable works despite closed receiver")
|
||||
_expect(_runtime.submit_command(&"set_facility_policy", {&"facility_id": _warehouse, &"patch": {&"business_open": true}}).is_success(), "normal opening restores logistical eligibility")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 0, "disabled plan stays off after reopening")
|
||||
_expect(_plan(true, 6).is_success(), "enable again")
|
||||
_clock.advance_minutes(1)
|
||||
var paid: Dictionary = _runtime.create_snapshot()
|
||||
var removal: CommandResult = _runtime.submit_command(&"demolish_facility", {&"instance_id": _warehouse})
|
||||
_expect(not removal.is_success() and _runtime.auto_purchase_plans.create_snapshot() == paid[&"systems"][&"runtime"][&"auto_purchase_plans"], "stock-bearing failed removal preserves plan")
|
||||
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "future closure fixture resets")
|
||||
cash = _state.get_resource(&"gold")
|
||||
_expect(_runtime.submit_command(&"set_facility_policy", {&"facility_id": _warehouse, &"patch": {&"business_open": false}, &"effective_at": _clock.absolute_minute + 1}).is_success(), "normal future closure is scheduled for next tick")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 0 and _state.get_resource(&"gold") == cash and not _runtime.facility_service.get_instance(_warehouse).operator_open, "due closure blocks auto purchase before FacilityCompletion without early policy mutation")
|
||||
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "empty warehouse with explicit intent exists")
|
||||
_expect(_runtime.submit_command(&"demolish_facility", {&"instance_id": _warehouse}).is_success() and _runtime.auto_purchase_plans.get_plans().is_empty(), "successful empty warehouse removal prunes its intent")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "removed warehouse history/ledger remains restorable")
|
||||
|
||||
func _test_actual_budget_power_reservation_and_terminal() -> void:
|
||||
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "configure a paid budget boundary")
|
||||
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
|
||||
_expect(_runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", cash - 17, _clock.absolute_minute, &"CONSTRUCTION", &"", &"auto.acceptance.budget").is_success(), "disclosed ledger cash fixture leaves17G")
|
||||
_clock.advance_minutes(1)
|
||||
var row: Dictionary = _runtime.supply_management.auto_purchase_report(_runtime, _warehouse, SupplyCatalog.RAW_FOOD)
|
||||
_expect(_stock() == 0 and _state.get_resource(&"gold") == 17 and row[&"reason"] == &"INSUFFICIENT_FUNDS", "full deficit18G never silently becomes partial free or affordable17G order")
|
||||
_expect(row[&"last_attempt_minute"] == -1, "ineligible quote does not fabricate an executed order")
|
||||
_expect(_runtime.economy_service.transfer(&"CONSTRUCTION_EXPENSE", &"PLAYER_CASH", 1, _clock.absolute_minute, &"CONSTRUCTION", &"", &"auto.acceptance.budget.refund").is_success(), "same actual ledger can return1G")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 6 and _state.get_resource(&"gold") == 0, "eligible full order charges exactly available18G")
|
||||
var paid: Dictionary = _runtime.create_snapshot()
|
||||
var manual_key: StringName = &"auto.acceptance.manual.replacement"
|
||||
var manual: CommandResult = _runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"player", _warehouse, 75, -1, manual_key)
|
||||
_expect(not manual.is_success(), "manual insufficient cash remains rejected")
|
||||
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "reserved-stock fixture resets")
|
||||
_expect(_runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 6}).is_success(), "reserved stock starts from an actual paid order")
|
||||
var facility: FacilityInstance = _runtime.facility_service.get_instance(_warehouse)
|
||||
var reservation: Dictionary = _runtime.inventory_service.reserve(facility.local_inventory_id, SupplyCatalog.RAW_FOOD, 6, &"disclosed.reserve.fixture", &"TEST", _clock.absolute_minute)
|
||||
_expect(reservation.get(&"ok", false), "disclosed Inventory reservation retains physical stock")
|
||||
cash = _state.get_resource(&"gold")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 6 and _runtime.inventory_service.available_quantity(facility.local_inventory_id, SupplyCatalog.RAW_FOOD) == 0 and _state.get_resource(&"gold") == cash, "reserved physical units are not mistaken for missing stock or double-purchased")
|
||||
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "power boundary resets")
|
||||
_state.set_resource(&"mana", 0) # Disclosed resource boundary, not natural Mana generation.
|
||||
cash = _state.get_resource(&"gold")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(_stock() == 0 and _state.get_resource(&"gold") == cash, "Mana0 stops automatic paid order")
|
||||
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "paused real-time boundary resets")
|
||||
var paused: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_clock.process_real_delta(60.0) == 0 and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paused), "paused real clock causes no automatic purchase or ID/timer change")
|
||||
var collision_key: StringName = SupplyAutoPurchasePlans.order_key(_warehouse, SupplyCatalog.RAW_FOOD, _clock.absolute_minute)
|
||||
_expect(_runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", 1, _clock.absolute_minute, &"CONSTRUCTION", _warehouse, collision_key).is_success(), "disclosed foreign ledger-key fixture uses a valid ordinary transfer")
|
||||
cash = _state.get_resource(&"gold")
|
||||
var collisions: Array = _runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute)
|
||||
_expect(collisions.size() == 1 and _stock() == 0 and _state.get_resource(&"gold") == cash and _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)[&"last_result"] == &"REPLAY_MISMATCH", "foreign ledger namespace cannot be treated as a free stocked purchase")
|
||||
var collision: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(collision) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(collision), "foreign-key failure receipt remains restorable without making it purchase income")
|
||||
_expect(_runtime.restore_snapshot(paid), "restore an actual paid receipt for corruption tests")
|
||||
var corrupt: Dictionary = paid.duplicate(true)
|
||||
corrupt[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0][&"last_transaction_id"] = String(_runtime.economy_service.get_entries()[0].transaction_id)
|
||||
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "an existing unrelated actual transaction cannot replace the automatic receipt")
|
||||
for field: StringName in [&"last_quantity", &"last_unit_price", &"last_total_cost"]:
|
||||
corrupt = paid.duplicate(true)
|
||||
corrupt[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0][field] += 1
|
||||
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "saved auto quantity/price/total quote cannot disagree with payment: %s" % field)
|
||||
for pair: Vector2i in [Vector2i(3, 6), Vector2i(2, 9), Vector2i(1, 18), Vector2i(9, 2)]:
|
||||
corrupt = paid.duplicate(true)
|
||||
var changed_row: Dictionary = corrupt[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0]
|
||||
changed_row[&"last_quantity"] = pair.x
|
||||
changed_row[&"last_unit_price"] = pair.y
|
||||
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "coordinated factorization retaining actual18G cannot rewrite paid6x3G quote: %s" % pair)
|
||||
var receipt_id: String = paid[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0][&"last_transaction_id"]
|
||||
for malformed: StringName in [&"supply_purchase", SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.BASIC_MEAL, 6, 3, 18), SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.RAW_FOOD, 3, 6, 18), &"supply_purchase.auto.v1:19:ingredient/raw_food:6:3:18:extra"]:
|
||||
corrupt = paid.duplicate(true)
|
||||
for entry: Dictionary in corrupt[&"systems"][&"runtime"][&"economy"][&"entries"]:
|
||||
if entry[&"transaction_id"] == receipt_id: entry[&"memo_key"] = String(malformed)
|
||||
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "ledger quote missing/wrong item/re-factored/noncanonical is rejected against independent plan atomically: %s" % malformed)
|
||||
# Operations can only be enabled at fresh Day1 06:00 (360), before building.
|
||||
# Keep the normal paid construction and intent after that canonical start.
|
||||
_clock.reset(1, 6, 0)
|
||||
_state.reset()
|
||||
_expect(_runtime.initialize(_clock, _state, 420_501)[&"valid"], "fresh canonical bankruptcy fixture initializes")
|
||||
_clock.set_paused(true)
|
||||
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
var started: Dictionary = _runtime.enable_production_operations()
|
||||
_expect(started[&"ok"] and _clock.absolute_minute == 360, "actual bankruptcy boundary enables ordinary daily operation at fresh Day1 06:00")
|
||||
_expect(_runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "terminal fixture uses ordinary paid warehouse construction")
|
||||
_clock.advance_minutes(180)
|
||||
_warehouse = &""
|
||||
for endpoint: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
if endpoint.definition_id == &"warehouse": _warehouse = endpoint.instance_id
|
||||
_expect(not _warehouse.is_empty() and _plan(true, 6).is_success(), "terminal fixture configures explicit player intent after ordinary warehouse completion")
|
||||
_clock.advance_minutes(4320 - _clock.absolute_minute)
|
||||
_expect(_runtime.operations_coordinator.get_day_summaries().size() == 3 and _runtime.operations_coordinator.get_terminal_state() == &"BANKRUPT", "warehouse-only actual costs reach existing three-deficit terminal rule")
|
||||
var terminal: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute + 1).is_empty() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(terminal), "BANKRUPT blocks every further internal purchase without fake commands")
|
||||
_expect(_runtime.restore_snapshot(terminal), "terminal intent/ledger/current14 save still restores")
|
||||
|
||||
func _test_multiple_warehouse_order() -> void:
|
||||
_expect(_runtime.restore_snapshot(_built), "multiple actual warehouses fixture starts with one paid unit")
|
||||
_expect(_runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(2,0,1)}).is_success(), "ordinary second warehouse construction is paid")
|
||||
_clock.advance_minutes(180)
|
||||
var ids: Array[StringName] = []
|
||||
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
if facility.definition_id == &"warehouse": ids.append(facility.instance_id)
|
||||
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
_expect(ids.size() == 2, "two actual warehouse endpoints exist")
|
||||
if ids.size() != 2: return
|
||||
for index: int in [1, 0]:
|
||||
_expect(_runtime.submit_command(&"set_supply_auto_purchase", {&"warehouse_id": ids[index], &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 1}).is_success(), "reverse player setting order does not define payment priority")
|
||||
var cash: int = _state.get_resource(&"gold")
|
||||
_expect(_runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", cash - 3, _clock.absolute_minute, &"CONSTRUCTION", &"", &"auto.acceptance.multi_budget").is_success(), "disclosed shared cash fixture funds exactly one existing3G unit")
|
||||
var before: Dictionary = _runtime.create_snapshot()
|
||||
_clock.advance_minutes(1)
|
||||
var after: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.inventory_service.total_quantity(_runtime.facility_service.get_instance(ids[0]).local_inventory_id, SupplyCatalog.RAW_FOOD) == 1 and _runtime.inventory_service.total_quantity(_runtime.facility_service.get_instance(ids[1]).local_inventory_id, SupplyCatalog.RAW_FOOD) == 0, "explicit lexicographic warehouse/item ordering deterministically spends shared cash once")
|
||||
_expect(_runtime.restore_snapshot(before), "multiple-plan pre-order snapshot restores")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(after), "restored multiple-plan allocation reproduces every typed field and event in order")
|
||||
|
||||
func _test_legacy13_and_composed_rollback() -> void:
|
||||
_expect(_runtime.restore_snapshot(_built), "legacy fixture resets")
|
||||
var legacy: Dictionary = _built.duplicate(true)
|
||||
legacy[&"systems"][&"runtime"][&"runtime_schema_version"] = 13
|
||||
legacy[&"systems"][&"runtime"].erase(&"auto_purchase_plans")
|
||||
_expect(_runtime.restore_snapshot(legacy) and _runtime.auto_purchase_plans.get_plans().is_empty(), "valid13 snapshot migrates to disabled14 without purchase inference")
|
||||
var migrated: Dictionary = _runtime.create_snapshot()
|
||||
var disguised: Dictionary = legacy.duplicate(true)
|
||||
disguised[&"systems"][&"runtime"][&"auto_purchase_plans"] = {&"version": 1, &"plans": []}
|
||||
_expect(not _runtime.restore_snapshot(disguised) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(migrated), "oldversion cannot covertly retain new aggregate")
|
||||
_clock.set_paused(false)
|
||||
_expect(_plan(true, 6).is_success(), "actual current14 pending automatic intent queues")
|
||||
_clock.set_paused(true)
|
||||
var pending: Dictionary = _runtime.create_snapshot()
|
||||
var disguised_pending: Dictionary = pending.duplicate(true)
|
||||
disguised_pending[&"systems"][&"runtime"][&"runtime_schema_version"] = 13
|
||||
disguised_pending[&"systems"][&"runtime"].erase(&"auto_purchase_plans")
|
||||
_expect(not _runtime.restore_snapshot(disguised_pending) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(pending), "old13 cannot reinterpret a covert queued automatic order as paid feature intent")
|
||||
_expect(_runtime.restore_snapshot(_built), "reset after pending downgrade refusal")
|
||||
_expect(_plan(true, 6).is_success(), "intent exists before composed rollback token")
|
||||
var token: Dictionary = _runtime.call("_capture_operations_external_state")
|
||||
_expect(not token.is_empty() and token.has(&"auto_purchase_plans"), "Operations rollback includes owned plan state")
|
||||
_expect(_runtime.auto_purchase_plans.set_plan(_warehouse, SupplyCatalog.RAW_FOOD, false, 0, _runtime.facility_service)[&"ok"], "disclosed boundary mutation fixture changes only owned intent")
|
||||
_expect(_runtime.call("_restore_operations_external_state", token) and _runtime.call("_operations_external_state_matches", token), "composed rollback restores exact plan and other participants")
|
||||
_runtime.call("_discard_operations_domain_events")
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
_failures += 1
|
||||
push_error("FAIL: " + message)
|
||||
|
||||
func _finish() -> void:
|
||||
print("SUPPLY_AUTO_PURCHASE checks=%d failures=%d" % [_checks, _failures])
|
||||
quit(_failures)
|
||||
@@ -0,0 +1 @@
|
||||
uid://camsaleipkl22
|
||||
@@ -0,0 +1,264 @@
|
||||
extends SceneTree
|
||||
|
||||
## Supplemental P05 QA only. Actual paid construction/purchases and porter paths
|
||||
## are used. Capacity pressure, cargo lot expiry, callback failures and a ledger
|
||||
## refund are explicit boundary fixtures, not normal-business success evidence.
|
||||
var runtime: Variant
|
||||
var clock: Variant
|
||||
var state: Variant
|
||||
var warehouse: StringName
|
||||
var second: StringName
|
||||
var kitchen: StringName
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
var output := ""
|
||||
var observations: Array[Dictionary] = []
|
||||
const PORTER := &"staff_gina"
|
||||
const RETURN_REQUEST := &"supply.boundary_return"
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
func expect(ok: bool, label: String) -> void:
|
||||
checks += 1
|
||||
if ok:
|
||||
print(" ✓ ", label)
|
||||
else:
|
||||
failures += 1
|
||||
push_error(label)
|
||||
|
||||
func _run() -> void:
|
||||
output = OS.get_environment("THE_TREE_AUTO_BOUNDARY_OUTPUT")
|
||||
var user_dir := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_USER_DIR")
|
||||
var res_dir := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_RES_DIR")
|
||||
var profile := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_PROFILE")
|
||||
var external_evidence: bool = not output.is_empty()
|
||||
var valid := false
|
||||
if external_evidence:
|
||||
valid = output.is_absolute_path() and DirAccess.dir_exists_absolute(output) and not FileAccess.file_exists(output.path_join("boundaries.json")) and user_dir.is_absolute_path() and OS.get_user_data_dir() == user_dir and res_dir.is_absolute_path() and ProjectSettings.globalize_path("res://").simplify_path().trim_suffix("/") == res_dir.simplify_path().trim_suffix("/") and not profile.is_empty() and bool(ProjectSettings.get_setting("application/config/use_custom_user_dir", false)) and ProjectSettings.get_setting("application/config/custom_user_dir_name", "") == profile
|
||||
else:
|
||||
# Standard regression needs no external output/guards. Use only a new
|
||||
# process-specific tests child, never a player slot or res:// source file.
|
||||
var relative := "user://tests/supply_auto_boundaries_%d_%d" % [OS.get_process_id(), Time.get_ticks_usec()]
|
||||
output = ProjectSettings.globalize_path(relative).simplify_path()
|
||||
var allowed_parent := OS.get_user_data_dir().path_join("tests").simplify_path()
|
||||
valid = user_dir.is_empty() and res_dir.is_empty() and profile.is_empty() and output.get_base_dir() == allowed_parent and not DirAccess.dir_exists_absolute(output)
|
||||
if valid: valid = DirAccess.make_dir_recursive_absolute(output) == OK
|
||||
profile = ProjectSettings.get_setting("application/config/custom_user_dir_name", "")
|
||||
if not valid:
|
||||
push_error("P05_AUTO_BOUNDARIES requires exact fresh actual profile/resource/output guards")
|
||||
quit(91)
|
||||
return
|
||||
runtime = root.get_node("SimulationRuntime")
|
||||
clock = root.get_node("GameClock")
|
||||
state = root.get_node("GameState")
|
||||
clock.set_process(false)
|
||||
_storage_and_incoming()
|
||||
_local_power_and_suspension()
|
||||
_operations_deferred()
|
||||
_operations_quarantine()
|
||||
_unpaid_expenses()
|
||||
_returned_cargo()
|
||||
var file := FileAccess.open(output.path_join("boundaries.json"), FileAccess.WRITE)
|
||||
expect(file != null, "fresh boundary summary opens")
|
||||
if file != null:
|
||||
file.store_string(JSON.stringify({"checks": checks, "failures": failures, "actual_user_data_dir": OS.get_user_data_dir(), "actual_resource_dir": ProjectSettings.globalize_path("res://"), "actual_profile": profile, "output": output, "external_evidence": external_evidence, "observations": observations, "normal_business_acceptance": false, "p05_full_acceptance": false}, "\t", true, true) + "\n")
|
||||
file.close()
|
||||
print("Supply auto purchase boundaries: %d checks, %d failures" % [checks, failures])
|
||||
print("P05_AUTO_BOUNDARY_OUTPUT ", output)
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
func fresh(rows: Array = [[&"warehouse", Vector3i(-8, 0, 1)]], operations := false) -> bool:
|
||||
warehouse = &""
|
||||
second = &""
|
||||
kitchen = &""
|
||||
clock.reset(1, 6, 0)
|
||||
state.reset()
|
||||
expect(runtime.initialize(clock, state, 420_505)[&"valid"], "canonical14 Runtime resets")
|
||||
clock.set_paused(true)
|
||||
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
if operations:
|
||||
expect(runtime.enable_production_operations()[&"ok"], "actual Operations enable at original Day1 06:00 before construction")
|
||||
for row: Array in rows:
|
||||
expect(runtime.submit_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}).is_success(), "ordinary paid build: %s" % row[0])
|
||||
clock.advance_minutes(180)
|
||||
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
||||
if facility.definition_id == &"warehouse":
|
||||
if warehouse.is_empty(): warehouse = facility.instance_id
|
||||
else: second = facility.instance_id
|
||||
if facility.definition_id == &"kitchen": kitchen = facility.instance_id
|
||||
expect(not warehouse.is_empty(), "real warehouse completed")
|
||||
return not warehouse.is_empty()
|
||||
|
||||
func inv(id: StringName) -> StringName:
|
||||
return runtime.facility_service.get_instance(id).local_inventory_id
|
||||
|
||||
func stock(id: StringName = &"") -> int:
|
||||
if id.is_empty(): id = warehouse
|
||||
return runtime.inventory_service.total_quantity(inv(id), SupplyCatalog.RAW_FOOD)
|
||||
|
||||
func plan(target: int, minute: int = -1) -> CommandResult:
|
||||
return runtime.submit_command(&"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": target}, &"player", warehouse, 75, minute)
|
||||
|
||||
func record(label: String, before: Dictionary, report: Dictionary) -> void:
|
||||
var after: Dictionary = runtime.create_snapshot()
|
||||
for row: Array in [["before", before], ["after", after]]:
|
||||
var path: String = output.path_join(label + "-" + String(row[0]) + ".var")
|
||||
expect(not FileAccess.file_exists(path), "fresh actual typed boundary snapshot: " + label + "/" + String(row[0]))
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
expect(file != null, "typed boundary evidence opens")
|
||||
if file != null:
|
||||
file.store_buffer(var_to_bytes(row[1]))
|
||||
file.close()
|
||||
observations.append({"boundary": label, "report": report, "before_digest": SimulationDigest.compute(before), "after_digest": SimulationDigest.compute(after), "whole_bytes_equal": var_to_bytes(before) == var_to_bytes(after), "checks_so_far": checks, "failures_so_far": failures})
|
||||
|
||||
func _storage_and_incoming() -> void:
|
||||
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)], [&"warehouse", Vector3i(2, 0, 1)]]): return
|
||||
expect(not second.is_empty(), "actual second warehouse is inbound source")
|
||||
if second.is_empty(): return
|
||||
# Disclosed capacity-pressure seam: actual supported other item, not invented
|
||||
# commodity/capacity or ordinary free-stock success evidence.
|
||||
expect(runtime.inventory_service.add_stack(inv(warehouse), SupplyCatalog.BASIC_MEAL, 95, 100, clock.absolute_minute + 720, clock.absolute_minute, 1)[&"ok"], "disclosed95 other-item pressure occupies canonical100 capacity")
|
||||
expect(runtime.submit_command(&"purchase_supplies", {&"warehouse_id": second, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 6}).is_success(), "inbound6 are actually paid at the source")
|
||||
expect(runtime.submit_command(&"create_logistics_request", {&"request_id": &"supply.boundary_inbound", &"source_inventory_id": inv(second), &"destination_inventory_id": inv(warehouse), &"item_requirements": {SupplyCatalog.RAW_FOOD: 6}}).is_success(), "actual reserved inbound6 request targets the receiver")
|
||||
expect(plan(10).is_success(), "player arms10 actual enabled auto target")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
|
||||
expect(report[&"enabled"] and report[&"incoming_quantity"] == 6 and report[&"deficit"] == 4 and report[&"reason"] == &"STORAGE_FULL", "other95 plus actual inbound6 blocks remaining auto4 with STORAGE_FULL")
|
||||
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "STORAGE_FULL enabled auto changes no whole typed state or payment")
|
||||
record("storage-full-with-inbound", before, report)
|
||||
var reserved: Dictionary = runtime.inventory_service.reserve(inv(warehouse), SupplyCatalog.BASIC_MEAL, 95, &"boundary.pressure.release", &"TEST", clock.absolute_minute)
|
||||
expect(reserved.get(&"ok", false), "disclosed pressure release uses actual reservation API")
|
||||
var ids: Array[StringName] = []
|
||||
for id: Variant in reserved.get(&"reservation_ids", []): ids.append(StringName(id))
|
||||
expect(runtime.inventory_service.consume_reservations(ids, &"boundary.pressure.consume", clock.absolute_minute).get(&"ok", false), "pressure releases only its disclosed95 cooked meals")
|
||||
before = runtime.create_snapshot()
|
||||
var cash: int = state.get_resource(&"gold")
|
||||
clock.advance_minutes(1)
|
||||
expect(stock() == 4 and state.get_resource(&"gold") == cash - 12, "released capacity buys actual deficit4 once, retaining inbound6 deduction")
|
||||
expect(runtime.restore_snapshot(runtime.create_snapshot()), "enabled auto plus real reserved inbound14 saves strictly")
|
||||
record("inbound-deducted-purchase", before, runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD))
|
||||
|
||||
func _local_power_and_suspension() -> void:
|
||||
for kind: String in ["local-power", "suspension"]:
|
||||
if not fresh(): return
|
||||
expect(plan(6).is_success(), "arm enabled auto before " + kind)
|
||||
var conditions: Dictionary = {&"power_available": false} if kind == "local-power" else {&"suspension_reason": &"RAIDED"}
|
||||
expect(runtime.facility_service.set_operational_status(warehouse, conditions, clock.absolute_minute).is_success(), "disclosed public operational boundary: " + kind)
|
||||
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
|
||||
expect(state.get_resource(&"mana") > 0 and not facility.allows_logistics(), "global Mana remains available while warehouse local condition blocks " + kind)
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
|
||||
expect(report[&"enabled"] and report[&"state"] == &"BLOCKED" and report[&"reason"] == &"FACILITY_CLOSED", "owned auto report exposes local blocked receiver: " + kind)
|
||||
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "local stopped enabled auto spends/stocks/notifies nothing: " + kind)
|
||||
record(kind, before, report)
|
||||
|
||||
func _operations_deferred() -> void:
|
||||
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)]], true): return
|
||||
var due: int = clock.absolute_minute + 30
|
||||
expect(plan(6, due).is_success(), "actual auto intent is queued for failing Operations boundary")
|
||||
expect(runtime.set_operations_test_boundary_hook(&"ARRIVAL_PLAN", func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary: return {&"ok": false, &"reason": &"DISCLOSED_BOUNDARY_PREPARE_FAILURE", &"phase": phase}), "disclosed actual prepare fault seam registered")
|
||||
clock.advance_minutes(30)
|
||||
var operations: Dictionary = runtime.get_operations_report()
|
||||
expect(operations[&"tick_blocked"] and operations[&"runtime_failure_latched"] and operations[&"deferred_runtime_minute"] == due and runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"], "actual deferred suffix is blocked with actual enabled auto intent")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
expect(stock() == 0 and runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)[&"reason"] == &"OPERATIONS_BLOCKED", "deferred boundary never purchases before its skipped suffix")
|
||||
expect(runtime.supply_management.advance_auto_purchases(runtime, due).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "deferred enabled auto is whole typed no-op")
|
||||
record("operations-deferred", before, operations)
|
||||
expect(runtime.restore_snapshot(before), "actual deferred enabled-auto14 snapshot strictly restores")
|
||||
var cash: int = state.get_resource(&"gold")
|
||||
var acknowledged: Dictionary = runtime.acknowledge_operations_pause()
|
||||
expect(acknowledged.get(&"ok", false) and acknowledged.get(&"suffix_replayed", false) and stock() == 6 and state.get_resource(&"gold") == cash - 18, "explicit actual retry runs deferred suffix and paid auto6 exactly once")
|
||||
var completed: Dictionary = runtime.create_snapshot()
|
||||
expect(runtime.supply_management.advance_auto_purchases(runtime, due).is_empty() and var_to_bytes(completed) == var_to_bytes(runtime.create_snapshot()), "retried actual suffix cannot purchase twice in the same minute")
|
||||
|
||||
func _quarantine_arrival(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
|
||||
if phase == OperationsCoordinator.PHASE_APPLY:
|
||||
return {&"ok": false, &"reason": &"DISCLOSED_APPLY_FAILURE"}
|
||||
if phase == OperationsCoordinator.PHASE_ROLLBACK:
|
||||
return {&"ok": false, &"reason": &"DISCLOSED_ROLLBACK_UNAVAILABLE"}
|
||||
return runtime.call("_operations_arrival_boundary", payload, phase, token)
|
||||
|
||||
func _operations_quarantine() -> void:
|
||||
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)]], true): return
|
||||
var due: int = clock.absolute_minute + 30
|
||||
expect(plan(6, due).is_success(), "actual auto intent is queued for quarantined boundary")
|
||||
# Public callback contract, no manual tick_blocked/quarantine flag mutation.
|
||||
runtime.operations_coordinator.configure_callbacks(Callable(runtime, "_operations_daily_boundary"), Callable(self, "_quarantine_arrival"), Callable(runtime, "_operations_morning_boundary"), Callable(runtime, "_operations_midnight_upkeep_boundary"))
|
||||
clock.advance_minutes(30)
|
||||
var operations: Dictionary = runtime.get_operations_report()
|
||||
expect(runtime.operations_coordinator.is_external_quarantined() and operations[&"tick_blocked"] and runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"], "actual external rollback failure quarantines owning Coordinator with enabled auto")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
expect(stock() == 0 and runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)[&"reason"] == &"OPERATIONS_BLOCKED", "actual quarantine exposes blocked enabled auto")
|
||||
expect(runtime.supply_management.advance_auto_purchases(runtime, due).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "quarantined enabled auto is whole typed no-op")
|
||||
expect(not runtime.acknowledge_operations_pause().get(&"ok", true), "normal acknowledgement cannot clear external uncertainty")
|
||||
record("operations-quarantine", before, {"operations": operations, "quarantine": runtime.operations_coordinator.get_quarantine_report()})
|
||||
|
||||
func _unpaid_expenses() -> void:
|
||||
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)]], true): return
|
||||
clock.advance_minutes(1439 - clock.absolute_minute)
|
||||
var cash: int = state.get_resource(&"gold")
|
||||
expect(runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", cash, clock.absolute_minute, &"CONSTRUCTION", warehouse, &"boundary.disclosed.budget").is_success(), "disclosed real ledger budget fixture spends cash before midnight")
|
||||
clock.advance_minutes(1)
|
||||
var debt: int = runtime.upkeep_settlement.get_arrears_total()
|
||||
expect(debt > 0 and runtime.operations_coordinator.get_arrears_total() == debt and state.get_resource(&"gold") == 0, "actual midnight accrual creates unpaid upkeep, not crafted debt")
|
||||
expect(runtime.economy_service.transfer(&"CONSTRUCTION_EXPENSE", &"PLAYER_CASH", 18, clock.absolute_minute, &"CONSTRUCTION_REFUND", warehouse, &"boundary.disclosed.refund").is_success(), "disclosed actual ledger refund funds a6-food order")
|
||||
expect(plan(6).is_success(), "player arms auto while actual debt remains unpaid")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
var settlement: Dictionary = runtime.upkeep_settlement.create_snapshot()
|
||||
clock.advance_minutes(1)
|
||||
expect(stock() == 6 and state.get_resource(&"gold") == 0 and runtime.upkeep_settlement.get_arrears_total() == debt, "actual18G paid auto preserves unpaid expense instead of erasing or netting debt")
|
||||
expect(var_to_bytes(settlement) == var_to_bytes(runtime.upkeep_settlement.create_snapshot()), "procurement leaves all unpaid settlement receipts/history/IDs exact")
|
||||
expect(runtime.restore_snapshot(runtime.create_snapshot()), "actual paid auto plus unpaid expense14 strictly restores")
|
||||
record("unpaid-expense-procurement", before, {"actual_arrears": debt, "settlement": settlement, "ordinary_sales_fixture": false})
|
||||
|
||||
func _returned_cargo() -> void:
|
||||
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]): return
|
||||
expect(not kitchen.is_empty(), "real kitchen destination completes")
|
||||
if kitchen.is_empty(): return
|
||||
expect(runtime.submit_command(&"hire_staff_candidate", {&"candidate_id": PORTER}).is_success(), "ordinary paid hire provides real porter")
|
||||
expect(runtime.submit_command(&"set_staff_work_assignment", {&"staff_id": PORTER, &"facility_id": warehouse, &"template_id": &"DAY"}).is_success(), "real recurring shift assigns warehouse porter")
|
||||
for minute: int in range(40):
|
||||
if runtime.staff_service.get_staff(PORTER).state == StaffRuntime.State.AVAILABLE: break
|
||||
clock.advance_minutes(1)
|
||||
expect(runtime.staff_service.get_staff(PORTER).state == StaffRuntime.State.AVAILABLE, "porter physically checks in")
|
||||
expect(runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8}).is_success(), "all cargo8 originate in ordinary paid purchase")
|
||||
expect(runtime.submit_command(&"create_logistics_request", {&"request_id": RETURN_REQUEST, &"source_inventory_id": inv(warehouse), &"destination_inventory_id": inv(kitchen), &"item_requirements": {SupplyCatalog.RAW_FOOD: 8}}).is_success(), "actual reserved request starts porter delivery")
|
||||
for minute: int in range(40):
|
||||
if runtime.logistics_requests[RETURN_REQUEST].state == LogisticsRequest.State.DELIVERING: break
|
||||
clock.advance_minutes(1)
|
||||
var request: LogisticsRequest = runtime.logistics_requests[RETURN_REQUEST]
|
||||
var cargo: StringName = StringName("cargo:%s" % PORTER)
|
||||
expect(request.state == LogisticsRequest.State.DELIVERING and stock() == 0 and runtime.inventory_service.total_quantity(cargo, SupplyCatalog.RAW_FOOD) == 8, "real employee picks up8 paid stock into owned cargo")
|
||||
if request.state != LogisticsRequest.State.DELIVERING: return
|
||||
clock.advance_minutes(2)
|
||||
# Disclosed expiry-lot seam preserves the exact8 paid units. No extra or free
|
||||
# stock is inserted; only half the existing lot expires at the next minute.
|
||||
var inventory: Dictionary = runtime.inventory_service.create_snapshot()
|
||||
for owner: Dictionary in inventory[&"inventories"]:
|
||||
if owner[&"owner_inventory_id"] == String(cargo):
|
||||
var lot: Dictionary = owner[&"stacks"][0]
|
||||
lot[&"quantity"] = 4
|
||||
var expiring: Dictionary = lot.duplicate(true)
|
||||
expiring[&"stack_id"] = "boundary-expiring-paid-lot"
|
||||
expiring[&"expires_at"] = clock.absolute_minute + 1
|
||||
(owner[&"stacks"] as Array).append(expiring)
|
||||
(inventory[&"known_stack_ids"] as Array).append("boundary-expiring-paid-lot")
|
||||
expect(runtime.inventory_service.restore_snapshot(inventory), "disclosed expiry split preserves8 actual paid units")
|
||||
clock.advance_minutes(1)
|
||||
request = runtime.logistics_requests[RETURN_REQUEST]
|
||||
expect(runtime.supply_logistics.is_returning(RETURN_REQUEST) and runtime.inventory_service.total_quantity(cargo, SupplyCatalog.RAW_FOOD) == 4, "actual bridge retargets unspoiled paid4 as return cargo")
|
||||
expect(plan(4).is_success(), "player enables auto4 while actual4 cargo are returning")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
|
||||
expect(report[&"enabled"] and report[&"physical_quantity"] == 0 and report[&"incoming_quantity"] == 4 and report[&"deficit"] == 0 and report[&"state"] == &"TARGET_REACHED", "actual return cargo4 satisfies target despite warehouse physical0")
|
||||
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "actual incoming return suppresses duplicate whole-state purchase")
|
||||
record("return-cargo-pending", before, report)
|
||||
var cash: int = state.get_resource(&"gold")
|
||||
for minute: int in range(40):
|
||||
if request.state == LogisticsRequest.State.CANCELLED: break
|
||||
clock.advance_minutes(1)
|
||||
expect(request.state == LogisticsRequest.State.CANCELLED and request.state_reason == "CARGO_EXPIRED_RETURNED" and stock() == 4 and state.get_resource(&"gold") == cash, "actual porter return unloads paid4 with no duplicate auto charge")
|
||||
expect(runtime.inventory_service.total_quantity(cargo, SupplyCatalog.RAW_FOOD) == 0 and runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and SupplyLogisticsBridge.validate_crosslinks(runtime.logistics_requests, runtime.staff_service, runtime.inventory_service, runtime.facility_service), "actual return clears cargo/task/reservations/carrier ownership")
|
||||
expect(runtime.restore_snapshot(runtime.create_snapshot()), "actual completed return plus enabled intent strictly restores14")
|
||||
record("return-cargo-completed", before, runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD))
|
||||
@@ -0,0 +1 @@
|
||||
uid://pyqycsjyyeuo
|
||||
@@ -0,0 +1,274 @@
|
||||
extends SceneTree
|
||||
|
||||
## Artifact-only acceptance input, not a new standard test or production value.
|
||||
## The warehouse is normally paid/built, full durability10000, and auto6 is paid.
|
||||
## A disclosed existing Defense command fixture attacks it through real automatic
|
||||
## 5-minute rounds. No pre-damage, direct status/suspension, mock outcome or clock
|
||||
## assignment is used. Threat10000/interval5 are existing test inputs; 201 rounds
|
||||
## and1010 minutes merely allow real max50 damage/round to exhaust full10000.
|
||||
## Mana0/recovery uses the public resource seam and actual Runtime projection;
|
||||
## this is not a claim of natural outage/thief/bankruptcy threat generation.
|
||||
|
||||
const SAVE_PATH := "user://p05-enabled-auto-destruction.ttwb"
|
||||
const RAID := &"p05.auto.destroy.raid"
|
||||
const SOURCE_PATHS: Array[String] = [
|
||||
"autoload/simulation_runtime.gd", "scripts/supply/supply_management_service.gd",
|
||||
"scripts/supply/supply_auto_purchase_plans.gd", "scripts/ui/supply_management_panel.gd",
|
||||
"scripts/defense/defense_service.gd", "scripts/facility/facility_instance.gd",
|
||||
"scripts/facility/facility_service.gd", "scripts/mana/mana_power_projection.gd",
|
||||
"scripts/core/save_service.gd", "scripts/core/simulation_snapshot.gd",
|
||||
]
|
||||
var runtime: Variant
|
||||
var clock: Variant
|
||||
var state: Variant
|
||||
var warehouse: StringName = &""
|
||||
var output := ""
|
||||
var mode := ""
|
||||
var save_path := SAVE_PATH
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
var purchase_notifications := 0
|
||||
var summary: Dictionary = {}
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
func expect(ok: bool, label: String) -> void:
|
||||
checks += 1
|
||||
if ok: print(" ✓ ", label)
|
||||
else:
|
||||
failures += 1
|
||||
push_error("P05_AUTO_DESTRUCTION: " + label)
|
||||
|
||||
func _run() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.size() == 1 and args[0] == "--write": mode = "write"
|
||||
elif args.size() == 1 and args[0] == "--read": mode = "read"
|
||||
output = OS.get_environment("THE_TREE_AUTO_DESTRUCTION_OUTPUT")
|
||||
var user_dir := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_USER_DIR")
|
||||
var res_dir := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_RES_DIR")
|
||||
var profile := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_PROFILE")
|
||||
var any_external_guard := false
|
||||
for key: String in ["THE_TREE_AUTO_DESTRUCTION_OUTPUT", "THE_TREE_AUTO_PURCHASE_EXPECTED_USER_DIR", "THE_TREE_AUTO_PURCHASE_EXPECTED_RES_DIR", "THE_TREE_AUTO_PURCHASE_EXPECTED_PROFILE"]:
|
||||
any_external_guard = any_external_guard or OS.has_environment(key)
|
||||
var guards := false
|
||||
if any_external_guard:
|
||||
guards = not mode.is_empty() and output.is_absolute_path() and DirAccess.dir_exists_absolute(output) and not FileAccess.file_exists(output.path_join(mode + "-summary.json")) and user_dir.is_absolute_path() and OS.get_user_data_dir() == user_dir and res_dir.is_absolute_path() and ProjectSettings.globalize_path("res://").simplify_path().trim_suffix("/") == res_dir.simplify_path().trim_suffix("/") and not profile.is_empty() and bool(ProjectSettings.get_setting("application/config/use_custom_user_dir", false)) and ProjectSettings.get_setting("application/config/custom_user_dir_name", "") == profile
|
||||
elif args.is_empty():
|
||||
mode = "standard"
|
||||
var nonce := "%d_%d" % [OS.get_process_id(), Time.get_ticks_usec()]
|
||||
var user_test_root := "user://tests/p05_auto_destruction_" + nonce
|
||||
save_path = user_test_root.path_join("formal.ttwb")
|
||||
output = ProjectSettings.globalize_path(user_test_root.path_join("proofs"))
|
||||
guards = not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(user_test_root)) and DirAccess.make_dir_recursive_absolute(output) == OK
|
||||
user_dir = OS.get_user_data_dir()
|
||||
res_dir = ProjectSettings.globalize_path("res://")
|
||||
profile = String(ProjectSettings.get_setting("application/config/custom_user_dir_name", ""))
|
||||
if not guards:
|
||||
push_error("P05_AUTO_DESTRUCTION exact mode/profile/resource/fresh output guards failed")
|
||||
quit(91)
|
||||
return
|
||||
runtime = root.get_node("SimulationRuntime")
|
||||
clock = root.get_node("GameClock")
|
||||
state = root.get_node("GameState")
|
||||
clock.set_process(false)
|
||||
_reset_fresh_runtime()
|
||||
summary = {"mode": mode, "actual_user_data_dir": OS.get_user_data_dir(), "actual_resource_dir": ProjectSettings.globalize_path("res://"), "actual_profile": profile, "configured_attack_fixture": true, "natural_threat_generation_acceptance": false, "production_balance_change": false, "whole_P05_acceptance": false, "source_sha256": {}}
|
||||
for path: String in SOURCE_PATHS:
|
||||
summary["source_sha256"][path] = FileAccess.get_sha256("res://" + path)
|
||||
expect(runtime.create_snapshot()[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "exact current14 snapshot required")
|
||||
if mode in ["write", "standard"]: _write_phase()
|
||||
if mode == "standard" and failures == 0:
|
||||
_reset_fresh_runtime()
|
||||
_read_phase()
|
||||
elif mode == "read": _read_phase()
|
||||
summary["checks"] = checks
|
||||
summary["failures"] = failures
|
||||
summary["save_path"] = ProjectSettings.globalize_path(save_path)
|
||||
summary["two_process_restarted_in_this_mode"] = mode == "read"
|
||||
var file := FileAccess.open(output.path_join(mode + "-summary.json"), FileAccess.WRITE)
|
||||
if file == null:
|
||||
push_error("P05_AUTO_DESTRUCTION cannot open fresh summary")
|
||||
quit(92)
|
||||
return
|
||||
file.store_string(JSON.stringify(summary, "\t", true, true) + "\n")
|
||||
file.close()
|
||||
print("P05_AUTO_DESTRUCTION phase=%s checks=%d failures=%d" % [mode, checks, failures])
|
||||
print("Supply auto purchase destruction: %d checks, %d failures" % [checks, failures])
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
func _reset_fresh_runtime() -> void:
|
||||
clock.reset(1, 6, 0)
|
||||
state.reset()
|
||||
expect(runtime.initialize(clock, state, 420_506)[&"valid"], "canonical14 Runtime initializes")
|
||||
clock.set_paused(true)
|
||||
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
purchase_notifications = 0
|
||||
|
||||
func _command(kind: StringName, payload: Dictionary) -> CommandResult:
|
||||
return runtime.submit_command(kind, payload, &"player", warehouse, 75)
|
||||
|
||||
func _stock() -> int:
|
||||
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
|
||||
return runtime.inventory_service.total_quantity(facility.local_inventory_id, SupplyCatalog.RAW_FOOD) if facility != null else -1
|
||||
|
||||
func _plan() -> Dictionary:
|
||||
return runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)
|
||||
|
||||
func _auto_payment_count() -> int:
|
||||
var count := 0
|
||||
for entry: LedgerEntry in runtime.economy_service.get_entries():
|
||||
if String(entry.memo_key).begins_with(SupplyAutoPurchasePlans.RECEIPT_PREFIX): count += 1
|
||||
return count
|
||||
|
||||
func _observe_payment(_id: StringName, entries: Array) -> void:
|
||||
for entry: LedgerEntry in entries:
|
||||
if String(entry.memo_key).begins_with(SupplyAutoPurchasePlans.RECEIPT_PREFIX): purchase_notifications += 1
|
||||
|
||||
func _typed(label: String, value: Dictionary) -> void:
|
||||
var path := output.path_join(label + ".var")
|
||||
expect(not FileAccess.file_exists(path), "fresh typed proof: " + label)
|
||||
if FileAccess.file_exists(path): return
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
expect(file != null, "typed proof opens: " + label)
|
||||
if file == null: return
|
||||
file.store_buffer(var_to_bytes(value))
|
||||
file.close()
|
||||
|
||||
func _write_phase() -> void:
|
||||
var absolute_save := ProjectSettings.globalize_path(save_path)
|
||||
expect(not DirAccess.dir_exists_absolute(absolute_save) and not FileAccess.file_exists(absolute_save), "fresh formal save never overwrites an existing package")
|
||||
if DirAccess.dir_exists_absolute(absolute_save) or FileAccess.file_exists(absolute_save): return
|
||||
expect(_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "normal paid warehouse construction begins")
|
||||
clock.advance_minutes(180)
|
||||
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
||||
if facility.definition_id == &"warehouse": warehouse = facility.instance_id
|
||||
expect(not warehouse.is_empty(), "normal warehouse completes without synthetic Facility instance")
|
||||
if warehouse.is_empty(): return
|
||||
var original: FacilityInstance = runtime.facility_service.get_instance(warehouse)
|
||||
expect(original.durability == FacilityInstance.DEFAULT_DURABILITY and original.durability == 10000 and original.suspension_reason.is_empty(), "paid warehouse starts at full10000 integrity without pre-damage")
|
||||
runtime.economy_service.transaction_posted.connect(_observe_payment)
|
||||
var cash: int = state.get_resource(&"gold")
|
||||
expect(_command(&"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 6}).is_success(), "player enables auto6 before actual combat")
|
||||
clock.advance_minutes(1)
|
||||
expect(_stock() == 6 and state.get_resource(&"gold") == cash - 18 and _auto_payment_count() == 1 and purchase_notifications == 1, "operating warehouse actually buys6 once for18G before attack")
|
||||
var pre_combat: Dictionary = runtime.create_snapshot()
|
||||
var owned_before: Dictionary = _plan()
|
||||
_typed("before-configured-attack", pre_combat)
|
||||
var definition := {&"source_id": &"p05.disclosed.configured_attack", &"entry_points": [&"gate.north"], &"objective_ids": [warehouse], &"round_minutes": 5, &"max_duration_minutes": 1010, &"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0, &"terrain_bonus": 0, &"command_bonus": 0, &"surprise_penalty": 0, &"multiple_entry_penalty": 0, &"waves": [{&"wave_id": &"p05.auto.destroy.wave", &"threat_strength": 10000, &"max_rounds": 201, &"retreat_threshold": 0, &"entry_points": [&"gate.north"]}]}
|
||||
expect(_command(&"create_defense_raid", {&"raid_id": RAID, &"definition": definition}).is_success(), "disclosed configured attack created through actual Runtime command")
|
||||
expect(_command(&"register_defense_asset", {&"raid_id": RAID, &"asset_id": warehouse, &"kind": &"OBJECTIVE", &"max_integrity": FacilityInstance.DEFAULT_DURABILITY, &"defense": 0, &"options": {&"target_order": 1}}).is_success(), "real full-durability warehouse registered as actual objective")
|
||||
expect(_command(&"start_defense_raid", {&"raid_id": RAID}).is_success(), "actual raid starts without injected destruction or mock outcome")
|
||||
var rng_before: Dictionary = runtime.get_rng_stream(&"defense").snapshot()
|
||||
clock.advance_minutes(4)
|
||||
expect(runtime.defense_service.get_raid(RAID)[&"current_round"] == 0 and runtime.get_rng_stream(&"defense").snapshot() == rng_before, "deadline-minus1 does not attack or consume RNG")
|
||||
clock.advance_minutes(1)
|
||||
for _index: int in range(200):
|
||||
var ongoing: Dictionary = runtime.defense_service.get_raid(RAID)
|
||||
if ongoing[&"state"] in DefenseService.TERMINAL_STATES: break
|
||||
clock.advance_minutes(5)
|
||||
var raid: Dictionary = runtime.defense_service.get_raid(RAID)
|
||||
var destroyed: FacilityInstance = runtime.facility_service.get_instance(warehouse)
|
||||
var damage := 0
|
||||
var target_matches := true
|
||||
for round_value: Variant in (raid[&"resolved_rounds"] as Dictionary).values():
|
||||
var round_row: Dictionary = round_value
|
||||
var hit: Dictionary = round_row[&"defense_damage"]
|
||||
damage += int(hit.get(&"damage", 0))
|
||||
target_matches = target_matches and hit.get(&"target_type", &"") == &"ASSET" and hit.get(&"target_id", &"") == warehouse
|
||||
expect(raid[&"state"] == DefenseService.RaidState.BREACHED and raid[&"current_round"] == 200 and damage == 10000 and target_matches, "200 real automatic combat rounds destroy full10000 warehouse via actual outcome boundary")
|
||||
expect(destroyed.durability == 0 and destroyed.state == FacilityInstance.State.BROKEN and destroyed.suspension_reason == &"DESTROYED" and destroyed.operation_state == FacilityInstance.OperationState.SUSPENDED, "Defense terminal projection owns actual persistent DESTROYED suspension")
|
||||
expect(var_to_bytes(_plan()) == var_to_bytes(owned_before) and _stock() == 6 and _auto_payment_count() == 1 and purchase_notifications == 1, "combat preserves enabled intent and its paid stock/receipt without repeat procurement")
|
||||
expect(runtime.economy_service.validate_ledger()[&"ok"] and not runtime.get_operations_report()[&"tick_blocked"], "combat settles without ledger or unrelated Operations block")
|
||||
# A normal post-destruction input makes a real positive deficit; no inventory
|
||||
# is consumed/replaced and no destroyed/operational status is directly set.
|
||||
expect(_command(&"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 7}).is_success(), "ordinary player target7 is retained even though receiver is destroyed")
|
||||
_check_blocked("post-combat", 6)
|
||||
var checkpoint: Dictionary = runtime.create_snapshot()
|
||||
_typed("formal-checkpoint14", checkpoint)
|
||||
var saved: Dictionary = runtime.save_to_path(save_path, {&"purpose": &"p05.actual_defense_destroyed_auto"})
|
||||
expect(saved.get(&"ok", false), "actual Runtime writes fresh formal current14 package")
|
||||
var inspected: Dictionary = SaveService.inspect_snapshot(save_path)
|
||||
expect(inspected.get(&"ok", false) and var_to_bytes(inspected.get(&"snapshot", {})) == var_to_bytes(checkpoint), "read-only formal package contains exact whole typed checkpoint14")
|
||||
summary["warehouse_id"] = String(warehouse)
|
||||
summary["actual_rounds"] = raid[&"current_round"]
|
||||
summary["actual_damage"] = damage
|
||||
summary["actual_resolved_rounds"] = (raid[&"resolved_rounds"] as Dictionary).size()
|
||||
summary["before_attack_plan"] = owned_before
|
||||
summary["saved_plan"] = _plan()
|
||||
summary["auto_payment_count"] = _auto_payment_count()
|
||||
summary["automatic_purchase_notifications"] = purchase_notifications
|
||||
|
||||
func _check_blocked(label: String, expected_stock: int) -> void:
|
||||
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
|
||||
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
|
||||
expect(facility != null and facility.durability == 0 and facility.suspension_reason == &"DESTROYED" and not facility.allows_logistics(), label + ": actual destroyed receiver rejects Logistics")
|
||||
expect(report[&"enabled"] and report[&"target_stock"] == 7 and report[&"physical_quantity"] == expected_stock and report[&"incoming_quantity"] == 0 and report[&"deficit"] == 1 and report[&"reason"] == &"FACILITY_CLOSED", label + ": positive real shortage exposes destroyed facility block")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), label + ": blocked procurement preserves every typed aggregate/ID/receipt/event/RNG byte")
|
||||
_typed(label + "-blocked-before", before)
|
||||
_typed(label + "-blocked-after", runtime.create_snapshot())
|
||||
|
||||
func _read_phase() -> void:
|
||||
var expected_file := FileAccess.open(output.path_join("formal-checkpoint14.var"), FileAccess.READ)
|
||||
var resumed_context := "new process" if mode == "read" else "fresh Runtime reinitialization"
|
||||
expect(expected_file != null, resumed_context + ": retains actual whole typed checkpoint")
|
||||
if expected_file == null: return
|
||||
var expected_value: Variant = bytes_to_var(expected_file.get_buffer(expected_file.get_length()))
|
||||
expected_file.close()
|
||||
expect(expected_value is Dictionary, "prior typed checkpoint is a Dictionary")
|
||||
if not expected_value is Dictionary: return
|
||||
var expected: Dictionary = expected_value
|
||||
var inspected: Dictionary = SaveService.inspect_snapshot(save_path)
|
||||
expect(inspected.get(&"ok", false) and var_to_bytes(inspected.get(&"snapshot", {})) == var_to_bytes(expected), resumed_context + ": same exact current14 formal package")
|
||||
var loaded: Dictionary = runtime.load_from_path(save_path)
|
||||
expect(loaded.get(&"ok", false), resumed_context + ": ordinary Runtime load commits strict14 destroyed checkpoint")
|
||||
if not loaded.get(&"ok", false): return
|
||||
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
||||
if facility.definition_id == &"warehouse": warehouse = facility.instance_id
|
||||
expect(not warehouse.is_empty(), "loaded original warehouse identity remains present")
|
||||
if warehouse.is_empty(): return
|
||||
var normal_loaded: Dictionary = runtime.create_snapshot()
|
||||
_typed("normal-runtime-load14", normal_loaded)
|
||||
# SAVE_LOADED publishes a legitimate event. Strict restore of that same
|
||||
# package separately proves whole-state byte equality, without hiding it.
|
||||
expect(runtime.restore_snapshot(expected) and var_to_bytes(runtime.create_snapshot()) == var_to_bytes(expected), "strict14 restore reproduces every saved typed byte; normal load event kept separately")
|
||||
var plan_before: Dictionary = _plan()
|
||||
var inventory_before: Dictionary = runtime.inventory_service.create_snapshot()
|
||||
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
|
||||
runtime.economy_service.transaction_posted.connect(_observe_payment)
|
||||
_check_blocked("loaded-restored", 6)
|
||||
var minute_before_power: int = clock.absolute_minute
|
||||
var positive_mana: int = state.get_resource(&"mana")
|
||||
expect(positive_mana > 0, "pre-outage reserve is actually positive")
|
||||
state.set_resource(&"mana", 0) # Explicit reserve boundary, actual power projection.
|
||||
clock.advance_minutes(1)
|
||||
var outage_facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
|
||||
expect(not outage_facility.power_available and outage_facility.durability == 0 and outage_facility.suspension_reason == &"DESTROYED", "actual Mana gate outage changes power only; destroyed status persists")
|
||||
var outage: Dictionary = runtime.create_snapshot()
|
||||
_typed("actual-power-outage14", outage)
|
||||
expect(runtime.restore_snapshot(outage) and var_to_bytes(runtime.create_snapshot()) == var_to_bytes(outage), "strict14 outage snapshot retains destroyed facility and owned enabled plan")
|
||||
state.set_resource(&"mana", positive_mana)
|
||||
clock.advance_minutes(1)
|
||||
var recovered_power: FacilityInstance = runtime.facility_service.get_instance(warehouse)
|
||||
expect(recovered_power.power_available and recovered_power.durability == 0 and recovered_power.suspension_reason == &"DESTROYED" and recovered_power.operation_state == FacilityInstance.OperationState.SUSPENDED, "actual power recovery cannot clear persistent destruction or resume operations")
|
||||
_check_blocked("power-restored", 6)
|
||||
expect(var_to_bytes(_plan()) == var_to_bytes(plan_before) and _stock() == 6 and _auto_payment_count() == 1 and purchase_notifications == 0, "restored power produces no stock/payment/notification and preserves explicit intent history")
|
||||
expect(clock.absolute_minute == minute_before_power + 2, "two ordinary elapsed projection ticks are separate from no-op procurement")
|
||||
# Inventory/Economy current_minute legitimately advances with the two clock
|
||||
# ticks. Compare complete participant state after normalizing only this field.
|
||||
var inventory_after: Dictionary = runtime.inventory_service.create_snapshot()
|
||||
var economy_after: Dictionary = runtime.economy_service.create_snapshot()
|
||||
inventory_after[&"current_minute"] = inventory_before[&"current_minute"]
|
||||
economy_after[&"current_minute"] = economy_before[&"current_minute"]
|
||||
expect(var_to_bytes(inventory_after) == var_to_bytes(inventory_before) and var_to_bytes(economy_after) == var_to_bytes(economy_before), "two public Mana projection ticks preserve all stock IDs/reservations/history and payment bytes except declared clocks")
|
||||
var final: Dictionary = runtime.create_snapshot()
|
||||
expect(runtime.restore_snapshot(final) and var_to_bytes(runtime.create_snapshot()) == var_to_bytes(final), "after power recovery whole14 snapshot strictly restores without implicit repair")
|
||||
_typed("final14", final)
|
||||
summary["warehouse_id"] = String(warehouse)
|
||||
summary["same_package_two_processes"] = mode == "read"
|
||||
summary["owned_plan_preserved"] = var_to_bytes(_plan()) == var_to_bytes(plan_before)
|
||||
summary["actual_power_available_after_recovery"] = recovered_power.power_available
|
||||
summary["persistent_suspension"] = String(recovered_power.suspension_reason)
|
||||
summary["auto_payment_count"] = _auto_payment_count()
|
||||
summary["automatic_purchase_notifications_after_load"] = purchase_notifications
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqccwfjhooq1t
|
||||
@@ -19,7 +19,7 @@ func _run() -> void:
|
||||
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"] == 13, "current Runtime emits schema v13")
|
||||
_expect(canonical_data[&"runtime_schema_version"] == 14, "current Runtime emits schema v14")
|
||||
_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"]
|
||||
@@ -35,7 +35,7 @@ func _run() -> void:
|
||||
_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 v9")
|
||||
var migrated: Dictionary = first_runtime.create_snapshot()
|
||||
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 13, "v6 migration re-emits current schema")
|
||||
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "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")
|
||||
|
||||
Reference in New Issue
Block a user