Files
TheTree_mock/tests/s2/test_kitchen_production.gd
T
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
2026-09-30 23:52:24 +09:00

357 lines
21 KiB
GDScript

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"},
]
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")
facilities.set_operational_status(&"kitchen.one", {&"durability": 0}, kitchen.current_minute)
_expect(kitchen.get_report(&"kitchen.one")[&"state"] == &"BROKEN", "destroyed kitchen stops production")
facilities.set_operational_status(&"kitchen.one", {&"durability": 10000}, kitchen.current_minute)
_expect(kitchen.get_report(&"kitchen.one")[&"state"] == &"SUSPENDED", "repair does not invent suspension recovery")
_expect(not facilities.get_instance(&"kitchen.one").allows_logistics(), "suspended kitchen cannot supply deliveries")
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)