extends SceneTree var _checks := 0 var _failures := 0 var _runtime: Variant var _clock: Variant var _state: Variant var _cafe_id: StringName = &"" var _serial := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("Business closure: real tick order, grouped queues and scheduled policies") _test_last_patience_minute() _test_group_preflight_and_retry() _test_scheduled_closure_restore() _test_outside_opening_hours_rejects_new_request() _clear_fixture() print("Business closure: %d checks, %d failures" % [_checks, _failures]) quit(1 if _failures else 0) func _test_last_patience_minute() -> void: if not _fixture(8101): return _queue([&"last_minute"], &"last.queue", 1) var available := _available_meals() _expect(_policy({&"business_open": false}).is_success(), "close while a guest has only one patience minute") _clock.advance_minutes(1) var guest: GuestRuntime = _runtime.guest_service.get_guest(&"last_minute") _expect(guest != null and guest.patience_remaining == 1, "actual tick releases closed queue before consuming its final patience minute") _expect(guest.queue_request_id.is_empty(), "the guest no longer owns the closed request") _expect(_runtime.facility_service.get_request(&"last.queue") == null, "closed queue request is removed") _expect(_available_meals() == available + 1, "the finite meal reservation returns to cafeteria stock") _expect(_closure_events(&"last_minute") == 1, "exactly one closure redecision event is published") _expect(not _has_abandonment(&"last_minute"), "closure never publishes a patience-exhausted abandonment") func _test_group_preflight_and_retry() -> void: if not _fixture(8102): return _queue([&"group_a", &"group_b"], &"group.queue", 1) _expect(_policy({&"business_open": false}).is_success(), "close a two-guest queued request") var blocked_minute: int = _clock.absolute_minute + 1 var observations := {&"before": {}, &"after": {}, &"closure_callbacks": 0} _runtime.domain_events.subscribe(&"GUEST_STATE_CHANGED", &"test.business.closure", func(event: SimulationDomainEvent): if StringName(event.payload.get(&"reason", &"")) == &"FACILITY_CLOSED": observations[&"closure_callbacks"] += 1 ) # Consume one member's budget immediately before the real queue phase. This # reproduces a same-minute transition limit without moving any clock backwards. _clock.register_tick_handler(_clock.TickPhase.FACILITY_QUEUES, &"aa.business.budget", func(minute: int): if minute != blocked_minute: return var second: GuestRuntime = _runtime.guest_service.get_guest(&"group_b") second.transition_tick = minute second.transitions_this_tick = GuestService.MAX_TRANSITIONS_PER_TICK observations[&"before"] = _runtime.create_snapshot() ) _clock.register_tick_handler(_clock.TickPhase.FACILITY_QUEUES, &"zz.business.budget", func(minute: int): if minute == blocked_minute: observations[&"after"] = _runtime.create_snapshot() ) var available := _available_meals() _clock.advance_minutes(1) _expect(not observations[&"before"].is_empty() and observations[&"before"] == observations[&"after"], "group budget failure leaves the entire queue-phase snapshot unchanged") _expect(observations[&"closure_callbacks"] == 0, "no first-member state event escapes a rejected group release") _expect(_runtime.facility_service.get_queue(_cafe_id).size() == 1 and _available_meals() == available, "deferred group retains one request and its exact stock reservation") for id: StringName in [&"group_a", &"group_b"]: var waiting: GuestRuntime = _runtime.guest_service.get_guest(id) _expect(waiting.state == GuestRuntime.State.QUEUING and waiting.patience_remaining == 1, "deferred closed group preserves %s state and patience" % id) _clock.advance_minutes(1) _expect(observations[&"closure_callbacks"] == 2, "both members publish closure redecision on the next available minute") _expect(_runtime.facility_service.get_queue(_cafe_id).is_empty() and _available_meals() == available + 1, "group retry releases its shared request and inventory once") for id: StringName in [&"group_a", &"group_b"]: var released: GuestRuntime = _runtime.guest_service.get_guest(id) _expect(released != null and released.queue_request_id.is_empty() and released.patience_remaining == 1, "successful retry preserves %s patience" % id) _expect(not _has_abandonment(id), "group retry never abandons %s through a patience failure" % id) func _test_scheduled_closure_restore() -> void: if not _fixture(8103): return _queue([&"scheduled"], &"scheduled.queue", 3) var effective_at: int = _clock.absolute_minute + 1 _expect(_cmd(&"set_facility_policy", { &"facility_id": _cafe_id, &"patch": {&"business_open": false}, &"effective_at": effective_at, }).is_success(), "schedule closure at the next minute boundary") _expect(_runtime.facility_service.get_instance(_cafe_id).operator_open, "a future closure does not change current operator intent") var saved: Dictionary = _runtime.create_snapshot() _clock.advance_minutes(1) var guest: GuestRuntime = _runtime.guest_service.get_guest(&"scheduled") _expect(not _runtime.facility_service.get_instance(_cafe_id).operator_open, "scheduled closure applies in the facility phase") _expect(guest != null and guest.patience_remaining == 3 and guest.queue_request_id.is_empty(), "scheduled closure releases its waiting guest before patience advances") var completed: Dictionary = _runtime.create_snapshot() _expect(_runtime.restore_snapshot(saved), "pending closure plus reserved guest request restores") _clock.advance_minutes(1) _expect(_runtime.create_snapshot() == completed, "restored scheduled closure reproduces the exact state and event history") func _test_outside_opening_hours_rejects_new_request() -> void: if not _fixture(8104): return _spawn(&"before_open") _expect(_runtime.facility_service.set_opening_hours(_cafe_id, 600, 1_200, _clock.absolute_minute).is_success(), "set opening hours beginning after the current minute") var stock: Dictionary = _runtime.inventory_service.create_snapshot() var guests: Dictionary = _runtime.guest_service.create_snapshot() var facilities: Dictionary = _runtime.facility_service.create_snapshot() var result := _cmd(&"create_service_request", {&"facility_id": _cafe_id, &"request_id": &"outside.queue", &"guest_ids": [&"before_open"]}) _expect(not result.is_success() and result.details.get(&"reason", &"") == &"FACILITY_CLOSED", "outside-hours request is rejected as closed") _expect(_runtime.inventory_service.create_snapshot() == stock, "outside-hours rejection does not reserve inventory") _expect(_runtime.guest_service.create_snapshot() == guests and _runtime.facility_service.create_snapshot() == facilities, "outside-hours rejection cannot enqueue or change the guest") func _fixture(seed_value: int) -> bool: _clear_fixture() _clock = load("res://autoload/game_clock.gd").new() _state = load("res://autoload/game_state.gd").new() _runtime = load("res://autoload/simulation_runtime.gd").new() _clock.reset(1, 6, 0) _state.reset() var initialized: bool = _runtime.initialize(_clock, _state, seed_value).get(&"valid", false) _expect(initialized, "closure runtime initializes") if not initialized: return false _clock.set_paused(true) for row: Array in [[&"reception", Vector3i(-8, 0, 0)], [&"cafeteria", Vector3i(2, 0, 0)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]: _expect(_cmd(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}).is_success(), "closure fixture starts %s construction" % row[0]) _clock.advance_minutes(180) _cafe_id = &"" for facility: FacilityInstance in _runtime.facility_service.get_instances(): if facility.definition_id == &"cafeteria": _cafe_id = facility.instance_id _expect(not _cafe_id.is_empty(), "cafeteria construction completes through ordinary ticks") if _cafe_id.is_empty(): return false var kitchen_setup: Dictionary = load("res://tests/fixtures/required_kitchen_setup.gd").ensure(_runtime, _clock, _cafe_id) _expect(bool(kitchen_setup.get(&"ok", false)), "public paid actual BACK kitchen link with physical worker for closed queues") var cafe: FacilityInstance = _runtime.facility_service.get_instance(_cafe_id) _expect(_cmd(&"add_inventory_stack", {&"owner_inventory_id": cafe.local_inventory_id, &"item_id": SupplyCatalog.BASIC_MEAL, &"quantity": 4}).is_success(), "finite meal fixture is available to reserve") return true func _queue(guest_ids: Array[StringName], request_id: StringName, patience: int) -> void: for id: StringName in guest_ids: _spawn(id) _expect(_cmd(&"create_service_request", { &"facility_id": _cafe_id, &"request_id": request_id, &"guest_ids": guest_ids, &"options": {&"initial_patience": patience}, }).is_success(), "real guest actors own the queued meal reservation") func _spawn(guest_id: StringName) -> void: _expect(_cmd(&"spawn_guest", { &"guest_id": guest_id, &"guest_type_id": &"traveler", &"gold": 200, &"options": {&"current_cell": Vector3i.ZERO, &"planned_exit_at": _clock.absolute_minute + 240}, }).is_success(), "spawn closure guest %s" % guest_id) _runtime.guest_service.transition_guest(guest_id, GuestRuntime.State.ENTERING, _clock.absolute_minute) _runtime.guest_service.transition_guest(guest_id, GuestRuntime.State.DECIDING, _clock.absolute_minute) func _closure_events(guest_id: StringName) -> int: var count := 0 for event: SimulationDomainEvent in _runtime.domain_events.recent_events(): if event.entity_ids.has(guest_id) and StringName(event.payload.get(&"reason", &"")) == &"FACILITY_CLOSED": count += 1 return count func _has_abandonment(guest_id: StringName) -> bool: for event: SimulationDomainEvent in _runtime.domain_events.recent_events(): if not event.entity_ids.has(guest_id): continue if event.event_type == &"GUEST_QUEUE_ABANDONED" or StringName(event.payload.get(&"reason", &"")) == &"PATIENCE_EXHAUSTED": return true return false func _available_meals() -> int: var cafe: FacilityInstance = _runtime.facility_service.get_instance(_cafe_id) return _runtime.inventory_service.available_quantity(cafe.local_inventory_id, SupplyCatalog.BASIC_MEAL) func _policy(patch: Dictionary) -> CommandResult: return _cmd(&"set_facility_policy", {&"facility_id": _cafe_id, &"patch": patch}) func _cmd(type: StringName, payload: Dictionary) -> CommandResult: _serial += 1 return _runtime.submit_command(type, payload, &"player", &"", 75, -1, StringName("business.closure.%d" % _serial)) func _expect(ok: bool, message: String) -> void: _checks += 1 if not ok: _failures += 1 push_error(message) func _clear_fixture() -> void: if _runtime != null: _runtime.free() _clock.free() _state.free() _runtime = null _clock = null _state = null