Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
212 lines
10 KiB
GDScript
212 lines
10 KiB
GDScript
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
|