Files
TheTree_mock/tests/s3/test_runtime_actor_hardening.gd
T
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
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.
2026-10-11 15:58:00 +09:00

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)