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)