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.
419 lines
19 KiB
GDScript
419 lines
19 KiB
GDScript
extends SceneTree
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _runtime: Variant
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S3: runtime actor hardening regressions")
|
|
_test_service_affordability_is_atomic()
|
|
_test_guest_moves_one_cell_per_minute()
|
|
_test_unreachable_staff_task_is_not_assigned()
|
|
_test_shift_end_interrupts_and_fails_service()
|
|
_test_corrupt_cross_domain_snapshots_are_atomic()
|
|
_cleanup()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _reset_runtime(seed: int) -> void:
|
|
_cleanup()
|
|
_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 report: Dictionary = _runtime.initialize(_clock, _state, seed)
|
|
_clock.set_paused(true)
|
|
_expect(bool(report.get(&"valid", false)), "runtime initializes for hardening scenario %d" % seed)
|
|
|
|
|
|
func _cleanup() -> void:
|
|
if _runtime != null:
|
|
_runtime.free()
|
|
if _clock != null:
|
|
_clock.free()
|
|
if _state != null:
|
|
_state.free()
|
|
_runtime = null
|
|
_clock = null
|
|
_state = null
|
|
|
|
|
|
func _test_service_affordability_is_atomic() -> void:
|
|
_reset_runtime(3311)
|
|
var facility: FacilityInstance = _build_cafeteria()
|
|
if facility == null:
|
|
return
|
|
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 3, &"hardening.stock.affordability"), "affordability scenario stocks three meals")
|
|
_expect(_register_wallet(&"low_funds", 20, &"hardening.wallet.low"), "low-balance wallet registers")
|
|
var available_before: int = _runtime.inventory_service.available_quantity(
|
|
facility.local_inventory_id, &"ingredient/basic_meal"
|
|
)
|
|
var reservations_before: int = _runtime.inventory_service.get_reservations(true).size()
|
|
var low_result = _runtime.submit_command(
|
|
&"create_service_request",
|
|
{
|
|
&"request_id": &"request_low_funds",
|
|
&"facility_id": facility.instance_id,
|
|
&"guest_ids": [&"low_funds"],
|
|
&"service_type": &"default",
|
|
&"options": {&"quoted_price": 35},
|
|
},
|
|
&"low_funds", facility.instance_id, 10, -1, &"hardening.service.low"
|
|
)
|
|
_expect(not low_result.is_success(), "service request exceeding the wallet balance is rejected")
|
|
_expect(_runtime.facility_service.get_request(&"request_low_funds") == null, "rejected low-balance request leaves no Facility request")
|
|
_expect(_runtime.facility_service.get_queue(facility.instance_id).is_empty(), "rejected low-balance request leaves no queue entry")
|
|
_expect(
|
|
_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == available_before,
|
|
"rejected low-balance request does not reserve stock"
|
|
)
|
|
_expect(_runtime.inventory_service.get_reservations(true).size() == reservations_before, "rejected low-balance request creates no active Reservation")
|
|
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:low_funds") == 20, "rejected low-balance request leaves the wallet unchanged")
|
|
|
|
_expect(_register_wallet(&"committed_guest", 50, &"hardening.wallet.commitment"), "commitment-test wallet registers")
|
|
var first_result = _runtime.submit_command(
|
|
&"create_service_request",
|
|
{
|
|
&"request_id": &"request_commitment_a",
|
|
&"facility_id": facility.instance_id,
|
|
&"guest_ids": [&"committed_guest"],
|
|
&"service_type": &"default",
|
|
&"options": {&"quoted_price": 30},
|
|
},
|
|
&"committed_guest", facility.instance_id, 10, -1, &"hardening.service.commitment.a"
|
|
)
|
|
_expect(first_result.is_success(), "first affordable service commitment is accepted")
|
|
var available_after_first: int = _runtime.inventory_service.available_quantity(
|
|
facility.local_inventory_id, &"ingredient/basic_meal"
|
|
)
|
|
var active_after_first: int = _runtime.inventory_service.get_reservations(true).size()
|
|
var second_result = _runtime.submit_command(
|
|
&"create_service_request",
|
|
{
|
|
&"request_id": &"request_commitment_b",
|
|
&"facility_id": facility.instance_id,
|
|
&"guest_ids": [&"committed_guest"],
|
|
&"service_type": &"default",
|
|
&"options": {&"quoted_price": 25},
|
|
},
|
|
&"committed_guest", facility.instance_id, 10, -1, &"hardening.service.commitment.b"
|
|
)
|
|
_expect(not second_result.is_success(), "combined service commitments exceeding the wallet are rejected")
|
|
var queue: Array = _runtime.facility_service.get_queue(facility.instance_id)
|
|
_expect(queue.size() == 1 and queue[0].request_id == &"request_commitment_a", "over-commit rejection preserves only the first queue entry")
|
|
_expect(_runtime.facility_service.get_request(&"request_commitment_b") == null, "over-commit rejection leaves no second request")
|
|
_expect(
|
|
_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == available_after_first,
|
|
"over-commit rejection does not reserve another meal"
|
|
)
|
|
_expect(_runtime.inventory_service.get_reservations(true).size() == active_after_first, "over-commit rejection creates no additional Reservation")
|
|
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:committed_guest") == 50, "queued commitments do not debit the wallet prematurely")
|
|
|
|
|
|
func _test_guest_moves_one_cell_per_minute() -> void:
|
|
_reset_runtime(3312)
|
|
var facility: FacilityInstance = _build_cafeteria()
|
|
if facility == null:
|
|
return
|
|
var opened = _runtime.submit_command(
|
|
&"set_facility_conditions",
|
|
{&"facility_id": facility.instance_id, &"conditions": {&"active_staff_ids": [&"cook_projection", &"server_projection"]}},
|
|
&"manager", facility.instance_id, 10, -1, &"hardening.guest.open"
|
|
)
|
|
_expect(opened.is_success() and facility.state == FacilityInstance.State.OPEN, "projected staffing opens the movement target")
|
|
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 1, &"hardening.stock.path"), "movement target has a service input")
|
|
var spawn = _runtime.submit_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": &"path_guest",
|
|
&"guest_type_id": &"traveler",
|
|
&"gold": 100,
|
|
&"options": {
|
|
&"current_cell": Vector3i(8, 0, WorldService.LAYER_FRONT),
|
|
&"needs": {&"hunger": 100, &"sleep": 0, &"hygiene": 0, &"medical": 0, &"leisure": 0, &"intimacy": 0},
|
|
&"planned_exit_at": _clock.absolute_minute + 240,
|
|
},
|
|
},
|
|
&"world", &"path_guest", 10, -1, &"hardening.guest.spawn"
|
|
)
|
|
_expect(spawn.is_success(), "distant Guest spawns for path progression")
|
|
var guest: GuestRuntime = null
|
|
for _minute in range(8):
|
|
_clock.advance_one_minute()
|
|
guest = _runtime.guest_service.get_guest(&"path_guest")
|
|
if guest != null and guest.state == GuestRuntime.State.MOVING:
|
|
break
|
|
_expect(guest != null and guest.state == GuestRuntime.State.MOVING, "autonomous Guest selects the reachable cafeteria")
|
|
if guest == null or guest.state != GuestRuntime.State.MOVING:
|
|
return
|
|
var target_cell: Vector3i = guest.path_handle.get(&"target_cell", guest.current_cell)
|
|
var planned_path: Array = _runtime.path_service.find_path(guest.current_cell, target_cell)
|
|
_expect(planned_path.size() >= 4, "movement regression uses a path longer than one minute")
|
|
var first_cell := guest.current_cell
|
|
_clock.advance_one_minute()
|
|
guest = _runtime.guest_service.get_guest(&"path_guest")
|
|
var second_cell := guest.current_cell
|
|
_expect(_cell_distance(first_cell, second_cell) == 1, "Guest advances exactly one graph cell in one minute")
|
|
_expect(second_cell != target_cell, "Guest does not teleport to a distant target in one minute")
|
|
_clock.advance_one_minute()
|
|
guest = _runtime.guest_service.get_guest(&"path_guest")
|
|
_expect(_cell_distance(second_cell, guest.current_cell) == 1, "Guest continues along the path at one cell per minute")
|
|
|
|
|
|
func _test_unreachable_staff_task_is_not_assigned() -> void:
|
|
_reset_runtime(3313)
|
|
var registered = _runtime.submit_command(
|
|
&"register_staff",
|
|
{
|
|
&"staff_id": &"isolated_worker",
|
|
&"role_id": &"worker",
|
|
&"options": {
|
|
&"state": StaffRuntime.State.AVAILABLE,
|
|
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
&"role_tags": [&"worker"],
|
|
&"permitted_task_tags": [&"work"],
|
|
&"skills": {&"operation": 50},
|
|
},
|
|
},
|
|
&"manager", &"isolated_worker", 10, -1, &"hardening.staff.unreachable.register"
|
|
)
|
|
_expect(registered.is_success(), "available Staff registers for unreachable-task test")
|
|
var created = _runtime.submit_command(
|
|
&"create_staff_task",
|
|
{
|
|
&"task_id": &"unreachable_task",
|
|
&"task_type": &"inspect",
|
|
&"priority": 95,
|
|
&"options": {
|
|
&"target_cell": Vector3i(0, 5, WorldService.LAYER_FRONT),
|
|
&"required_role_tags": [&"worker"],
|
|
&"required_skill_min": {&"operation": 10},
|
|
&"task_tags": [&"work"],
|
|
&"estimated_minutes": 5,
|
|
},
|
|
},
|
|
&"manager", &"unreachable_task", 10, -1, &"hardening.staff.unreachable.task"
|
|
)
|
|
_expect(created.is_success(), "unreachable StaffTask registers")
|
|
_clock.advance_one_minute()
|
|
var task: StaffTask = _runtime.staff_service.get_task(&"unreachable_task")
|
|
var staff: StaffRuntime = _runtime.staff_service.get_staff(&"isolated_worker")
|
|
_expect(task != null and task.state == StaffTask.State.OPEN, "unreachable StaffTask remains OPEN")
|
|
_expect(task != null and task.assigned_staff_id.is_empty(), "unreachable StaffTask has no assigned Staff")
|
|
_expect(staff != null and staff.current_task_id.is_empty(), "unreachable assignment does not reserve the Staff member")
|
|
var unassigned: Array = _runtime.staff_service.last_dispatch_report.get(&"unassigned", [])
|
|
_expect(not unassigned.is_empty() and unassigned[0].get(&"reason", &"") == &"NO_ELIGIBLE_STAFF", "dispatcher reports unreachable work as having no eligible Staff")
|
|
_expect(_runtime.inventory_service.get_reservations(true).is_empty(), "unreachable work creates no inventory Reservation")
|
|
|
|
|
|
func _test_shift_end_interrupts_and_fails_service() -> void:
|
|
_reset_runtime(3314)
|
|
var facility: FacilityInstance = _build_cafeteria(true)
|
|
if facility == null:
|
|
return
|
|
for staff_id: StringName in [&"short_cook", &"short_server"]:
|
|
var registered = _runtime.submit_command(
|
|
&"register_staff",
|
|
{
|
|
&"staff_id": staff_id,
|
|
&"role_id": &"worker",
|
|
&"options": {
|
|
&"state": StaffRuntime.State.OFF_DUTY,
|
|
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
},
|
|
},
|
|
&"manager", staff_id, 10, -1, StringName("hardening.shift.staff.%s" % staff_id)
|
|
)
|
|
_expect(registered.is_success(), "short-shift Staff %s registers" % staff_id)
|
|
var start_day: int = _clock.absolute_minute / 1_440
|
|
var start_minute: int = posmod(_clock.absolute_minute, 1_440)
|
|
var end_minute: int = start_minute + 5
|
|
for staff_id: StringName in [&"short_cook", &"short_server"]:
|
|
var shifted = _runtime.submit_command(
|
|
&"create_staff_shift",
|
|
{
|
|
&"shift_id": StringName("shift_%s" % staff_id),
|
|
&"staff_id": staff_id,
|
|
&"template_id": &"FLEX",
|
|
&"start_day": start_day,
|
|
&"start_minute_of_day": start_minute,
|
|
&"end_minute_of_day": end_minute,
|
|
&"facility_id": facility.instance_id,
|
|
},
|
|
&"manager", staff_id, 10, -1, StringName("hardening.shift.create.%s" % staff_id)
|
|
)
|
|
_expect(shifted.is_success(), "short Shift for %s registers" % staff_id)
|
|
_clock.advance_one_minute()
|
|
_expect(facility.active_staff_ids == [&"short_cook", &"short_server"], "active short-shift Staff project into the Facility")
|
|
_expect(facility.state == FacilityInstance.State.OPEN, "short-shift staffing opens the Facility")
|
|
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 1, &"hardening.shift.stock"), "interruption scenario stocks one meal")
|
|
_expect(_register_wallet(&"shift_guest", 100, &"hardening.shift.wallet"), "service payer registers for Shift interruption")
|
|
var requested = _runtime.submit_command(
|
|
&"create_service_request",
|
|
{
|
|
&"request_id": &"shift_service_request",
|
|
&"facility_id": facility.instance_id,
|
|
&"guest_ids": [&"shift_guest"],
|
|
&"service_type": &"default",
|
|
&"options": {&"quoted_price": 35},
|
|
},
|
|
&"shift_guest", facility.instance_id, 10, -1, &"hardening.shift.service"
|
|
)
|
|
_expect(requested.is_success(), "service queues while short Shift is staffed")
|
|
_clock.advance_one_minute()
|
|
var active: Array = _runtime.facility_service.get_active_services(facility.instance_id)
|
|
_expect(active.size() == 1, "service starts before the short Shift ends")
|
|
if active.is_empty():
|
|
return
|
|
var service: ServiceInstance = active[0]
|
|
var service_id: StringName = service.service_id
|
|
var shift_end_absolute: int = start_day * 1_440 + end_minute
|
|
_clock.advance_minutes(maxi(shift_end_absolute - _clock.absolute_minute, 0))
|
|
_expect(facility.active_staff_ids.is_empty(), "Shift end removes all projected active Staff")
|
|
_expect(facility.state == FacilityInstance.State.NO_STAFF, "Shift end moves the Facility to NO_STAFF")
|
|
_clock.advance_one_minute()
|
|
service = _runtime.facility_service.get_service(service_id)
|
|
_expect(service != null and service.state == ServiceInstance.State.WAITING_FOR_STAFF, "active service waits for replacement Staff after Shift end")
|
|
var grace_deadline: int = service.staff_grace_deadline if service != null else _clock.absolute_minute
|
|
_expect(grace_deadline == _clock.absolute_minute + FacilityService.STAFF_REPLACEMENT_GRACE_MINUTES, "service records the deterministic Staff replacement grace deadline")
|
|
_clock.advance_minutes(maxi(grace_deadline - _clock.absolute_minute, 0))
|
|
service = _runtime.facility_service.get_service(service_id)
|
|
_expect(service != null and service.state == ServiceInstance.State.FAILED, "service fails when no replacement arrives before grace expiry")
|
|
_expect(service != null and service.failure_reason == FacilityService.INTERRUPTION_STAFF_UNAVAILABLE, "failed service preserves the Staff-unavailable reason")
|
|
_expect(_runtime.facility_service.get_active_services(facility.instance_id).is_empty(), "failed service leaves no active Facility slot")
|
|
_expect(_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == 1, "failed service releases its meal Reservation")
|
|
|
|
|
|
func _test_corrupt_cross_domain_snapshots_are_atomic() -> void:
|
|
_reset_runtime(3315)
|
|
var registered = _runtime.submit_command(
|
|
&"register_staff",
|
|
{
|
|
&"staff_id": &"snapshot_worker",
|
|
&"role_id": &"worker",
|
|
&"options": {
|
|
&"state": StaffRuntime.State.AVAILABLE,
|
|
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
&"role_tags": [&"worker"],
|
|
&"permitted_task_tags": [&"work"],
|
|
},
|
|
},
|
|
&"manager", &"snapshot_worker", 10, -1, &"hardening.snapshot.staff"
|
|
)
|
|
_expect(registered.is_success(), "snapshot Staff registers")
|
|
_expect(_stock(&"staff.supplies", &"tool/snapshot_kit", 1, &"hardening.snapshot.stock"), "snapshot task tool is stocked")
|
|
var created = _runtime.submit_command(
|
|
&"create_staff_task",
|
|
{
|
|
&"task_id": &"snapshot_task",
|
|
&"task_type": &"inspect",
|
|
&"priority": 90,
|
|
&"options": {
|
|
&"target_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
&"required_role_tags": [&"worker"],
|
|
&"required_items": {&"tool/snapshot_kit": 1},
|
|
&"task_tags": [&"work"],
|
|
&"estimated_minutes": 10,
|
|
},
|
|
},
|
|
&"manager", &"snapshot_task", 10, -1, &"hardening.snapshot.task"
|
|
)
|
|
_expect(created.is_success(), "snapshot StaffTask registers")
|
|
_clock.advance_one_minute()
|
|
var task: StaffTask = _runtime.staff_service.get_task(&"snapshot_task")
|
|
_expect(task != null and task.state == StaffTask.State.WORKING and task.reservation_ids.size() == 1, "snapshot captures a working Task with an active Reservation")
|
|
var valid_snapshot: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.restore_snapshot(valid_snapshot), "valid hardening snapshot restores before corruption checks")
|
|
|
|
var before_corrupt: Dictionary = _runtime.create_snapshot()
|
|
var before_digest: String = SimulationDigest.compute(before_corrupt)
|
|
var minute_corrupt: Dictionary = before_corrupt.duplicate(true)
|
|
var runtime_data: Dictionary = minute_corrupt[&"systems"][&"runtime"]
|
|
runtime_data[&"facilities"][&"current_minute"] = _clock.absolute_minute + 1
|
|
_expect(not _runtime.restore_snapshot(minute_corrupt), "snapshot with FacilityService minute diverging from the clock is rejected")
|
|
_expect(_runtime.create_snapshot() == before_corrupt, "minute-mismatch restore rejection is fully atomic")
|
|
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before_digest, "minute-mismatch rejection preserves the live digest")
|
|
|
|
var reservation_corrupt: Dictionary = before_corrupt.duplicate(true)
|
|
var task_entries: Array = reservation_corrupt[&"systems"][&"runtime"][&"staff"][&"tasks"]
|
|
for raw_task: Dictionary in task_entries:
|
|
if StringName(raw_task.get(&"task_id", "")) == &"snapshot_task":
|
|
raw_task[&"reservation_ids"] = ["reservation_missing"]
|
|
_expect(not _runtime.restore_snapshot(reservation_corrupt), "snapshot with a dangling StaffTask Reservation reference is rejected")
|
|
_expect(_runtime.create_snapshot() == before_corrupt, "dangling-Reservation restore rejection is fully atomic")
|
|
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before_digest, "dangling-Reservation rejection preserves the live digest")
|
|
|
|
|
|
func _build_cafeteria(physical_kitchen_staff: bool = false) -> FacilityInstance:
|
|
var build = _runtime.submit_command(
|
|
&"build_facility",
|
|
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT)},
|
|
&"player", &"", 50, -1, StringName("hardening.build.%d" % _clock.absolute_minute)
|
|
)
|
|
_expect(build.is_success(), "cafeteria construction starts")
|
|
_clock.advance_minutes(180)
|
|
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
|
|
_expect(facilities.size() == 1, "cafeteria construction completes")
|
|
if facilities.size() != 1:
|
|
return null
|
|
var facility_id := StringName(facilities[0].get(&"instance_id", ""))
|
|
var facility: FacilityInstance = _runtime.facility_service.get_instance(facility_id)
|
|
_expect(facility != null, "completed cafeteria registers with FacilityService")
|
|
if facility != null:
|
|
var kitchen_setup: Dictionary = load("res://tests/fixtures/required_kitchen_setup.gd").ensure(_runtime, _clock, facility.instance_id, physical_kitchen_staff)
|
|
_expect(bool(kitchen_setup.get(&"ok", false)), "public paid kitchen link uses scenario-selected physical or logical attendance")
|
|
return facility
|
|
|
|
|
|
func _stock(
|
|
owner_inventory_id: StringName,
|
|
item_id: StringName,
|
|
quantity: int,
|
|
idempotency_key: StringName
|
|
) -> bool:
|
|
var result = _runtime.submit_command(
|
|
&"add_inventory_stack",
|
|
{
|
|
&"owner_inventory_id": owner_inventory_id,
|
|
&"item_id": item_id,
|
|
&"quantity": quantity,
|
|
},
|
|
&"manager", owner_inventory_id, 10, -1, idempotency_key
|
|
)
|
|
return result.is_success()
|
|
|
|
|
|
func _register_wallet(guest_id: StringName, gold: int, idempotency_key: StringName) -> bool:
|
|
var result = _runtime.submit_command(
|
|
&"register_guest_wallet",
|
|
{&"guest_id": guest_id, &"gold": gold},
|
|
&"manager", guest_id, 10, -1, idempotency_key
|
|
)
|
|
return result.is_success()
|
|
|
|
|
|
func _cell_distance(a: Vector3i, b: Vector3i) -> int:
|
|
var delta := a - b
|
|
return absi(delta.x) + absi(delta.y) + absi(delta.z)
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|