Connect paid food purchasing, kitchen production, and porter deliveries

This commit is contained in:
Takeshi
2026-09-09 09:46:33 +09:00
parent 29a660aed0
commit 627b200f00
42 changed files with 3787 additions and 84 deletions
@@ -379,6 +379,7 @@ func _test_v3_migration_and_v4_tamper_guards() -> void:
var v3: Dictionary = _runtime.create_snapshot()
var v3_runtime: Dictionary = v3[&"systems"][&"runtime"]
v3_runtime[&"runtime_schema_version"] = 3
v3_runtime.erase(&"kitchen_production")
v3_runtime.erase(&"operations")
v3_runtime.erase(&"catalog_manifest")
v3_runtime.erase(&"progression")
@@ -395,10 +396,10 @@ func _test_v3_migration_and_v4_tamper_guards() -> void:
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 2, "v3 migration derives lifetime Guests from active plus persistent records")
_expect(_runtime.get_mvp_guest_definition_ids().size() == 32, "v3 migration registers the current 32 catalog definitions")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 6, "v3 migration emits canonical Runtime schema v6")
_expect(int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 7, "v3 migration emits canonical Runtime schema v7")
var digest := SimulationDigest.compute(migrated)
_expect(_runtime.restore_snapshot(migrated), "canonical v5 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "canonical v5 restore is exact")
_expect(_runtime.restore_snapshot(migrated), "canonical v7 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "canonical v7 restore is exact")
var progressed_legacy: Dictionary = v3.duplicate(true)
progressed_legacy[&"game_state"][&"world_tree_rank"] = 2
+23 -22
View File
@@ -1,7 +1,7 @@
extends SceneTree
const FOURTEEN_DAYS_MINUTES := 14 * SimulationKernel.MINUTES_PER_DAY
const RUNTIME_SNAPSHOT_VERSION := 6
const RUNTIME_SNAPSHOT_VERSION := 7
var _checks := 0
var _failures := 0
@@ -15,7 +15,7 @@ func _run() -> void:
print("MVP: production Operations acceptance")
_test_waiting_choice_and_command_exactly_once()
_test_twenty_four_hour_actual_tick_smoke()
_test_full_partial_payroll_bankruptcy_and_v6_tamper()
_test_full_partial_payroll_bankruptcy_and_v7_tamper()
_test_fourteen_day_boundary_snapshot_determinism()
await process_frame
if _failures == 0:
@@ -39,7 +39,7 @@ func _new_fixture(seed: int, enable_operations := true) -> Dictionary:
int(initial_snapshot[&"systems"][&"runtime"].get(
&"runtime_schema_version", -1
)) == RUNTIME_SNAPSHOT_VERSION,
"fixture emits Runtime snapshot schema v6"
"fixture emits Runtime snapshot schema v7"
)
if enable_operations:
var enabled: Dictionary = runtime.enable_production_operations()
@@ -100,6 +100,7 @@ func _advance_operations_boundary_slice(fixture: Dictionary, amount: int) -> int
func _align_operations_fixture_clocks(runtime: Variant, minute: int) -> void:
runtime.inventory_service.current_minute = minute
runtime.facility_service.current_minute = minute
runtime.kitchen_production.current_minute = minute
runtime.guest_service.current_minute = minute
runtime.staff_service.current_minute = minute
runtime.authority_service.current_minute = minute
@@ -207,7 +208,7 @@ func _test_waiting_choice_and_command_exactly_once() -> void:
var command_snapshot: Dictionary = runtime.create_snapshot()
_expect(
runtime.restore_snapshot(command_snapshot),
"command history survives an exact v6 snapshot round trip"
"command history survives an exact v7 snapshot round trip"
)
var restored_retry: CommandResult = runtime.submit_command(
&"change_resources",
@@ -247,12 +248,12 @@ func _test_twenty_four_hour_actual_tick_smoke() -> void:
_expect(
runtime.restore_snapshot(snapshot)
and runtime.latest_digest == SimulationDigest.compute(snapshot),
"24-hour actual-tick v6 snapshot round trip is exact"
"24-hour actual-tick v7 snapshot round trip is exact"
)
_free_fixture(fixture)
func _test_full_partial_payroll_bankruptcy_and_v6_tamper() -> void:
func _test_full_partial_payroll_bankruptcy_and_v7_tamper() -> void:
var fixture := _new_fixture(73_002, false)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
@@ -339,7 +340,7 @@ func _test_full_partial_payroll_bankruptcy_and_v6_tamper() -> void:
var terminal_digest := SimulationDigest.compute(terminal_snapshot)
_expect(
runtime.restore_snapshot(terminal_snapshot),
"BANKRUPT terminal v6 snapshot restores without clearing terminal state"
"BANKRUPT terminal v7 snapshot restores without clearing terminal state"
)
clock.advance_minutes(180)
_expect(
@@ -352,32 +353,32 @@ func _test_full_partial_payroll_bankruptcy_and_v6_tamper() -> void:
"BANKRUPT cannot be acknowledged away"
)
_assert_v6_tamper_rejected(
_assert_v7_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"operations"][&"personal_action_points"] = 99,
"v6 rejects an out-of-range Operations action-point total"
"v7 rejects an out-of-range Operations action-point total"
)
_assert_v6_tamper_rejected(
_assert_v7_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"next_command_id"] = int(
tampered[&"systems"][&"runtime"][&"next_command_id"]
) + 1,
"v6 rejects command high-water gaps"
"v7 rejects command high-water gaps"
)
_assert_v6_tamper_rejected(
_assert_v7_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
var accepted: Array = tampered[&"systems"][&"runtime"][&"command_queue"][&"accepted"]
if not accepted.is_empty():
accepted[0][&"idempotency_key"] = &"tampered.accepted.key",
"v6 rejects accepted-command/idempotency cross-link tampering"
"v7 rejects accepted-command/idempotency cross-link tampering"
)
_assert_v6_tamper_rejected(
_assert_v7_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
@@ -385,26 +386,26 @@ func _test_full_partial_payroll_bankruptcy_and_v6_tamper() -> void:
summaries[0][&"category_totals"][&"wages"] = int(
summaries[0][&"category_totals"][&"wages"]
) + 1,
"v6 rejects DaySummary/payroll wage reconciliation tampering"
"v7 rejects DaySummary/payroll wage reconciliation tampering"
)
_assert_v6_tamper_rejected(
_assert_v7_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"unexpected_v6_field"] = true,
"v6 rejects unknown Runtime fields"
tampered[&"systems"][&"runtime"][&"unexpected_v7_field"] = true,
"v7 rejects unknown Runtime fields"
)
_assert_v6_tamper_rejected(
_assert_v7_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"operations"][&"coordinator"][&"unexpected_field"] = true,
"v6 rejects unknown Coordinator fields"
"v7 rejects unknown Coordinator fields"
)
_free_fixture(fixture)
func _assert_v6_tamper_rejected(
func _assert_v7_tamper_rejected(
runtime: Variant,
canonical: Dictionary,
mutator: Callable,
@@ -453,7 +454,7 @@ func _test_fourteen_day_boundary_snapshot_determinism() -> void:
var resumed_clock: Variant = resumed[&"clock"]
var resumed_runtime: Variant = resumed[&"runtime"]
var restored: bool = resumed_runtime.restore_snapshot(midpoint)
_expect(restored, "fresh Runtime restores the enabled v6 midpoint snapshot")
_expect(restored, "fresh Runtime restores the enabled v7 midpoint snapshot")
var resumed_advanced := 0
if restored:
resumed_advanced = _advance_operations_boundary_slice(
@@ -57,7 +57,7 @@ func _test_disabled_compatibility_and_enable() -> void:
_expect(not disabled[&"enabled"], "Operations remain disabled for existing Runtime tests")
var disabled_snapshot: Dictionary = _runtime.create_snapshot()
var disabled_runtime: Dictionary = disabled_snapshot[&"systems"][&"runtime"]
_expect(int(disabled_runtime[&"runtime_schema_version"]) == 6, "Runtime snapshot advances to schema v6")
_expect(int(disabled_runtime[&"runtime_schema_version"]) == 7, "Runtime snapshot advances to schema v7")
_expect(
disabled_runtime[&"operations"] == {
&"wrapper_version": 1,
@@ -360,6 +360,7 @@ func _test_daily_payroll_financials_and_crosslink_tamper() -> void:
)
var downgraded: Dictionary = canonical.duplicate(true)
downgraded[&"systems"][&"runtime"][&"runtime_schema_version"] = 4
downgraded[&"systems"][&"runtime"].erase(&"kitchen_production")
_expect(
_rejects_atomically(downgraded),
"relabelled enabled v5 cannot discard Operations history through v4 migration"
@@ -1,6 +1,6 @@
extends SceneTree
const RUNTIME_SNAPSHOT_VERSION := 6
const RUNTIME_SNAPSHOT_VERSION := 7
const INFRASTRUCTURE_SNAPSHOT_KEYS: Array[StringName] = [
&"excavation",
&"spatial_subslots",
@@ -61,7 +61,7 @@ func _run() -> void:
_test_proxy_completion_projection_failure_is_atomic()
_test_vertical_preflight_rejection()
_test_demolition_projection_atomicity()
_test_v6_snapshot_strictness()
_test_current_snapshot_strictness()
_test_v5_proxy_migration_determinism()
_test_rank_expansion_preserves_route_union()
_finish()
@@ -112,12 +112,12 @@ func _test_runtime_surface(fixture: Dictionary) -> bool:
ready = ready and service != null
var snapshot: Dictionary = runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
var v6 := int(runtime_data.get(&"runtime_schema_version", -1)) == RUNTIME_SNAPSHOT_VERSION
_expect(v6, "Runtime emits infrastructure snapshot v6")
ready = ready and v6
var current_version := int(runtime_data.get(&"runtime_schema_version", -1)) == RUNTIME_SNAPSHOT_VERSION
_expect(current_version, "Runtime emits schema v7 with infrastructure state")
ready = ready and current_version
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
var valid_dictionary := runtime_data.get(key, null) is Dictionary
_expect(valid_dictionary, "v6 stores strict %s state" % key)
_expect(valid_dictionary, "current Runtime stores strict %s state" % key)
ready = ready and valid_dictionary
if not ready:
print(" PENDING: Runtime v6 infrastructure surface is not connected yet; behavioral cases skipped.")
@@ -290,7 +290,7 @@ func _test_excavation_tick_pause_resume_and_completion() -> void:
_expect(
runtime.restore_snapshot(paused_snapshot)
and runtime.create_snapshot() == paused_snapshot,
"paused excavation survives an exact Runtime v6 save/restore"
"paused excavation survives an exact current Runtime save/restore"
)
_expect(
runtime.excavation_service.get_job(excavation_job_id).get(&"state", &"")
@@ -731,7 +731,7 @@ func _test_demolition_projection_atomicity() -> void:
_free_fixture(fixture)
func _test_v6_snapshot_strictness() -> void:
func _test_current_snapshot_strictness() -> void:
var fixture := _new_fixture(81_060)
var runtime: Variant = fixture[&"runtime"]
_expect(_promote_rank_two(fixture, &"infra.snapshot.rank"), "snapshot fixture reaches Rank 2")
@@ -750,10 +750,10 @@ func _test_v6_snapshot_strictness() -> void:
_expect(built[&"ok"], "snapshot fixture stores one completed projection")
var snapshot: Dictionary = runtime.create_snapshot()
var expected_digest := SimulationDigest.compute(snapshot)
_expect(runtime.restore_snapshot(snapshot), "valid Runtime v6 snapshot restores")
_expect(runtime.restore_snapshot(snapshot), "valid Runtime v7 snapshot restores")
_expect(
runtime.create_snapshot() == snapshot and runtime.latest_digest == expected_digest,
"Runtime v6 round trip is exact"
"Runtime v7 round trip is exact"
)
_assert_runtime_restore_rejected(
@@ -761,7 +761,7 @@ func _test_v6_snapshot_strictness() -> void:
snapshot,
func(candidate: Dictionary) -> void:
candidate[&"systems"][&"runtime"][&"unknown_infrastructure"] = {},
"unknown v6 Runtime field"
"unknown v7 Runtime field"
)
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
_assert_runtime_restore_rejected(
@@ -837,6 +837,7 @@ func _test_v5_proxy_migration_determinism() -> void:
var legacy_runtime: Dictionary = legacy[&"systems"][&"runtime"]
var stripped_v6_events := _strip_v6_infrastructure_domain_history(legacy_runtime)
legacy_runtime[&"runtime_schema_version"] = 5
legacy_runtime.erase(&"kitchen_production")
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
legacy_runtime.erase(key)
legacy_runtime[&"path"] = baseline[&"runtime"].path_service.create_snapshot()
@@ -854,8 +855,8 @@ func _test_v5_proxy_migration_determinism() -> void:
_expect(first_runtime.restore_snapshot(legacy), "v5 proxy snapshot migrates through Runtime restore")
var first_migrated: Dictionary = first_runtime.create_snapshot()
_expect(
int(first_migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 6,
"v5 migration is immediately re-emitted as v6"
int(first_migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 7,
"v5 migration is immediately re-emitted as v7"
)
_expect(
first_runtime.facility_infrastructure_projector.has_projection(instance_id)
@@ -1056,6 +1057,8 @@ func _strip_v6_infrastructure_domain_history(runtime_data: Dictionary) -> int:
func _legacy_v5_infrastructure_shape_is_clean(runtime_data: Dictionary) -> bool:
if runtime_data.has(&"kitchen_production"):
return false
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
if runtime_data.has(key) or runtime_data.has(String(key)):
return false
+352
View File
@@ -0,0 +1,352 @@
extends SceneTree
const Catalog = preload("res://scripts/supply/supply_catalog.gd")
const KitchenService = preload("res://scripts/supply/kitchen_production_service.gd")
class RejectingOutputInventory:
extends InventoryService
var reject_output := true
func add_stack(owner: StringName, item: StringName, quantity: int, quality: int = 100,
expires_at: int = -1, created_at: int = -1, unit_weight: int = 1, stack_id: StringName = &"") -> Dictionary:
if reject_output and item == &"ingredient/basic_meal":
return {&"ok": false, &"code": &"OUTPUT_REJECTED"}
return super.add_stack(owner, item, quantity, quality, expires_at, created_at, unit_weight, stack_id)
var _checks := 0
var _failures := 0
var _registry: DefinitionRegistry
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P1: independent kitchen production and supply catalogue")
_registry = DefinitionRegistry.new()
_expect(_registry.rebuild()[&"valid"], "facility definitions load")
_test_catalog()
_test_batch_and_target()
_test_interruptions()
_test_expiry_and_retry()
_test_capacity()
_test_output_failure_rollback()
_test_snapshot_round_trip()
_test_snapshot_rejections()
_test_completed_batch_counter_guard()
_test_crosslinks_and_removal()
_test_plan_removal()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _fixture(inventory: InventoryService = null) -> Dictionary:
if inventory == null:
inventory = InventoryService.new()
var facilities := FacilityService.new()
var result := facilities.create_instance({
&"instance_id": &"kitchen.one", &"definition_id": &"kitchen",
&"local_inventory_id": &"kitchen.stock", &"staff_min": 1, &"staff_max": 3,
&"active_staff_ids": [&"cook"], &"base_quality": 55,
})
_expect(result.is_success(), "test kitchen registers")
var kitchen := KitchenService.new()
kitchen.configure(inventory, facilities, _registry, 0)
return {&"inventory": inventory, &"facilities": facilities, &"service": kitchen}
func _test_catalog() -> void:
var raw := Catalog.get_item(Catalog.RAW_FOOD)
_expect(raw[&"unit_price"] == 3 and raw[&"unit_weight"] == 1 and raw[&"shelf_life_minutes"] == 1440, "raw food has explicit P1 price, weight, and expiry")
_expect(raw[&"purchasable"], "raw food can be purchased")
var meal := Catalog.get_item(Catalog.BASIC_MEAL)
_expect(not meal[&"purchasable"] and meal[&"shelf_life_minutes"] == 720, "meals are produced and expire after 720 minutes")
raw[&"unit_price"] = 999
_expect(Catalog.get_item(Catalog.RAW_FOOD)[&"unit_price"] == 3, "catalogue callers cannot mutate global prices")
_expect(Catalog.get_item(&"unknown").is_empty(), "unknown items have no implicit free purchase")
_expect(Catalog.get_purchase_items().size() == 1, "only raw food is offered for purchase")
_expect(Catalog.storage_capacity(&"warehouse") == 100 and Catalog.storage_capacity(&"kitchen") == 40 and Catalog.storage_capacity(&"cafeteria") == 24, "owner storage capacities are distinct from service capacities")
_expect(Catalog.storage_capacity(&"unknown") == 0, "unsupported facilities have no supply storage")
var recipe := Catalog.get_recipe()
_expect(recipe[&"input_quantity"] == 4 and recipe[&"output_quantity"] == 4 and recipe[&"duration_minutes"] == 15, "recipe preserves existing facility tuning")
func _test_batch_and_target() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
_expect(kitchen.get_report(&"kitchen.one")[&"state"] == &"DISABLED", "kitchen waits for a player production plan")
_expect(not kitchen.set_plan(&"missing", true, 8)[&"ok"], "unknown facility plan is rejected")
for target: int in [0, 3, 5, 17]:
_expect(not kitchen.set_plan(&"kitchen.one", true, target)[&"ok"], "non-batch target %d is rejected" % target)
_expect(kitchen.set_plan(&"kitchen.one", true, 8)[&"ok"], "ordinary target enables production")
_expect(kitchen.get_report(&"kitchen.one")[&"state"] == &"NO_INPUT", "missing inputs are visible")
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 12, 90, 2000, 0)
_expect(kitchen.advance_one_minute(1)[&"events"][0][&"type"] == &"BATCH_STARTED", "production starts without guest or wallet")
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 12 and inventory.available_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 8, "start reserves without consuming")
_tick(kitchen, 14)
_expect(kitchen.get_report(&"kitchen.one")[&"progress_minutes"] == 14, "14 operating minutes do not complete 15-minute recipe")
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 0, "recipe output is not available prematurely")
var finished := kitchen.advance_one_minute(16)
_expect(finished[&"events"][0][&"type"] == &"BATCH_COMPLETED", "15 operating minutes complete batch")
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 8 and inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 4, "exact input consumption and output quantity")
for stack: Dictionary in inventory.get_stacks(&"kitchen.stock"):
if stack[&"item_id"] == Catalog.BASIC_MEAL:
_expect(stack[&"expires_at"] == 736 and stack[&"created_at"] == 16 and stack[&"quality"] == 55, "output captures completion time, recipe quality, and shelf life")
var snapshot := kitchen.create_snapshot()
_expect(kitchen.advance_one_minute(16)[&"replay"] and kitchen.create_snapshot() == snapshot, "duplicate minute cannot duplicate output")
_expect(not kitchen.advance_one_minute(18)[&"ok"] and kitchen.create_snapshot() == snapshot, "time jumps cannot invent operating time")
_tick(kitchen, 16)
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 8, "second batch reaches target")
_tick(kitchen, 3)
_expect(kitchen.get_report(&"kitchen.one")[&"state"] == &"TARGET_REACHED" and inventory.total_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 4, "target prevents speculative overproduction")
_expect(kitchen.validate_crosslinks(), "completed batches leave no orphan active holds")
_expect(inventory.validate_invariants()[&"ok"], "normal production preserves inventory invariants")
func _test_interruptions() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
var facilities: FacilityService = f[&"facilities"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 4, 100, 1000, 0)
kitchen.set_plan(&"kitchen.one", true, 4)
_tick(kitchen, 6)
_expect(kitchen.get_report(&"kitchen.one")[&"progress_minutes"] == 5, "partial progress exists before interruption")
var cases := [
{&"off": {&"active_staff_ids": []}, &"on": {&"active_staff_ids": [&"cook"]}, &"state": &"NO_STAFF"},
{&"off": {&"power_available": false}, &"on": {&"power_available": true}, &"state": &"NO_POWER"},
{&"off": {&"access_available": false}, &"on": {&"access_available": true}, &"state": &"NO_ACCESS"},
{&"off": {&"operator_open": false}, &"on": {&"operator_open": true}, &"state": &"CLOSED"},
{&"off": {&"durability": 0}, &"on": {&"durability": 10000}, &"state": &"BROKEN"},
]
for item: Dictionary in cases:
facilities.set_operational_status(&"kitchen.one", item[&"off"], kitchen.current_minute)
_tick(kitchen, 3)
var report := kitchen.get_report(&"kitchen.one")
_expect(report[&"state"] == item[&"state"] and report[&"progress_minutes"] == 5, "%s pauses without erasing work" % item[&"state"])
_expect(inventory.available_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 0, "interruption retains the batch's materials")
facilities.set_operational_status(&"kitchen.one", item[&"on"], kitchen.current_minute)
kitchen.set_plan(&"kitchen.one", false, 4)
_tick(kitchen, 2)
_expect(kitchen.get_report(&"kitchen.one")[&"progress_minutes"] == 5, "player pause also retains progress")
kitchen.set_plan(&"kitchen.one", true, 4)
_tick(kitchen, 10)
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 4, "resuming earns only missing operating minutes")
func _test_expiry_and_retry() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 2, 100, 5, 0)
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 6, 100, 1000, 0)
kitchen.set_plan(&"kitchen.one", true, 4)
_tick(kitchen, 2)
_expect(kitchen.get_report(&"kitchen.one")[&"active_batch"][&"reservation_ids"].size() == 2, "FEFO can reserve a batch across two stacks")
kitchen.set_plan(&"kitchen.one", false, 4)
_tick(kitchen, 2)
var failed := kitchen.advance_one_minute(5)
_expect(failed[&"events"][0][&"type"] == &"BATCH_FAILED", "input expiry fails a batch even while player paused it")
_expect(inventory.available_quantity(&"kitchen.stock", Catalog.RAW_FOOD, 5) == 6, "failure releases the still-fresh part of a split reservation")
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 0, "expired food cannot become fresh meals")
_expect(kitchen.validate_crosslinks(), "expired batch has no orphan holds")
kitchen.set_plan(&"kitchen.one", true, 4)
_expect(kitchen.advance_one_minute(6)[&"events"][0][&"type"] == &"BATCH_STARTED", "fresh input can retry on the following minute")
_tick(kitchen, 15)
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 4, "retry completes using fresh ingredients")
func _test_capacity() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 4, 100, 1000, 0)
inventory.add_stack(&"kitchen.stock", &"other", 40)
kitchen.set_plan(&"kitchen.one", true, 4)
_tick(kitchen, 16)
_expect(kitchen.get_report(&"kitchen.one")[&"state"] == &"OUTPUT_FULL", "overfull storage waits after cooking")
_expect(kitchen.get_report(&"kitchen.one")[&"progress_minutes"] == 15, "output wait retains completed work")
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 4 and inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 0, "full output never consumes input")
_tick(kitchen, 2)
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 4, "repeated output waits are non-destructive")
var removal := inventory.reserve_item(&"kitchen.stock", &"other", 4, &"test", &"remove")
inventory.consume_reservations(removal[&"reservation_ids"])
_tick(kitchen, 1)
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 4 and Catalog.inventory_weight(inventory, &"kitchen.stock") == 40, "a same-weight recipe can finish at exact storage capacity")
func _test_snapshot_round_trip() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 8, 90, 1000, 0)
kitchen.set_plan(&"kitchen.one", true, 8)
_tick(kitchen, 8)
var copy := _fixture()
var restored_inventory: InventoryService = copy[&"inventory"]
var restored: KitchenProductionService = copy[&"service"]
_expect(restored_inventory.restore_snapshot(inventory.create_snapshot()), "inventory round trip restores reserved input")
_expect(restored.restore_snapshot(kitchen.create_snapshot()), "production round trip restores against matching inventory")
_expect(restored.create_snapshot() == kitchen.create_snapshot() and restored.validate_crosslinks(), "plan and batch round trip exactly")
_tick(kitchen, 24)
_tick(restored, 24)
_expect(restored.create_snapshot() == kitchen.create_snapshot(), "restored production follows identical completion times")
_expect(restored_inventory.create_snapshot() == inventory.create_snapshot(), "restore cannot duplicate or lose ingredients and meals")
var public_report := restored.get_report(&"kitchen.one")
public_report[&"target_stock"] = 16
var public_plans := restored.get_plans()
public_plans[0][&"enabled"] = false
_expect(restored.get_plan(&"kitchen.one")[&"enabled"] and restored.get_plan(&"kitchen.one")[&"target_stock"] == 8, "reports are defensive copies")
func _test_output_failure_rollback() -> void:
var inventory := RejectingOutputInventory.new()
var f := _fixture(inventory)
var kitchen: KitchenProductionService = f[&"service"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 4, 100, 1000, 0)
kitchen.set_plan(&"kitchen.one", true, 4)
_tick(kitchen, 1)
var reserved_inventory := inventory.create_snapshot()
_tick(kitchen, 15)
_expect(inventory.create_snapshot() == reserved_inventory, "failed output insert rolls input consumption back atomically")
_expect(kitchen.get_report(&"kitchen.one")[&"progress_minutes"] == 15 and kitchen.validate_crosslinks(), "output failure keeps completed work and a valid input hold")
inventory.reject_output = false
_tick(kitchen, 1)
_expect(inventory.total_quantity(&"kitchen.stock", Catalog.RAW_FOOD) == 0 and inventory.total_quantity(&"kitchen.stock", Catalog.BASIC_MEAL) == 4, "recovered output inserts exactly one batch")
_expect(kitchen.validate_crosslinks() and inventory.validate_invariants()[&"ok"], "fault recovery preserves both service invariants")
func _test_snapshot_rejections() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 4, 100, 1000, 0)
kitchen.set_plan(&"kitchen.one", true, 8)
_tick(kitchen, 4)
var original := kitchen.create_snapshot()
var inventory_before := inventory.create_snapshot()
var mutations: Array[Dictionary] = []
for pair: Array in [[&"version", 2], [&"current_minute", -1], [&"current_minute", 4.0], [&"next_batch_sequence", 1], [&"plans", {}]]:
var malformed := original.duplicate(true)
malformed[pair[0]] = pair[1]
mutations.append(malformed)
for pair: Array in [[&"enabled", 1], [&"target_stock", 5], [&"target_stock", "8"], [&"facility_id", "missing"]]:
var malformed := original.duplicate(true)
malformed[&"plans"][0][pair[0]] = pair[1]
mutations.append(malformed)
for pair: Array in [[&"progress_minutes", 16], [&"progress_minutes", 4], [&"started_at", -1], [&"quality", 101], [&"batch_id", "kitchen.batch:00000000"], [&"inventory_id", "other"], [&"reservation_ids", []], [&"reservation_ids", ["missing"]]]:
var malformed := original.duplicate(true)
malformed[&"active_batches"][0][pair[0]] = pair[1]
mutations.append(malformed)
var duplicate_plan := original.duplicate(true)
duplicate_plan[&"plans"].append(duplicate_plan[&"plans"][0].duplicate(true))
mutations.append(duplicate_plan)
var duplicate_batch := original.duplicate(true)
duplicate_batch[&"active_batches"].append(duplicate_batch[&"active_batches"][0].duplicate(true))
mutations.append(duplicate_batch)
var duplicate_reservation := original.duplicate(true)
duplicate_reservation[&"active_batches"][0][&"reservation_ids"].append(duplicate_reservation[&"active_batches"][0][&"reservation_ids"][0])
mutations.append(duplicate_reservation)
var missing_batch := original.duplicate(true)
missing_batch[&"active_batches"] = []
mutations.append(missing_batch)
var unknown_field := original.duplicate(true)
unknown_field[&"arbitrary"] = true
mutations.append(unknown_field)
for i: int in range(mutations.size()):
_expect(not kitchen.restore_snapshot(mutations[i]) and not kitchen.last_restore_error.is_empty(), "malformed snapshot %d is rejected" % i)
_expect(kitchen.create_snapshot() == original and inventory.create_snapshot() == inventory_before, "snapshot rejection %d leaves both services intact" % i)
func _test_crosslinks_and_removal() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
var facilities: FacilityService = f[&"facilities"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 8, 100, 1000, 0)
kitchen.set_plan(&"kitchen.one", true, 8)
_tick(kitchen, 2)
var saved := kitchen.create_snapshot()
var id := StringName(saved[&"active_batches"][0][&"reservation_ids"][0])
var reservation := inventory.get_reservation(id)
reservation.requester_id = &"other.batch"
_expect(not kitchen.validate_crosslinks() and not kitchen.restore_snapshot(saved), "a different requester cannot steal a production reservation")
reservation.requester_id = StringName(saved[&"active_batches"][0][&"batch_id"])
reservation.quantity = 3
_expect(not kitchen.validate_crosslinks(), "partial ingredient allocation is rejected")
reservation.quantity = 4
var extra := inventory.reserve_item(&"kitchen.stock", Catalog.RAW_FOOD, 1, reservation.requester_id, Catalog.PRODUCTION_PURPOSE, 2)
_expect(not kitchen.validate_crosslinks() and not kitchen.restore_snapshot(saved), "orphan production reservations are rejected")
inventory.release_reservation(extra[&"reservation_ids"][0], &"", 2)
_expect(kitchen.validate_crosslinks(), "removing orphan restores valid links")
facilities.unregister_instance(&"kitchen.one")
var removed := kitchen.advance_one_minute(3)
_expect(removed[&"events"][0][&"reason"] == &"FACILITY_REMOVED", "removed kitchen fails its batch")
_expect(kitchen.get_plans().is_empty() and inventory.available_quantity(&"kitchen.stock", Catalog.RAW_FOOD, 3) == 8, "removal releases input holds and retires the plan")
_expect(kitchen.validate_crosslinks(), "facility removal leaves no active production orphan")
func _test_plan_removal() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
var facilities: FacilityService = f[&"facilities"]
_expect(kitchen.remove_plan(&"missing")[&"ok"], "removing an absent plan is idempotent")
kitchen.set_plan(&"kitchen.one", true, 4)
_expect(kitchen.remove_plan(&"kitchen.one")[&"reason"] == &"PLAN_ENABLED", "enabled plan cannot be forgotten")
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 4, 100, 1000, 0)
_tick(kitchen, 1)
kitchen.set_plan(&"kitchen.one", false, 4)
_expect(kitchen.remove_plan(&"kitchen.one")[&"reason"] == &"ACTIVE_BATCH", "paused batch prevents plan removal")
kitchen.set_plan(&"kitchen.one", true, 4)
_tick(kitchen, 15)
kitchen.set_plan(&"kitchen.one", false, 4)
facilities.unregister_instance(&"kitchen.one")
_expect(kitchen.remove_plan(&"kitchen.one")[&"reason"] == &"REMOVED", "disabled idle plan can be removed after facility demolition")
_expect(kitchen.get_plans().is_empty(), "removed plan is omitted from saved state")
_expect(kitchen.remove_plan(&"kitchen.one")[&"reason"] == &"NO_PLAN", "repeated removal does not recreate a plan")
func _test_completed_batch_counter_guard() -> void:
var f := _fixture()
var kitchen: KitchenProductionService = f[&"service"]
var inventory: InventoryService = f[&"inventory"]
inventory.add_stack(&"kitchen.stock", Catalog.RAW_FOOD, 8, 100, 1000, 0)
kitchen.set_plan(&"kitchen.one", true, 4)
_tick(kitchen, 16)
var canonical := kitchen.create_snapshot()
_expect(canonical[&"active_batches"].is_empty() and canonical[&"next_batch_sequence"] == 2, "completed batch retains its next sequence without an active batch")
var inventory_before := inventory.create_snapshot()
var reused := canonical.duplicate(true)
reused[&"next_batch_sequence"] = 1
_expect(not kitchen.restore_snapshot(reused), "closed input reservations forbid rewinding the batch counter")
_expect(kitchen.create_snapshot() == canonical and inventory.create_snapshot() == inventory_before, "counter rejection leaves service and inventory unchanged")
kitchen.set_plan(&"kitchen.one", false, 4)
kitchen.remove_plan(&"kitchen.one")
var without_plan := kitchen.create_snapshot()
without_plan[&"next_batch_sequence"] = 1
_expect(not kitchen.restore_snapshot(without_plan), "removed plans cannot erase durable batch identity evidence")
_expect(kitchen.validate_crosslinks(), "terminal reservations remain valid after plan removal")
kitchen.set_plan(&"kitchen.one", true, 8)
_tick(kitchen, 1)
_expect(kitchen.get_report(&"kitchen.one")[&"active_batch"][&"batch_id"] == "kitchen.batch:00000002", "the next actual batch uses a fresh identity")
func _tick(kitchen: KitchenProductionService, count: int) -> void:
for ignored: int in range(count):
var result := kitchen.advance_one_minute(kitchen.current_minute + 1)
if not result[&"ok"]:
_expect(false, "minute advance succeeds: %s" % result)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
+1
View File
@@ -0,0 +1 @@
uid://dpmobtia1rvj5
+9 -8
View File
@@ -1,6 +1,7 @@
extends SceneTree
const POST_V2_RUNTIME_KEYS := [
&"kitchen_production",
&"movement_credits",
&"catalog_manifest",
&"progression",
@@ -48,7 +49,7 @@ func _run() -> void:
_test_occupied_cell_graph_rebuild_is_rejected()
_test_stuck_guest_and_staff_recovery()
_test_event_recovery_path_transaction()
_test_v6_snapshot_crosslinks_and_legacy_v2()
_test_current_snapshot_crosslinks_and_legacy_v2()
_test_dispatcher_request_ownership_crosslinks()
_test_twenty_four_hour_projection_integrity()
_cleanup()
@@ -292,7 +293,7 @@ func _test_event_recovery_path_transaction() -> void:
)
func _test_v6_snapshot_crosslinks_and_legacy_v2() -> void:
func _test_current_snapshot_crosslinks_and_legacy_v2() -> void:
_reset_runtime(8_106)
var origin := Vector3i(-3, 0, 0)
_runtime.path_service.configure_cell_traffic(origin, 1, 0, false)
@@ -301,11 +302,11 @@ func _test_v6_snapshot_crosslinks_and_legacy_v2() -> void:
_clock.advance_one_minute()
var snapshot: Dictionary = _runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
_expect(int(runtime_data[&"runtime_schema_version"]) == 6, "Runtime snapshot advertises schema v6")
_expect(int(runtime_data[&"runtime_schema_version"]) == 7, "Runtime snapshot advertises schema v7")
_expect(not runtime_data[&"movement_credits"].is_empty(), "mid-move snapshot persists fixed-point movement credits")
var digest := SimulationDigest.compute(snapshot)
_expect(_runtime.restore_snapshot(snapshot), "valid mid-move v6 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "mid-move v6 save/load is exact")
_expect(_runtime.restore_snapshot(snapshot), "valid mid-move v7 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "mid-move v7 save/load is exact")
var corruptions: Array[Dictionary] = []
var bad_actor := snapshot.duplicate(true)
@@ -349,16 +350,16 @@ func _test_v6_snapshot_crosslinks_and_legacy_v2() -> void:
traffic[&"temporary_block"] = true
_expect(
_is_true_runtime_v2_shape(legacy_data),
"legacy fixture removes every v3-v6 aggregate and dedicated history instead of relabelling v6"
"legacy fixture removes every v3-v7 aggregate and dedicated history instead of relabelling v7"
)
_expect(_runtime.restore_snapshot(legacy_v2), "legacy Runtime v2 deterministically rebuilds Path actors and credits")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(
int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 6
int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 7
and migrated[&"systems"][&"runtime"][&"path"][&"actor_cells"].size() == 2
and migrated[&"systems"][&"runtime"][&"movement_credits"].size() == 2
and bool(_runtime.path_service.get_cell_traffic(origin)[&"temporary_block"]),
"legacy migration emits canonical v6 ownership while preserving blocked traffic state"
"legacy migration emits canonical v7 ownership while preserving blocked traffic state"
)
+1
View File
@@ -793,6 +793,7 @@ func _test_snapshot_replay_corruption_and_legacy_migration() -> void:
_reset_runtime(7_606, 9, 17)
var legacy: Dictionary = _runtime.create_snapshot()
legacy[&"systems"][&"runtime"].erase(&"runtime_schema_version")
legacy[&"systems"][&"runtime"].erase(&"kitchen_production")
legacy[&"systems"][&"runtime"].erase(&"operations")
legacy[&"systems"][&"runtime"].erase(&"defense")
legacy[&"systems"][&"runtime"][&"rng_streams"].erase("defense")
+202
View File
@@ -0,0 +1,202 @@
extends SceneTree
## A real inn from the default 5,000 G / 50 materials. No inserted guests,
## inventory, employees, completed construction, or free resource commands.
var _checks := 0
var _failures := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _ids: Dictionary = {}
var _saw_loaded := false
var _saw_movement := false
var _saw_batch := false
var _save_checked := false
var _last_porter_cell := Vector3i.ZERO
var _save_path := "user://tests/normal_supply_%d/cargo" % OS.get_process_id()
var _main: Control
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 424242)
_clock.set_paused(true)
root.size = Vector2i(1440, 810)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
_main.set("manual_save_path", "user://tests/supply_normal_%d/manual" % OS.get_process_id())
root.add_child(_main)
await process_frame
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(_state.get_resource(&"gold") == 5000 and _state.get_resource(&"materials") == 50, "ordinary starting resources")
for row: Array in [[&"reception", Vector3i(-8, 0, 0)], [&"cafeteria", Vector3i(2, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
if not _command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}):
_finish()
return
await _advance(180)
_command(&"build_facility", {&"definition_id": &"standard_room", &"origin": Vector3i(-8, 2, 0)})
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(-4, 1, 0)})
await _advance(60)
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(-4, 2, 0)})
await _advance(10)
for x: int in [-5, -6]:
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(x, 2, 0)})
await _advance(10)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
_ids[facility.definition_id] = facility.instance_id
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"], [&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
if not _command(&"hire_staff_candidate", {&"candidate_id": assignment[0]}) or not _command(&"set_staff_work_assignment", {&"staff_id": assignment[0], &"facility_id": _ids[assignment[1]], &"template_id": &"DAY"}):
_finish()
return
_expect(_state.get_resource(&"materials") == 4, "all buildings and the staff door fit within the starting material budget")
var warehouse: FacilityInstance = _runtime.facility_service.get_instance(_ids[&"warehouse"])
var cash_before: int = _state.get_resource(&"gold")
_command(&"purchase_supplies", {&"warehouse_id": warehouse.instance_id, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(_state.get_resource(&"gold") == cash_before - 120 and _runtime.inventory_service.total_quantity(warehouse.local_inventory_id, SupplyCatalog.RAW_FOOD) == 40, "40 raw food costs exactly 120 G and arrives only in the warehouse")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
await _advance(120)
_expect(_saw_movement and _saw_loaded, "an employed porter walks the real route and carries purchased food")
_expect(_saw_batch, "the kitchen starts autonomous production without a guest request")
_expect(_save_checked, "a mid-delivery disk save restores ownership and replays exactly")
_expect(_completed_deliveries(SupplyCatalog.RAW_FOOD) > 0 and _completed_deliveries(SupplyCatalog.BASIC_MEAL) > 0, "both warehouse-to-kitchen and kitchen-to-cafeteria deliveries complete")
_expect(_sales(&"cafeteria") > 0, "natural guests pay for meals made from purchased food")
await _capture()
if OS.get_environment("THE_TREE_SUPPLY_PREVIEW") == "1":
_start_preview()
return
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE_ONLY") == "1":
_finish()
return
for day: int in range(1, 4):
await _advance(360 + 1440 * day - _clock.absolute_minute)
var summaries: Array = _runtime.operations_coordinator.get_day_summaries()
_expect(summaries.size() == day and bool(summaries[-1].get(&"balanced", false)), "business day %d closes with a reconciled ledger" % day)
_expect(_state.get_resource(&"gold") >= 0, "day %d retains cash after real payroll and purchases" % day)
if day < 3:
_command(&"purchase_supplies", {&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(_sales(&"standard_room") > 0, "the same inn earns lodging revenue")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "three-day supply history remains restorable")
var evidence := {"checks": _checks, "failures": _failures, "meal_sales": _sales(&"cafeteria"), "lodging_sales": _sales(&"standard_room"), "raw_deliveries": _completed_deliveries(SupplyCatalog.RAW_FOOD), "meal_deliveries": _completed_deliveries(SupplyCatalog.BASIC_MEAL), "cash": _state.get_resource(&"gold"), "closes": _runtime.operations_coordinator.get_day_summaries(), "supply": _runtime.get_supply_management_report()}
var evidence_path := ProjectSettings.globalize_path("res://artifacts/supply-chain/normal-play-report.json")
DirAccess.make_dir_recursive_absolute(evidence_path.get_base_dir())
var file := FileAccess.open(evidence_path, FileAccess.WRITE)
if file != null:
file.store_string(JSON.stringify(evidence, "\t"))
print("NORMAL_SUPPLY_REPORT ", JSON.stringify({"cash": evidence["cash"], "meal_sales": evidence["meal_sales"], "raw_deliveries": evidence["raw_deliveries"], "meal_deliveries": evidence["meal_deliveries"]}))
_finish()
func _advance(minutes: int) -> void:
for index: int in maxi(minutes, 0):
var before: int = _clock.absolute_minute
_clock.advance_minutes(1)
if _clock.absolute_minute != before + 1:
_expect(false, "clock must progress: %s" % _runtime.get_operations_report())
return
var porter: StaffRuntime = _runtime.staff_service.get_staff(&"staff_gina")
if porter != null:
_saw_movement = _saw_movement or porter.current_cell != _last_porter_cell
_last_porter_cell = porter.current_cell
if not porter.carried_inventory_id.is_empty():
_saw_loaded = true
if not _save_checked:
_check_loaded_save()
if _ids.has(&"kitchen"):
_saw_batch = _saw_batch or not _runtime.kitchen_production.get_report(_ids[&"kitchen"]).get(&"active_batch", {}).is_empty()
if index % 120 == 119:
print("SUPPLY_PROGRESS ", _clock.formatted_time(), " meals=", _sales(&"cafeteria"))
await process_frame
func _check_loaded_save() -> void:
_save_checked = true
var saved: Dictionary = _runtime.save_to_path(_save_path)
_expect(bool(saved.get(&"ok", false)), "mid-delivery save writes to an isolated slot")
var before: Dictionary = _runtime.create_snapshot()
var loaded: Dictionary = _runtime.load_from_path(_save_path)
_expect(bool(loaded.get(&"ok", false)), "mid-delivery disk restore: %s" % loaded)
var normalized: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(5)
var expected: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(normalized), "mid-delivery checkpoint rewinds")
_clock.advance_minutes(5)
_expect(_runtime.create_snapshot() == expected, "mid-delivery replay preserves exact inventory, tasks, positions, and ledger")
_expect(_runtime.restore_snapshot(before), "resume the observed minute after replay verification")
func _completed_deliveries(item: StringName) -> int:
var count := 0
for request: LogisticsRequest in _runtime.logistics_requests.values():
if String(request.request_id).begins_with("supply.") and request.state == LogisticsRequest.State.COMPLETED and request.item_requirements.has(item):
count += 1
return count
func _sales(definition_id: StringName) -> int:
var total := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == _ids.get(definition_id, &"missing"):
total += entry.amount
return total
func _command(type: StringName, payload: Dictionary) -> bool:
var result: CommandResult = _runtime.submit_command(type, payload)
_expect(result.is_success(), "%s %s: %s" % [type, payload, result.message])
return result.is_success()
func _capture() -> void:
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE") != "1":
return
await process_frame
_main.call("_activate_management_mode", &"product")
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
for tab: StringName in [&"purchase", &"kitchen", &"delivery"]:
panel.select_tab(tab)
await process_frame
await RenderingServer.frame_post_draw
var path := ProjectSettings.globalize_path("res://artifacts/supply-chain/%s.png" % tab)
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
root.get_texture().get_image().save_png(path)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error("FAIL: " + message)
func _finish() -> void:
SaveService.delete_save(_save_path)
print("Normal supply chain: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
func _start_preview() -> void:
if _failures > 0:
_finish()
return
SaveService.delete_save(_save_path)
root.title = "The Tree · 仕入れと厨房の試遊(保存なし)"
for node: Node in _main.find_children("*", "Button", true, false):
var button := node as Button
for connection: Dictionary in button.pressed.get_connections():
var callback: Callable = connection[&"callable"]
if callback.get_method() in [&"_save_manual", &"_load_manual"]:
button.disabled = true
button.tooltip_text = "この試遊は保存しません。"
_main.call("_activate_management_mode", &"product")
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
panel.select_tab(&"kitchen")
_main.call("_set_status", "通常予算から育てた宿の試遊です。Spaceで再開し、商品タブで仕入れ・調理・配送を確認できます(保存なし)。", true, true)
_clock.set_paused(true)
print("SUPPLY_PREVIEW_READY checks=%d failures=0" % _checks)
@@ -0,0 +1 @@
uid://bb4vjqk7okwwa
@@ -0,0 +1,315 @@
extends SceneTree
## Real construction, hired porter and purchased stock; failure injection is
## limited to route closures, capacity pressure and expired cargo.
var _checks := 0
var _failures := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _source: StringName
var _destination: StringName
var _loaded: Dictionary
var _reserved: Dictionary
const REQUEST := &"supply.bridge_test"
const PORTER := &"staff_gina"
class LateFailInventory extends InventoryService:
var fail_delivery := true
func transfer_available(source_inventory_id: StringName, destination_inventory_id: StringName,
item_id: StringName, quantity: int, operation_id: StringName = &"", absolute_minute: int = -1) -> Dictionary:
var result := super.transfer_available(source_inventory_id, destination_inventory_id, item_id, quantity, operation_id, absolute_minute)
if fail_delivery and String(source_inventory_id).begins_with("cargo:") and result[&"ok"]:
return {&"ok": false, &"code": &"INJECTED_LATE_FAILURE"}
return result
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("Supply bridge: real movement, cargo conservation and recovery")
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_expect(_runtime.initialize(_clock, _state, 653322)[&"valid"], "Runtime initializes")
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [[&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
_expect(_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}), "build %s" % row[0])
_clock.advance_minutes(180)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse": _source = facility.instance_id
if facility.definition_id == &"kitchen": _destination = facility.instance_id
if _source.is_empty() or _destination.is_empty():
_expect(false, "normal construction completes")
_finish()
return
_expect(_command(&"hire_staff_candidate", {&"candidate_id": PORTER}), "hire the catalogue porter")
_expect(_command(&"set_staff_work_assignment", {&"staff_id": PORTER, &"facility_id": _source, &"template_id": &"DAY"}), "assign porter to a repeating real shift")
_expect(_command(&"purchase_supplies", {&"warehouse_id": _source, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8}), "purchase the source stock through the ledger")
_expect(_command(&"create_logistics_request", {&"request_id": REQUEST, &"source_inventory_id": _inventory(_source), &"destination_inventory_id": _inventory(_destination), &"item_requirements": {SupplyCatalog.RAW_FOOD: 8}}), "create the reserved supply request")
_expect(_quantity(_inventory(_destination)) == 0, "request creation does not deliver anything")
_test_manual_dispatch_boundary()
_reserved = _runtime.create_snapshot()
_clock.advance_minutes(1)
_expect(_request().assigned_carrier_id == PORTER and _worker().current_cell.distance_to(Vector3i.ZERO) <= 1.0, "dispatcher selects the actual employee without teleporting")
var start := _worker().current_cell
_wait_for(LogisticsRequest.State.DELIVERING, 40)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _worker().current_cell != start, "porter walks to the source before pickup")
if _request().state != LogisticsRequest.State.DELIVERING:
_finish()
return
_expect(_quantity(_inventory(_source)) == 0 and _quantity(_cargo()) == 8 and _quantity(_inventory(_destination)) == 0, "pickup moves all eight units into owned cargo only")
_expect(_crosslinks(), "loaded Request, Task, reservations and cargo agree")
_loaded = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(_loaded), "mid-delivery Runtime snapshot restores")
_test_ownership_and_replay()
_test_capacity_wait()
_test_route_recovery()
_test_unavailable_and_expiry()
_test_restore_guards()
_test_pickup_retry()
_test_unavailable_before_pickup()
_test_partial_expiry_return()
_test_shift_boundary()
_test_late_delivery_failure()
_finish()
func _test_ownership_and_replay() -> void:
var owned := _owned_snapshot()
var manual: CommandResult = _runtime.submit_command(&"advance_logistics_request", {&"request_id": REQUEST})
_expect(not manual.is_success() and manual.details.get(&"reason") == &"SUPPLY_DRIVER_REQUIRED", "external advance cannot bypass real arrival")
_expect(_owned_snapshot() == owned, "rejected manual advance leaves inventory, Task and carrier ownership unchanged")
_expect(_runtime.restore_snapshot(_loaded), "rewind the rejected command audit before deterministic replay")
var task_id := _worker().current_task_id
_expect(not _runtime.staff_service.cancel_task(task_id).is_success(), "ordinary cancellation cannot orphan loaded cargo")
_expect(not _runtime.staff_service.remove_staff(PORTER).is_success(), "loaded porter cannot be removed")
_expect(not _runtime.staff_service.retire_staff(PORTER).is_success() and not _worker().is_retiring, "retirement rejects atomically while hauling")
_expect(not _runtime.staff_service.progress_task(task_id, 1, _clock.absolute_minute + 2).is_success(), "generic Task progress cannot bypass delivery")
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_request().state == LogisticsRequest.State.COMPLETED and _quantity(_inventory(_destination)) == 8, "arrival transfers the purchased stock into kitchen storage")
_expect(_quantity(_cargo()) == 0 and _worker().current_task_id.is_empty() and _worker().carried_inventory_id.is_empty(), "completion clears cargo and employee ownership together")
_expect(_runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "completion releases exact dispatcher weight")
var completed: Dictionary = _runtime.create_snapshot()
var end_minute: int = _clock.absolute_minute
_expect(_runtime.restore_snapshot(_loaded), "rewind to a loaded employee")
_clock.advance_minutes(end_minute - _clock.absolute_minute)
_expect(_runtime.create_snapshot() == completed, "loaded save replay produces identical inventory, Tasks, positions and accounts")
func _test_manual_dispatch_boundary() -> void:
var owned := _owned_snapshot()
var manual: CommandResult = _runtime.submit_command(&"dispatch_logistics", {&"carriers": {&"synthetic_porter": {&"capacity": 100, &"current_load": 0, &"path_minutes": 0, &"role": &"logistics"}}})
_expect(manual.is_success() and manual.details[&"assignments"].is_empty(), "external synthetic carrier cannot claim a managed supply request")
_expect(_owned_snapshot() == owned, "manual dispatch does not change reserved stock, Staff Task or request ownership")
func _owned_snapshot() -> Dictionary:
return {&"inventory": _runtime.inventory_service.create_snapshot(), &"staff": _runtime.staff_service.create_snapshot(), &"dispatcher": _runtime.logistics_dispatcher.create_snapshot(), &"request": _request().create_snapshot()}
func _test_capacity_wait() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore capacity scenario")
_runtime.inventory_service.add_stack(_inventory(_destination), SupplyCatalog.BASIC_MEAL, 40, 100, -1, _clock.absolute_minute)
_clock.advance_minutes(30)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _request().state_reason == "DESTINATION_FULL", "capacity becoming full after pickup pauses delivery")
_expect(_quantity(_cargo()) == 8 and _runtime.logistics_dispatcher.get_reserved_load(PORTER) == 8, "full destination retains all cargo and weight ownership")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "waiting loaded cargo is restorable")
_runtime.inventory_service.transfer_available(_inventory(_destination), &"test.capacity_sink", SupplyCatalog.BASIC_MEAL, 40, &"", _clock.absolute_minute)
_wait_for(LogisticsRequest.State.COMPLETED, 10)
_expect(_quantity(_inventory(_destination)) == 8 and _quantity(_cargo()) == 0, "freeing storage resumes once without losing or duplicating stock")
func _test_route_recovery() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore route scenario")
var target := _task().target_cell
_runtime.path_service.set_cell_temporary_block(target, true)
_clock.advance_minutes(40)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _quantity(_cargo()) == 8, "blocked delivery route never releases carried stock")
_expect(_worker().current_task_id == _task().task_id and _crosslinks(), "stuck recovery keeps employee and Task linked")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "route-blocked cargo saves and restores")
_runtime.path_service.set_cell_temporary_block(target, false)
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_request().state == LogisticsRequest.State.COMPLETED and _quantity(_inventory(_destination)) == 8, "restoring the real route resumes delivery")
func _test_unavailable_and_expiry() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore unavailable scenario")
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
var cell := _worker().current_cell
_clock.advance_minutes(3)
_expect(_worker().current_cell == cell and _quantity(_cargo()) == 8, "incapacitated porter does not move or lose cargo")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "incapacitated cargo owner is restorable")
_worker().health = 100
_worker().transition_to(StaffRuntime.State.MOVING_TO_TASK, &"TEST_RECOVERED", _clock.absolute_minute)
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_request().state == LogisticsRequest.State.COMPLETED, "employee recovery resumes the retained delivery")
_expect(_runtime.restore_snapshot(_loaded), "restore spoilage scenario")
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
var inventory_snapshot: Dictionary = _runtime.inventory_service.create_snapshot()
for owner: Dictionary in inventory_snapshot[&"inventories"]:
if owner[&"owner_inventory_id"] == String(_cargo()):
for stack: Dictionary in owner[&"stacks"]:
stack[&"expires_at"] = _clock.absolute_minute + 1
_expect(_runtime.inventory_service.restore_snapshot(inventory_snapshot), "inject a cargo expiry boundary")
_clock.advance_minutes(1)
_expect(_request().state == LogisticsRequest.State.CANCELLED and _quantity(_cargo()) == 0, "expired cargo enters inventory waste and closes the impossible delivery")
_expect(_worker().state == StaffRuntime.State.INCAPACITATED and _worker().current_task_id.is_empty(), "spoilage cleanup preserves incapacitation while releasing the Task")
_expect(_runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "spoilage releases weight without a ghost assignment")
func _test_restore_guards() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore crosslink scenario")
var stable: Dictionary = _runtime.create_snapshot()
var bad := stable.duplicate(true)
for task: Dictionary in bad[&"systems"][&"runtime"][&"staff"][&"tasks"]:
if task[&"task_type"] == String(SupplyLogisticsBridge.DELIVERY_TYPE):
task[&"target_cell"] = [99, 99, 1]
_expect(not _runtime.restore_snapshot(bad) and _runtime.create_snapshot() == stable, "restore rejects a remote delivery target atomically")
var worker := _worker()
worker.carried_inventory_id = &"cargo:somebody_else"
_expect(not _crosslinks(), "crosslink validator rejects stolen cargo ownership")
worker.carried_inventory_id = _cargo()
var task := _task()
var saved_type := task.task_type
task.task_type = SupplyLogisticsBridge.PICKUP_TYPE
_expect(not _crosslinks(), "crosslink validator rejects pickup Task paired with loaded request")
task.task_type = saved_type
_expect(_crosslinks(), "valid ownership survives rejected candidates")
func _test_pickup_retry() -> void:
_expect(_runtime.restore_snapshot(_reserved), "restore before pickup dispatch")
_clock.advance_minutes(1)
var old_task := _worker().current_task_id
var stopped: Dictionary = _runtime.supply_logistics.handle_path_stuck(old_task, _clock.absolute_minute)
_expect(stopped[&"ok"] and _request().state == LogisticsRequest.State.BLOCKED, "unloaded route failure blocks the request and cancels the approach")
_expect(_runtime.inventory_service.available_quantity(_inventory(_source), SupplyCatalog.RAW_FOOD) == 8 and _worker().current_task_id.is_empty(), "failed approach releases source reservation and employee together")
_expect(_runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "failed approach retains no dispatcher load")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "blocked unloaded request restores")
_wait_for(LogisticsRequest.State.DELIVERING, 40)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _worker().current_task_id != old_task, "retries reserve stock again and create a fresh attempt Task")
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_expect(_quantity(_inventory(_destination)) == 8 and _quantity(_cargo()) == 0, "retry delivers the same stock exactly once")
func _test_unavailable_before_pickup() -> void:
_expect(_runtime.restore_snapshot(_reserved), "restore unloaded health failure")
_clock.advance_minutes(1)
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
_clock.advance_minutes(1)
_expect(_request().state == LogisticsRequest.State.BLOCKED and _worker().current_task_id.is_empty(), "incapacitation before pickup releases approach ownership")
_expect(_worker().state == StaffRuntime.State.INCAPACITATED and _runtime.inventory_service.available_quantity(_inventory(_source), SupplyCatalog.RAW_FOOD) == 8, "unloaded health failure preserves health state and releases reserved food")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()) and _crosslinks(), "unloaded health failure saves without expired reservation links")
func _test_partial_expiry_return() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore partial-spoilage scenario")
var inventory: Dictionary = _runtime.inventory_service.create_snapshot()
for owner: Dictionary in inventory[&"inventories"]:
if owner[&"owner_inventory_id"] == String(_cargo()):
owner[&"stacks"][0][&"quantity"] = 4
_expect(_runtime.inventory_service.restore_snapshot(inventory), "split loaded cargo into two expiry lots")
_runtime.inventory_service.add_stack(_cargo(), SupplyCatalog.RAW_FOOD, 4, 100, _clock.absolute_minute + 1, _clock.absolute_minute)
_clock.advance_minutes(1)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _task().task_type == SupplyLogisticsBridge.RETURN_TYPE and _quantity(_cargo()) == 4, "partial expiry retargets the surviving cargo to its source")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "returning cargo saves its destination in the existing Task")
_worker().health = 0
_worker().transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", _clock.absolute_minute)
_clock.advance_minutes(1)
_expect(_crosslinks(), "unavailability reason cannot erase return destination")
_worker().health = 100
_worker().transition_to(StaffRuntime.State.MOVING_TO_TASK, &"RECOVERED", _clock.absolute_minute)
_wait_for(LogisticsRequest.State.CANCELLED, 40)
_expect(_request().state_reason == "CARGO_EXPIRED_RETURNED" and _quantity(_inventory(_source)) == 4 and _quantity(_inventory(_destination)) == 0, "return unloads only unspoiled stock at the original warehouse")
_expect(_quantity(_cargo()) == 0 and _runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and _crosslinks(), "partial-expiry return releases all cargo ownership")
func _test_shift_boundary() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore end-of-shift scenario")
var target := _task().target_cell
_runtime.path_service.set_cell_temporary_block(target, true)
_clock.advance_minutes(1081 - _clock.absolute_minute)
_expect(_worker().is_overtime and not _worker().current_task_id.is_empty() and _quantity(_cargo()) == 8, "18:00 shift end retains loaded non-interruptible work")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "overtime delivery is restorable")
_runtime.path_service.set_cell_temporary_block(target, false)
_wait_for(LogisticsRequest.State.COMPLETED, 40)
_clock.advance_minutes(1)
_expect(_worker().state == StaffRuntime.State.OFF_DUTY and _worker().current_task_id.is_empty() and _quantity(_inventory(_destination)) == 8, "employee ends the shift after safe unloading")
func _test_late_delivery_failure() -> void:
_expect(_runtime.restore_snapshot(_loaded), "restore delivery transaction failure")
var fault := LateFailInventory.new()
_expect(fault.restore_snapshot(_runtime.inventory_service.create_snapshot()), "install failure after inventory transfer has already mutated")
_runtime.inventory_service = fault
_clock.advance_minutes(30)
_expect(_request().state == LogisticsRequest.State.DELIVERING and _request().state_reason == "DELIVERY_RETRY", "late inventory failure rolls the request back to owned delivery")
_expect(_quantity(_cargo()) == 8 and _quantity(_inventory(_destination)) == 0 and _runtime.logistics_dispatcher.get_reserved_load(PORTER) == 8, "failed delivery reverses all physical transfer and dispatcher changes")
_expect(_crosslinks() and not _worker().current_task_id.is_empty(), "failed delivery preserves the employee and Task pair")
fault.fail_delivery = false
_wait_for(LogisticsRequest.State.COMPLETED, 10)
_expect(_request().state == LogisticsRequest.State.COMPLETED and _quantity(_inventory(_destination)) == 8 and _quantity(_cargo()) == 0, "clearing transient failure retries the same operation exactly once")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "transaction retry leaves a restorable Runtime")
func _wait_for(state: int, minutes: int) -> void:
for index: int in minutes:
if _request().state == state: return
_clock.advance_minutes(1)
func _command(type: StringName, payload: Dictionary) -> bool:
var result: CommandResult = _runtime.submit_command(type, payload)
if not result.is_success(): print(result.message, result.details)
return result.is_success()
func _inventory(id: StringName) -> StringName:
return _runtime.facility_service.get_instance(id).local_inventory_id
func _request() -> LogisticsRequest:
return _runtime.logistics_requests[REQUEST]
func _worker() -> StaffRuntime:
return _runtime.staff_service.get_staff(PORTER)
func _task() -> StaffTask:
return _runtime.staff_service.get_task(_worker().current_task_id)
func _cargo() -> StringName:
return StringName("cargo:%s" % PORTER)
func _quantity(inventory_id: StringName) -> int:
return _runtime.inventory_service.total_quantity(inventory_id, SupplyCatalog.RAW_FOOD)
func _crosslinks() -> bool:
return SupplyLogisticsBridge.validate_crosslinks(_runtime.logistics_requests, _runtime.staff_service, _runtime.inventory_service, _runtime.facility_service)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _finish() -> void:
print("%s: %d checks / %d failures" % ["PASS" if _failures == 0 else "FAIL", _checks, _failures])
quit(_failures)
@@ -0,0 +1 @@
uid://dbj5dk0uqj0ay
@@ -0,0 +1,337 @@
extends SceneTree
## Runtime-level purchasing and replenishment. All physical stock is purchased
## and cooked normally; failure fixtures cover payment, closed routes and expiry.
class RejectingPaymentEconomy:
extends EconomyService
func transfer(_from: StringName, _to: StringName, _amount: int,
_minute: int = -1, _category: StringName = DEFAULT_CATEGORY,
_source: StringName = &"", _key: StringName = &"", _memo: StringName = &"",
_related: StringName = &"", _notify := true) -> CommandResult:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Injected payment refusal")
const PORTER := &"staff_gina"
const COOK := &"staff_chika"
const RETURN_REQUEST := &"supply.management_return"
var _checks := 0
var _failures := 0
var _completed_groups := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _ids: Dictionary = {}
var _built: Dictionary = {}
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_expect(_runtime.initialize(_clock, _state, 553221)[&"valid"], "Runtime initializes")
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [
[&"warehouse", Vector3i(-8, 0, 1)], [ &"kitchen", Vector3i(2, 0, 1)],
[&"cafeteria", Vector3i(2, 0, 0)], [&"staff_door", Vector3i(0, 0, 1)],
]:
_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]})
_clock.advance_minutes(180)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
_ids[facility.definition_id] = facility.instance_id
var completed := 0
for job: ConstructionJob in _runtime.construction_service.get_jobs():
if job.state == ConstructionJob.State.COMPLETED:
completed += 1
_expect(_ids.has(&"warehouse") and _ids.has(&"kitchen") and _ids.has(&"cafeteria") and completed == 4,
"normal starting budget completes three supply facilities and the staff door")
if not _ids.has(&"warehouse") or not _ids.has(&"kitchen") or not _ids.has(&"cafeteria"):
_finish()
return
_built = _runtime.create_snapshot()
_test_paid_purchase_and_rejection()
_test_disabled_kitchen_still_delivers()
_test_multiple_sources_choose_connected_nearest()
_test_returning_cargo_reserves_actual_space()
_expect(_completed_groups == 4, "all four regression scenarios reach their final assertions")
_finish()
func _test_paid_purchase_and_rejection() -> void:
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var quote: Dictionary = _runtime.preview_supply_purchase(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, 8)
_expect(bool(quote[&"ok"]), "unassigned completed warehouse accepts a paid purchase")
var result := _purchase(8)
_expect(result.is_success() and _quantity(&"warehouse", SupplyCatalog.RAW_FOOD) == 8,
"successful purchase creates exactly eight physical units")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash - int(quote[&"total_cost"])
and _state.get_resource(&"gold") == cash - int(quote[&"total_cost"]),
"ledger and player cash both reflect the exact purchase price")
var entries: Array = _runtime.economy_service.get_transaction_entries(StringName(result.details.get(&"transaction_id", &"")))
_expect(entries.size() == 1 and entries[0].amount == int(quote[&"total_cost"])
and entries[0].source_entity_id == _ids[&"warehouse"], "purchase records one expense attributed to its warehouse")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 0}, &"INVALID_QUANTITY")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 100}, &"STORAGE_FULL")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1000}, &"INSUFFICIENT_FUNDS")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.BASIC_MEAL, &"quantity": 1}, &"ITEM_NOT_PURCHASABLE")
_assert_purchase_rejected({&"warehouse_id": _ids[&"kitchen"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"NOT_WAREHOUSE")
_runtime.facility_service.set_operational_status(_ids[&"warehouse"], {&"operator_open": false}, _clock.absolute_minute)
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"FACILITY_CLOSED")
_runtime.facility_service.set_operational_status(_ids[&"warehouse"], {&"operator_open": true}, _clock.absolute_minute)
# Exercise the rollback after stock has been inserted, rather than only
# testing quote failures that reject before the first inventory write.
var original_economy: EconomyService = _runtime.economy_service
var rejecting := RejectingPaymentEconomy.new()
_expect(rejecting.restore_snapshot(original_economy.create_snapshot()), "payment-failure fixture retains all real balances")
_runtime.economy_service = rejecting
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"PAYMENT_FAILED")
_runtime.economy_service = original_economy
_completed_groups += 1
func _test_disabled_kitchen_still_delivers() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for production")
_hire_and_assign(PORTER, &"warehouse")
_hire_and_assign(COOK, &"kitchen")
_expect(_purchase(8).is_success(), "purchase ingredients for real production")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 4})
for minute: int in 100:
if _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) >= 4:
break
_clock.advance_minutes(1)
_expect(_quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) == 4, "paid food is transported and cooked into one real batch")
if _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) != 4:
return
_expect(_meal_requests().is_empty(), "production finishes before the following logistics tick creates a meal request")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": false, &"target_stock": 4})
# Preserve the legacy compatibility boundary: a kitchen with no explicit
# production plan must not suddenly become an automatic source.
_expect(_runtime.kitchen_production.remove_plan(_ids[&"kitchen"])[&"ok"], "completed disabled kitchen can model a legacy unconfigured source")
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_expect(_meal_requests().is_empty(), "unconfigured kitchen stock does not trigger unsolicited automatic delivery")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": false, &"target_stock": 4})
_clock.advance_minutes(1)
_expect(_meal_requests().size() == 1 and not _runtime.kitchen_production.get_plan(_ids[&"kitchen"])[&"enabled"],
"an explicitly disabled production plan still dispatches its completed meals")
_expect(_quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 0
and _runtime.inventory_service.available_quantity(_inventory(&"kitchen"), SupplyCatalog.BASIC_MEAL) == 0,
"meal request reserves stock without instantly moving it to the dining room")
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_expect(_meal_requests().size() == 1, "repeated replenishment cannot duplicate the already reserved meal shipment")
for minute: int in 100:
if _quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 4:
break
_clock.advance_minutes(1)
_expect(_quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 4
and _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) == 0,
"real porter completes the outgoing shipment while kitchen production remains off")
_completed_groups += 1
func _test_multiple_sources_choose_connected_nearest() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for competing suppliers")
_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(6, 0, 1)})
_clock.advance_minutes(180)
var near_id: StringName = &""
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse" and facility.instance_id != _ids[&"warehouse"]:
near_id = facility.instance_id
_expect(not near_id.is_empty(), "starting budget also completes a second, closer warehouse")
if near_id.is_empty():
return
_expect(_purchase(8).is_success(), "older distant supplier holds eight paid units")
_command(&"purchase_supplies", {&"warehouse_id": near_id, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8})
var near: FacilityInstance = _runtime.facility_service.get_instance(near_id)
var kitchen: FacilityInstance = _runtime.facility_service.get_instance(_ids[&"kitchen"])
var near_route: Dictionary = _runtime.supply_logistics.find_facility_access_path(Vector3i(6, 0, 1), kitchen)
var far_route: Dictionary = _runtime.supply_logistics.find_facility_access_path(Vector3i(-5, 0, 1), kitchen)
_expect(bool(near_route[&"reachable"]) and bool(far_route[&"reachable"])
and int(near_route[&"steps"]) < int(far_route[&"steps"])
and String(near_id) > String(_ids[&"warehouse"]),
"both actual routes connect, and the shorter route belongs to the later warehouse ID")
var stocked: Dictionary = _runtime.create_snapshot()
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
_clock.advance_minutes(1)
var requests := _raw_requests_for_kitchen()
_expect(requests.size() == 1 and int(requests[0].item_requirements[SupplyCatalog.RAW_FOOD]) == 4,
"two stocked suppliers create only one four-unit shipment in the first logistics tick")
_expect(not requests.is_empty() and requests[0].source_inventory_id == near.local_inventory_id,
"connected nearby supplier wins over the older, farther warehouse")
_expect(_runtime.inventory_service.available_quantity(near.local_inventory_id, SupplyCatalog.RAW_FOOD) == 4
and _runtime.inventory_service.available_quantity(_inventory(&"warehouse"), SupplyCatalog.RAW_FOOD) == 8
and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 4,
"exactly four units are reserved once and counted once as incoming")
_clock.advance_minutes(1)
requests = _raw_requests_for_kitchen()
_expect(requests.size() == 2 and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 8,
"next tick adds at most one batch to cover the remaining four-unit deficit")
_clock.advance_minutes(1)
_expect(_raw_requests_for_kitchen().size() == 2
and _runtime.inventory_service.available_quantity(_inventory(&"warehouse"), SupplyCatalog.RAW_FOOD) == 8,
"incoming stock at target stops extra requests and leaves other supplier stock untouched")
# Geometric proximity must not beat an actual connection. Closing every
# access cell of the near warehouse leaves the far route into the kitchen.
_expect(_runtime.restore_snapshot(stocked), "rewind to the two unreserved suppliers")
near = _runtime.facility_service.get_instance(near_id)
kitchen = _runtime.facility_service.get_instance(_ids[&"kitchen"])
for cell: Vector3i in near.occupied_cells:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
if _runtime.path_service.has_cell(cell + delta):
_runtime.path_service.set_cell_temporary_block(cell + delta, true)
far_route = _runtime.supply_logistics.find_facility_access_path(Vector3i(-5, 0, 1), kitchen)
near_route = _runtime.supply_logistics.find_facility_access_path(Vector3i(6, 0, 1), kitchen)
_expect(bool(far_route[&"reachable"]) and not bool(near_route[&"reachable"]),
"route closure really disconnects the nearby supplier while preserving the far route")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
_clock.advance_minutes(1)
requests = _raw_requests_for_kitchen()
_expect(requests.size() == 1 and requests[0].source_inventory_id == _inventory(&"warehouse")
and int(requests[0].item_requirements[SupplyCatalog.RAW_FOOD]) == 4,
"unreachable near stock cannot steal the single shipment from a connected supplier")
_completed_groups += 1
func _test_returning_cargo_reserves_actual_space() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for returning cargo")
_hire_and_assign(PORTER, &"warehouse")
_expect(_purchase(2).is_success() and _purchase(6).is_success(), "two paid stacks provide an eight-unit shipment")
_command(&"create_logistics_request", {
&"request_id": RETURN_REQUEST, &"source_inventory_id": _inventory(&"warehouse"),
&"destination_inventory_id": _inventory(&"kitchen"), &"item_requirements": {SupplyCatalog.RAW_FOOD: 8},
})
var request: LogisticsRequest = _runtime.logistics_requests[RETURN_REQUEST]
for minute: int in 60:
if request.state == LogisticsRequest.State.DELIVERING:
break
_clock.advance_minutes(1)
var cargo_id := StringName("cargo:%s" % PORTER)
_expect(request.state == LogisticsRequest.State.DELIVERING
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 8,
"actual pickup puts purchased goods in the employee's cargo")
if request.state != LogisticsRequest.State.DELIVERING:
return
var inventory_snapshot: Dictionary = _runtime.inventory_service.create_snapshot()
var injected := false
for inventory: Dictionary in inventory_snapshot[&"inventories"]:
if inventory[&"owner_inventory_id"] != String(cargo_id):
continue
for stack: Dictionary in inventory[&"stacks"]:
if int(stack[&"quantity"]) == 2:
stack[&"expires_at"] = _clock.absolute_minute + 1
injected = true
_expect(injected, "the original two-unit paid stack remains identifiable inside real cargo")
_expect(_runtime.inventory_service.restore_snapshot(inventory_snapshot), "inject expiry of only the two-unit paid stack")
_clock.advance_minutes(1)
_expect(request.state == LogisticsRequest.State.DELIVERING and request.state_reason == "RETURNING_EXPIRED_CARGO"
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 6,
"partial expiry starts a real return of the six surviving units")
if request.state != LogisticsRequest.State.DELIVERING or request.state_reason != "RETURNING_EXPIRED_CARGO":
return
for reason: String in ["RETURNING_EXPIRED_CARGO", "EXPIRED_CARGO_RETURN_BLOCKED", "CARRIER_UNAVAILABLE"]:
# Both bridge states retain the same real cargo. Capacity accounting
# must follow that cargo even if the return route becomes blocked.
request.state_reason = reason
var quote: Dictionary = _runtime.preview_supply_purchase(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, 94)
_expect(bool(quote[&"ok"]) and int(quote[&"max_quantity"]) == 94,
"%s reserves six actual returning units, not the original eight" % reason)
_expect(_incoming(&"warehouse", SupplyCatalog.RAW_FOOD) == 6 and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 0,
"%s counts returning goods only at the true receiving warehouse" % reason)
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 95}, &"STORAGE_FULL")
request.state_reason = "RETURNING_EXPIRED_CARGO"
_expect(_purchase(94).is_success(), "buyer may use the exact free space left after reserving the return")
for minute: int in 30:
if request.is_terminal():
break
_clock.advance_minutes(1)
_expect(request.state == LogisticsRequest.State.CANCELLED and request.state_reason == "CARGO_EXPIRED_RETURNED",
"capacity-aware purchase leaves enough room for the real return to complete")
_expect(_quantity(&"warehouse", SupplyCatalog.RAW_FOOD) == 100
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 0
and _incoming(&"warehouse", SupplyCatalog.RAW_FOOD) == 0,
"return fills warehouse exactly once and clears incoming cargo")
_expect(_runtime.inventory_service.validate_invariants()[&"ok"]
and SupplyLogisticsBridge.validate_crosslinks(_runtime.logistics_requests, _runtime.staff_service, _runtime.inventory_service, _runtime.facility_service),
"completed return preserves inventory and carrier ownership invariants")
_completed_groups += 1
func _assert_purchase_rejected(payload: Dictionary, reason: StringName) -> void:
var inventory_before: Dictionary = _runtime.inventory_service.create_snapshot()
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
var gold_before: int = _state.get_resource(&"gold")
var result: CommandResult = _runtime.submit_command(&"purchase_supplies", payload)
_expect(not result.is_success() and result.details.get(&"reason") == reason, "purchase rejects %s" % reason)
_expect(_runtime.inventory_service.create_snapshot() == inventory_before
and _runtime.economy_service.create_snapshot() == economy_before and _state.get_resource(&"gold") == gold_before,
"%s leaves full inventory, ledger and player cash unchanged" % reason)
func _command(type: StringName, payload: Dictionary) -> CommandResult:
var result: CommandResult = _runtime.submit_command(type, payload)
_expect(result.is_success(), "%s succeeds: %s" % [type, result.details])
return result
func _purchase(quantity: int) -> CommandResult:
return _runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": quantity})
func _hire_and_assign(staff_id: StringName, facility: StringName) -> void:
_command(&"hire_staff_candidate", {&"candidate_id": staff_id})
_command(&"set_staff_work_assignment", {&"staff_id": staff_id, &"facility_id": _ids[facility], &"template_id": &"DAY"})
func _inventory(definition: StringName) -> StringName:
return _runtime.facility_service.get_instance(_ids[definition]).local_inventory_id
func _quantity(definition: StringName, item: StringName) -> int:
return _runtime.inventory_service.total_quantity(_inventory(definition), item)
func _incoming(definition: StringName, item: StringName) -> int:
var report: Dictionary = _runtime.get_supply_management_report()
var group := &"warehouses" if definition == &"warehouse" else &"kitchens"
for row: Dictionary in report[group]:
if StringName(row[&"facility_id"]) != _ids[definition]:
continue
for stock: Dictionary in row[&"stock"]:
if StringName(stock[&"item_id"]) == item:
return int(stock[&"in_transit"])
return -1
func _meal_requests() -> Array[LogisticsRequest]:
var requests: Array[LogisticsRequest] = []
for request: LogisticsRequest in _runtime.logistics_requests.values():
if request.item_requirements.has(SupplyCatalog.BASIC_MEAL):
requests.append(request)
return requests
func _raw_requests_for_kitchen() -> Array[LogisticsRequest]:
var requests: Array[LogisticsRequest] = []
for request: LogisticsRequest in _runtime.logistics_requests.values():
if request.destination_inventory_id == _inventory(&"kitchen") and request.item_requirements.has(SupplyCatalog.RAW_FOOD):
requests.append(request)
return requests
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _finish() -> void:
print("Supply management service: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
@@ -0,0 +1 @@
uid://dmfdmeee0ev61
@@ -0,0 +1,92 @@
extends SceneTree
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P1: Runtime v7 supply save migration")
var source := _fixture(7301)
var first := _fixture(7302)
var second := _fixture(7303)
var source_runtime: Variant = source[&"runtime"]
var first_runtime: Variant = first[&"runtime"]
var second_runtime: Variant = second[&"runtime"]
source[&"clock"].advance_minutes(3)
var canonical: Dictionary = source_runtime.create_snapshot()
var canonical_data: Dictionary = canonical[&"systems"][&"runtime"]
_expect(canonical_data[&"runtime_schema_version"] == 7, "current Runtime emits schema v7")
_expect(canonical_data[&"kitchen_production"][&"current_minute"] == source[&"clock"].absolute_minute, "production clock follows Runtime clock without existing kitchens")
var legacy := canonical.duplicate(true)
var legacy_data: Dictionary = legacy[&"systems"][&"runtime"]
legacy_data[&"runtime_schema_version"] = 6
legacy_data.erase(&"kitchen_production")
_expect(legacy_data.has(&"facility_infrastructure") and legacy_data.has(&"route_projection"), "v6 fixture retains infrastructure introduced in v6")
_expect(first_runtime.restore_snapshot(legacy), "genuine v6 state restores into v7")
var migrated: Dictionary = first_runtime.create_snapshot()
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 7, "v6 migration re-emits current schema")
var kitchen: Dictionary = migrated[&"systems"][&"runtime"][&"kitchen_production"]
_expect(kitchen[&"plans"].is_empty() and kitchen[&"active_batches"].is_empty(), "legacy migration does not infer kitchen production plans or batches")
_expect(kitchen[&"current_minute"] == source[&"clock"].absolute_minute, "legacy production clock binds to the restored Runtime minute")
_expect(migrated == canonical, "v6 migration preserves non-supply state exactly")
_expect(second_runtime.restore_snapshot(legacy) and second_runtime.create_snapshot() == migrated, "v6 migration is deterministic across Runtime instances")
_expect(first_runtime.restore_snapshot(migrated) and first_runtime.create_snapshot() == migrated, "canonical v7 round trip is exact")
var missing_supply := canonical.duplicate(true)
missing_supply[&"systems"][&"runtime"].erase(&"kitchen_production")
_expect(_rejects_atomically(first_runtime, missing_supply), "v7 cannot omit its supply aggregate")
var relabelled := canonical.duplicate(true)
relabelled[&"systems"][&"runtime"][&"runtime_schema_version"] = 6
_expect(_rejects_atomically(first_runtime, relabelled), "relabelled v7 with retained kitchen state cannot masquerade as v6")
var bad_clock := canonical.duplicate(true)
bad_clock[&"systems"][&"runtime"][&"kitchen_production"][&"current_minute"] -= 1
_expect(_rejects_atomically(first_runtime, bad_clock), "production and Runtime clock disagreement is rejected")
var bad_version := canonical.duplicate(true)
bad_version[&"systems"][&"runtime"][&"kitchen_production"][&"version"] = 2
_expect(_rejects_atomically(first_runtime, bad_version), "unknown nested production schema is rejected")
var stock: Dictionary = source_runtime.inventory_service.add_stack(&"supplier.legacy", SupplyCatalog.RAW_FOOD, 4)
_expect(stock[&"ok"], "orphan migration fixture has raw food")
var hold: Dictionary = source_runtime.inventory_service.reserve_item(
&"supplier.legacy", SupplyCatalog.RAW_FOOD, 4,
&"kitchen.batch:00000001", SupplyCatalog.PRODUCTION_PURPOSE, source[&"clock"].absolute_minute
)
_expect(hold[&"ok"], "orphan fixture creates a production reservation")
var orphaned: Dictionary = source_runtime.create_snapshot()
orphaned[&"systems"][&"runtime"][&"runtime_schema_version"] = 6
orphaned[&"systems"][&"runtime"].erase(&"kitchen_production")
_expect(_rejects_atomically(first_runtime, orphaned), "legacy label cannot hide input holds for a discarded production batch")
for fixture: Dictionary in [source, first, second]:
fixture[&"runtime"].free()
fixture[&"clock"].free()
fixture[&"state"].free()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _fixture(seed: int) -> Dictionary:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
_expect(runtime.initialize(clock, state, seed)[&"valid"], "migration fixture initializes")
clock.set_paused(true)
return {&"clock": clock, &"state": state, &"runtime": runtime}
func _rejects_atomically(runtime: Variant, candidate: Dictionary) -> bool:
var before: Dictionary = runtime.create_snapshot()
return not runtime.restore_snapshot(candidate) and runtime.create_snapshot() == before
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
@@ -0,0 +1 @@
uid://t3p5aqdc4g5h
+242
View File
@@ -0,0 +1,242 @@
extends SceneTree
## Normal build/purchase commands: UI drafts, money/stock reconciliation, queued
## purchases and an isolated save slot. No free inventory is inserted.
var _checks := 0
var _failures := 0
var _main: Control
var _panel: SupplyManagementPanel
var _runtime: Variant
var _clock: Variant
var _warehouse: StringName = &""
var _kitchens: Array[StringName] = []
var _item: StringName = &""
var _save_path := "user://tests/supply_ui_%d/queued_purchase" % OS.get_process_id()
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_clock = root.get_node("GameClock")
_runtime = root.get_node("SimulationRuntime")
var state: Variant = root.get_node("GameState")
_clock.reset(1, 6, 0)
state.reset()
_runtime.initialize(_clock, state, 824242)
_clock.set_paused(true)
root.size = Vector2i(1280, 720)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
_main.set("manual_save_path", "user://tests/supply_ui_%d/manual" % OS.get_process_id())
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
await process_frame
await process_frame
_panel = _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
_expect(_panel != null, "normal scene includes supply management")
if _panel == null:
_finish()
return
(_main.get("navigation_buttons")[&"product"] as Button).pressed.emit()
_expect(_panel.visible and _panel.purchase_button.disabled, "product opens a safe empty state before any warehouse exists")
_expect("倉庫" in _panel.warehouse_stock_label.text, "empty purchase page explains the warehouse prerequisite")
for request: Array in [
[&"warehouse", Vector3i(-8, 0, 1)],
[&"kitchen", Vector3i(0, 0, 1)],
[&"kitchen", Vector3i(6, 0, 1)],
]:
var before: int = _runtime.construction_service.get_jobs().size()
_main.call("_select_build_definition", request[0])
_main.call("_on_map_cell_clicked", request[1])
_expect(_runtime.construction_service.get_jobs().size() == before + 1, "normal UI builds %s" % request[0])
_clock.advance_minutes(180)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse":
_warehouse = facility.instance_id
elif facility.definition_id == &"kitchen":
_kitchens.append(facility.instance_id)
_expect(not _warehouse.is_empty() and _kitchens.size() == 2, "paid construction produces a warehouse and two kitchens")
if _warehouse.is_empty() or _kitchens.size() != 2:
_finish()
return
_main.call("_activate_management_mode", &"product")
_panel.refresh()
_item = _panel.selected_item_id
_expect(not _item.is_empty(), "Runtime purchase catalog supplies a real item")
_test_purchase_and_failure()
_test_kitchen_drafts()
_test_saved_pending_purchase()
await _test_navigation_and_layout()
_finish()
func _test_purchase_and_failure() -> void:
_choose(_panel.warehouse_option, _warehouse)
_panel.quantity_spin.value = 4
var quote: Dictionary = _runtime.preview_supply_purchase(_warehouse, _item, 4)
_expect(bool(quote.get(&"ok", false)), "Runtime quotes a paid purchase for the completed warehouse")
var balance_before: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var stock_before := _stock()
_panel.purchase_button.pressed.emit()
_expect(_stock() == stock_before + 4, "purchase button credits the warehouse's actual inventory")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == balance_before - int(quote.get(&"total_cost", -1)),
"purchase pays exactly the authoritative quoted cost")
_expect("仕入れました" in _panel.result_label.text, "successful purchase is visible in Japanese")
# A deliberately oversized draft must fail without reducing cash or stock.
_panel.quantity_spin.allow_greater = true
_panel.quantity_spin.value = int(quote.get(&"max_quantity", 100)) + 100
var invalid_quantity := int(_panel.quantity_spin.value)
var balance_after: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var stock_after := _stock()
_expect(_panel.purchase_button.disabled, "invalid quoted quantity disables purchase before submission")
_panel.submit_purchase()
_expect(_stock() == stock_after and _runtime.economy_service.get_balance(&"PLAYER_CASH") == balance_after,
"rejected purchase never partially spends money or inserts stock")
_expect(int(_panel.quantity_spin.value) == invalid_quantity and not _panel.result_label.text.is_empty(),
"rejected purchase preserves the quantity and an actionable result")
_panel.quantity_spin.allow_greater = false
_panel.quantity_spin.value = 2
func _test_kitchen_drafts() -> void:
_panel.select_tab(&"kitchen")
_choose(_panel.kitchen_option, _kitchens[0])
_panel.production_enabled.button_pressed = true
_panel.target_spin.value = 4
_panel.select_tab(&"purchase")
_clock.advance_minutes(1)
_panel.select_tab(&"kitchen")
_expect(_panel.production_enabled.button_pressed and int(_panel.target_spin.value) == 4,
"time updates and tab changes preserve an unsaved kitchen draft")
_clock.set_paused(false)
_panel.production_button.pressed.emit()
_expect(_panel.production_button.disabled, "queued kitchen setting disables another submission for that kitchen")
_choose(_panel.kitchen_option, _kitchens[1])
_panel.production_enabled.button_pressed = true
_panel.target_spin.value = 8
_clock.advance_minutes(1)
_clock.set_paused(true)
_expect(_panel.selected_kitchen_id == _kitchens[1] and int(_panel.target_spin.value) == 8
and _panel.production_enabled.button_pressed, "another kitchen's completion preserves the selected kitchen's draft")
var configured := _kitchen_row(_kitchens[0])
_expect(bool(configured.get(&"enabled", false)) and int(configured.get(&"target_stock", -1)) == 4,
"saved kitchen settings reach the authoritative Runtime model")
_panel.production_button.pressed.emit()
var second := _kitchen_row(_kitchens[1])
_expect(bool(second.get(&"enabled", false)) and int(second.get(&"target_stock", -1)) == 8,
"second kitchen applies its own saved target")
_expect("職員" in _panel.kitchen_status_label.text or "経路" in _panel.kitchen_status_label.text,
"unready kitchen explains its authoritative staffing or route blocker in Japanese")
_panel.production_enabled.button_pressed = false
_panel.production_button.pressed.emit()
_expect(not bool(_kitchen_row(_kitchens[1]).get(&"enabled", true)), "automatic production can be stopped through the same controls")
func _test_saved_pending_purchase() -> void:
_panel.select_tab(&"purchase")
_panel.quantity_spin.value = 2
var stock_before := _stock()
var balance_before: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var quote: Dictionary = _runtime.preview_supply_purchase(_warehouse, _item, 2)
_clock.set_paused(false)
_panel.purchase_button.pressed.emit()
var pending_before: int = _runtime.command_queue.create_snapshot().get("pending", []).size()
_panel.submit_purchase()
_expect(pending_before == 1 and _runtime.command_queue.create_snapshot().get("pending", []).size() == 1,
"repeated purchase cannot enqueue the same draft twice")
var saved: Dictionary = _runtime.save_to_path(_save_path)
_expect(bool(saved.get(&"ok", false)), "queued purchase saves to an isolated test slot")
_clock.advance_minutes(1)
var loaded: Dictionary = _runtime.load_from_path(_save_path)
_expect(bool(loaded.get(&"ok", false)), "pending purchase reloads successfully")
_panel.refresh()
_expect(_panel.purchase_button.disabled, "restored pending purchase cannot be resubmitted")
_clock.advance_minutes(1)
_clock.set_paused(true)
_panel.refresh()
_expect(_stock() == stock_before + 2 and _runtime.economy_service.get_balance(&"PLAYER_CASH") == balance_before - int(quote.get(&"total_cost", -1)),
"restored purchase credits and charges exactly once")
_expect(not _panel.purchase_button.disabled, "completed restored command releases purchase controls")
var digest: String = _runtime.latest_digest
_panel.refresh()
_panel.select_tab(&"delivery")
_expect(_runtime.latest_digest == digest, "displaying stock and deliveries never mutates simulation")
_expect(not (_panel.get_report().get(&"deliveries", []) as Array).is_empty(),
"real warehouse stock creates visible kitchen replenishment requests")
_expect("倉庫" in _panel.delivery_label.text and "厨房" in _panel.delivery_label.text,
"delivery page resolves source and destination to identifiable facilities")
_expect("運搬員" in _panel.delivery_label.text and "経路" in _panel.delivery_label.text,
"waiting delivery explains the actual porter or route shortage in Japanese")
func _test_navigation_and_layout() -> void:
_main.call("_activate_management_mode", &"ledger")
_expect(not _panel.visible and (_main.get("operating_ledger_panel") as Control).visible, "ledger replaces supply controls")
_main.call("_unhandled_key_input", _key(KEY_H))
_expect(not _panel.visible and (_main.get("staff_candidate_panel") as Control).visible, "H returns to the hiring candidates")
_main.call("_activate_management_mode", &"product")
await process_frame
await process_frame
var side := _main.find_child("SidePanel", true, false) as Control
var guide := _main.find_child("InnOpeningGuide", true, false) as Control
_expect(not side.get_global_rect().intersects(guide.get_global_rect()), "1280x720 keeps supply inspector clear of opening guidance")
_expect(_panel.get_global_rect().position.x >= side.get_global_rect().position.x
and _panel.get_global_rect().end.x <= side.get_global_rect().end.x,
"supply tabs and inputs fit inside the existing sidebar width")
if OS.get_environment("THE_TREE_SUPPLY_UI_CAPTURE") == "1":
for tab: StringName in [&"purchase", &"kitchen", &"delivery"]:
_panel.select_tab(tab)
if tab == &"kitchen":
_choose(_panel.kitchen_option, _kitchens[0])
await process_frame
await RenderingServer.frame_post_draw
var captured := root.get_texture().get_image()
captured.save_png("/tmp/the-tree-supply-%s.png" % tab)
if tab == &"purchase":
captured.save_png("/tmp/the-tree-supply-management.png")
_main.call("_unhandled_key_input", _key(KEY_ESCAPE))
_expect(not _panel.visible and not bool(_main.get("_management_panel_open")), "Escape closes supply management and restores the map")
func _stock() -> int:
var facility: FacilityInstance = _runtime.facility_service.get_instance(_warehouse)
return _runtime.inventory_service.total_quantity(facility.local_inventory_id, _item)
func _kitchen_row(id: StringName) -> Dictionary:
for row: Dictionary in _runtime.get_supply_management_report().get(&"kitchens", []):
if StringName(row.get(&"facility_id", &"")) == id:
return row
return {}
func _choose(picker: OptionButton, id: StringName) -> void:
for index: int in picker.item_count:
if StringName(picker.get_item_metadata(index)) == id:
picker.select(index)
picker.item_selected.emit(index)
return
_expect(false, "requested facility appears in the relevant supply dropdown: %s" % id)
func _key(code: Key) -> InputEventKey:
var event := InputEventKey.new()
event.keycode = code
event.pressed = true
return event
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _finish() -> void:
SaveService.delete_save(_save_path)
if is_instance_valid(_main):
_main.queue_free()
print("Supply management UI: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
+1
View File
@@ -0,0 +1 @@
uid://f077idu4qluv