Files
TheTree_mock/tests/s2/test_facility_service.gd
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

331 lines
17 KiB
GDScript

extends SceneTree
const FacilityInstanceType = preload("res://scripts/facility/facility_instance.gd")
const ServiceInstanceType = preload("res://scripts/facility/service_instance.gd")
const FacilityServiceType = preload("res://scripts/facility/facility_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: Facility Service")
_test_state_priority_and_opening_hours()
_test_policy_revision_and_request_snapshot()
_test_deterministic_queue_and_departure()
_test_capacity_progress_and_boundaries()
_test_interruption_and_recovery()
_test_determinism()
_test_snapshot_round_trip_is_atomic()
_test_deferred_queue_clock_contract()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_state_priority_and_opening_hours() -> void:
var facility := _facility(&"priority_room", 1, 4, 1, 2)
facility.set_operational_conditions({&"active_staff_ids": []}, 0)
_expect(facility.state == FacilityInstanceType.State.NO_STAFF, "staff shortage initially blocks opening")
facility.set_operational_conditions({
&"durability": 0,
&"access_available": false,
&"power_available": false,
&"operator_open": false,
&"active_staff_ids": [],
&"degraded_reasons": [&"DIRTY"],
}, 600)
_expect(facility.state == FacilityInstanceType.State.BROKEN, "BROKEN wins over every other state")
_expect(facility.state_reasons.size() == 7, "all simultaneous state reasons are retained")
facility = _facility(&"undamaged_room", 1, 4, 1, 2)
facility.set_operational_conditions({
&"durability": 5_000,
&"access_available": false,
&"power_available": false,
&"operator_open": false,
&"active_staff_ids": [],
&"degraded_reasons": [],
}, 600)
_expect(facility.state == FacilityInstanceType.State.NO_ACCESS, "NO_ACCESS wins in an undamaged facility")
facility.set_operational_conditions({&"access_available": true}, 600)
_expect(facility.state == FacilityInstanceType.State.NO_POWER, "NO_POWER wins over staffing and closure")
facility.set_operational_conditions({&"power_available": true}, 600)
_expect(facility.state == FacilityInstanceType.State.NO_STAFF, "NO_STAFF wins over manual closure")
facility.set_operational_conditions({&"active_staff_ids": [&"staff_a"]}, 600)
_expect(facility.state == FacilityInstanceType.State.CLOSED, "manual closure follows operational failures")
facility.set_operational_conditions({&"operator_open": true, &"durability": 2_999}, 600)
_expect(facility.state == FacilityInstanceType.State.DEGRADED, "low durability produces DEGRADED")
facility.set_operational_conditions({&"durability": 5_000}, 600)
_expect(facility.state == FacilityInstanceType.State.OPEN, "healthy staffed facility opens")
_expect(facility.set_opening_hours(600, 1_200), "valid opening hours are accepted")
facility.evaluate_state(500)
_expect(facility.state == FacilityInstanceType.State.CLOSED, "schedule closes facility before opening")
facility.evaluate_state(600)
_expect(facility.state == FacilityInstanceType.State.OPEN, "opening minute is inclusive")
facility.evaluate_state(1_200)
_expect(facility.state == FacilityInstanceType.State.CLOSED, "closing minute is exclusive")
_expect(facility.set_opening_hours(1_200, 0), "midnight close creates a valid overnight interval")
_expect(facility.is_within_opening_hours(1_300), "midnight-close schedule remains open late at night")
func _test_policy_revision_and_request_snapshot() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"policy_room", 1, 3, 0, 1)
_expect(service.register_instance(facility).is_success(), "facility instance registers")
_expect(not service.register_instance(facility).is_success(), "duplicate facility instance ID is rejected")
var changed = service.set_policy(facility.instance_id, {&"price_mode": &"standard"})
_expect(changed.is_success() and facility.policy_revision == 1, "immediate policy patch increments revision")
var request := service.create_request(
&"policy_request",
facility.instance_id,
[&"guest_a"],
&"meal",
100,
{&"quoted_price": 120}
)
_expect(service.enqueue_request(request, 100).is_success(), "policy-quoted request enters queue")
service.set_policy(facility.instance_id, {&"price_mode": &"expensive"})
_expect(request.policy_snapshot[&"price_mode"] == &"standard", "queued request retains original policy snapshot")
_expect(request.policy_revision == 1 and facility.policy_revision == 2, "request retains original policy revision")
var scheduled = service.set_policy(facility.instance_id, {&"risk_mode": true}, 120)
_expect(scheduled.is_success() and facility.policy_pending_revision == 3, "future policy receives pending revision")
service.advance_one_minute(119)
_expect(not facility.policy_current.has(&"risk_mode"), "pending policy does not apply early")
service.advance_one_minute(120)
_expect(facility.policy_current.get(&"risk_mode", false), "pending policy applies at exact minute")
_expect(facility.policy_revision == 3, "scheduled application increments current revision once")
func _test_deterministic_queue_and_departure() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"queue_room", 1, 3, 0, 2)
service.register_instance(facility)
var request_b := _request(service, facility, &"request_b", 10, 0, 2)
var request_a := _request(service, facility, &"request_a", 10, 0, 2)
var request_high := _request(service, facility, &"request_high", 11, 5, 2)
_expect(service.enqueue_request(request_b, 10).is_success(), "first request enters queue")
_expect(service.enqueue_request(request_a, 10).is_success(), "same-minute request enters queue")
_expect(service.enqueue_request(request_high, 11).is_success(), "later high-priority request enters queue")
_expect(
_queue_ids(service, facility.instance_id) == [&"request_high", &"request_a", &"request_b"],
"queue sorts by priority, arrival minute, then request ID"
)
_expect(request_high.expected_wait == 0, "high-priority request wait estimate excludes requests behind it")
_expect(not service.enqueue_request(request_a, 12).is_success(), "duplicate request ID is rejected permanently")
var overflow := _request(service, facility, &"request_overflow", 12, 0, 2)
_expect(not service.enqueue_request(overflow, 12).is_success(), "queue capacity rejects overflow")
_expect(service.leave_queue(facility.instance_id, &"request_a").is_success(), "guest can leave queue")
_expect(_queue_ids(service, facility.instance_id) == [&"request_high", &"request_b"], "queue departure removes entry immediately")
_expect(not service.enqueue_request(request_a, 13).is_success(), "departed request ID cannot be reused")
func _test_capacity_progress_and_boundaries() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"capacity_room", 2, 4, 0, 2)
service.register_instance(facility)
var starts: Array[StringName] = []
var completions: Array[StringName] = []
service.configure_callbacks(
func(request: ServiceRequest, _facility_instance: FacilityInstance, _minute: int) -> bool:
starts.append(request.request_id)
return true,
func(service_instance: ServiceInstance, _request_value: ServiceRequest, _facility_instance: FacilityInstance, _minute: int) -> bool:
completions.append(service_instance.service_id)
return true
)
for request_id: StringName in [&"request_c", &"request_a", &"request_b"]:
service.enqueue_request(_request(service, facility, request_id, 100, 0, 2), 100)
var filled := service.fill_available_slots(facility.instance_id, 100)
_expect(filled.size() == 2 and filled[0].is_success() and filled[1].is_success(), "capacity opens exactly two slots")
_expect(starts == [&"request_a", &"request_b"], "slot starts follow deterministic request-ID tie break")
_expect(facility.active_services.size() == 2, "two services occupy both slots")
_expect(not service.start_next_service(facility.instance_id, 100).is_success(), "full facility cannot start a third service")
service.advance_one_minute(101)
_expect(completions.is_empty(), "two-minute service does not complete after one tick")
service.advance_one_minute(102)
_expect(completions.size() == 2, "both occupied slots complete independently")
_expect(facility.active_services.size() == 1, "completion fills one freed slot from queue")
_expect(starts == [&"request_a", &"request_b", &"request_c"], "next queued request is promoted after completion")
var rejecting := FacilityServiceType.new()
var rejection_room := _facility(&"rejection_room", 1, 2, 0, 1)
rejecting.register_instance(rejection_room)
rejecting.configure_callbacks(
func(_request_value: ServiceRequest, _instance: FacilityInstance, _minute: int) -> bool:
return true,
func(_service_value: ServiceInstance, _request_value: ServiceRequest, _instance: FacilityInstance, _minute: int) -> String:
return "ledger transaction rejected"
)
rejecting.enqueue_request(_request(rejecting, rejection_room, &"atomic_request", 0, 0, 1), 0)
var started = rejecting.start_next_service(rejection_room.instance_id, 0)
var service_id: StringName = started.details[&"service_id"]
var results := rejecting.advance_one_minute(1)
_expect(results.any(func(result: CommandResult) -> bool: return not result.is_success()), "completion callback failure is surfaced")
_expect(rejection_room.active_services.has(service_id), "failed completion transaction leaves service active")
_expect(rejecting.get_request(&"atomic_request") != null, "failed completion transaction retains request")
func _test_interruption_and_recovery() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"interrupt_room", 1, 2, 0, 3)
service.register_instance(facility)
service.enqueue_request(_request(service, facility, &"interrupt_request", 0, 0, 3), 0)
var started = service.start_next_service(facility.instance_id, 0)
var service_id: StringName = started.details[&"service_id"]
service.set_operational_status(facility.instance_id, {&"power_available": false}, 1)
service.advance_one_minute(1)
var active := service.get_service(service_id)
_expect(active.state == ServiceInstanceType.State.PAUSED, "power loss safely pauses active service")
_expect(active.elapsed_minutes == 0, "paused service makes no progress")
service.set_operational_status(facility.instance_id, {&"power_available": true}, 2)
service.advance_one_minute(2)
_expect(active.state == ServiceInstanceType.State.ACTIVE and active.elapsed_minutes == 1, "power restoration resumes service")
service.interrupt_service(service_id, FacilityServiceType.INTERRUPTION_PLAYER_CLOSED, 2)
_expect(active.state == ServiceInstanceType.State.ACTIVE, "normal player closure lets active service finish")
service.set_operational_status(facility.instance_id, {&"durability": 0}, 3)
service.advance_one_minute(3)
_expect(active.state == ServiceInstanceType.State.FAILED, "facility breakage immediately fails active service")
_expect(facility.active_services.is_empty(), "failed service frees its capacity slot")
var grace_service := FacilityServiceType.new()
var grace_room := _facility(&"grace_room", 1, 1, 0, 3)
grace_service.register_instance(grace_room)
grace_service.enqueue_request(_request(grace_service, grace_room, &"grace_request", 10, 0, 3), 10)
var grace_started = grace_service.start_next_service(grace_room.instance_id, 10)
var grace_id: StringName = grace_started.details[&"service_id"]
grace_service.interrupt_service(grace_id, FacilityServiceType.INTERRUPTION_STAFF_UNAVAILABLE, 10)
grace_service.advance_one_minute(14)
_expect(grace_service.get_service(grace_id).state == ServiceInstanceType.State.WAITING_FOR_STAFF, "staff replacement waits through minute four")
grace_service.advance_one_minute(15)
_expect(grace_service.get_service(grace_id).state == ServiceInstanceType.State.FAILED, "staff replacement fails at five-minute deadline")
func _test_determinism() -> void:
var first := _deterministic_run()
var second := _deterministic_run()
_expect(first == second, "identical commands produce byte-equivalent facility snapshots")
func _test_snapshot_round_trip_is_atomic() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"save_room", 1, 3, 0, 4)
service.register_instance(facility)
service.set_policy(facility.instance_id, {&"mode": &"quiet"})
service.set_policy(facility.instance_id, {&"mode": &"busy"}, 500)
service.enqueue_request(_request(service, facility, &"active_request", 400, 2, 4), 400)
service.enqueue_request(_request(service, facility, &"queued_request", 401, 0, 4), 401)
service.start_next_service(facility.instance_id, 401)
service.advance_one_minute(402)
var snapshot := service.create_snapshot()
var restored := FacilityServiceType.new()
_expect(restored.restore_snapshot(snapshot), "valid facility snapshot restores")
_expect(restored.create_snapshot() == snapshot, "snapshot round trip preserves full facility state")
var before_invalid := restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"requests"].append(corrupt[&"requests"][0].duplicate(true))
_expect(not restored.restore_snapshot(corrupt), "duplicate request in snapshot is rejected")
_expect(restored.create_snapshot() == before_invalid, "rejected restore leaves live state untouched")
var corrupt_reference := snapshot.duplicate(true)
corrupt_reference[&"instances"][0][&"queue_entries"] = ["missing_request"]
_expect(not restored.restore_snapshot(corrupt_reference), "orphan queue reference is rejected")
_expect(restored.create_snapshot() == before_invalid, "orphan rejection is also atomic")
func _deterministic_run() -> Dictionary:
var service := FacilityServiceType.new()
var facility := _facility(&"deterministic_room", 2, 6, 0, 2)
service.register_instance(facility)
for request_data: Array in [
[&"request_d", 0],
[&"request_b", 1],
[&"request_c", 1],
[&"request_a", 0],
]:
var request_id: StringName = request_data[0]
var priority: int = request_data[1]
service.enqueue_request(_request(service, facility, request_id, 50, priority, 2), 50)
service.fill_available_slots(facility.instance_id, 50)
service.advance_one_minute(51)
service.advance_one_minute(52)
service.advance_one_minute(53)
return service.create_snapshot()
func _facility(
instance_id: StringName,
capacity: int,
queue_capacity: int,
staff_min: int,
service_minutes: int
) -> FacilityInstance:
return FacilityInstanceType.new().configure(
instance_id,
&"test_facility",
{
&"capacity": capacity,
&"queue_capacity": queue_capacity,
&"staff_min": staff_min,
&"staff_max": maxi(staff_min, 2),
&"active_staff_ids": [&"staff_a"] if staff_min > 0 else [],
&"base_service_minutes": service_minutes,
&"base_quality": 75,
&"base_price": 100,
}
)
func _request(
service: FacilityService,
facility: FacilityInstance,
request_id: StringName,
created_at: int,
priority: int,
service_minutes: int
) -> ServiceRequest:
return service.create_request(
request_id,
facility.instance_id,
[StringName("guest_%s" % request_id)],
&"test_service",
created_at,
{&"priority": priority, &"service_minutes": service_minutes}
)
func _queue_ids(service: FacilityService, instance_id: StringName) -> Array[StringName]:
var ids: Array[StringName] = []
for request: ServiceRequest in service.get_queue(instance_id):
ids.append(request.request_id)
return ids
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
func _test_deferred_queue_clock_contract() -> void:
var service := FacilityService.new()
var facility := FacilityInstance.new().configure(&"clock_contract", &"test_facility", {&"capacity": 1, &"queue_capacity": 8, &"staff_min": 0, &"staff_max": 2, &"base_service_minutes": 2, &"base_quality": 75, &"base_price": 100})
_expect(service.register_instance(facility).is_success(), "standalone fixture registers")
var request := service.create_request(&"default", facility.instance_id, [&"guest"], &"meal", 100)
_expect(service.enqueue_request(request, 100).is_success() and service.current_minute == 100 and request.enqueued_at == 100, "existing default API advances committed minute")
for minute: int in [-1, 99, 100, 102]:
var before := var_to_bytes(service.create_snapshot())
var invalid := service.create_request(StringName("invalid_%d" % minute), facility.instance_id, [&"guest"], &"meal", maxi(0, minute))
_expect(not service.enqueue_request(invalid, minute, false).is_success() and before == var_to_bytes(service.create_snapshot()), "deferred invalid minute %d is rejected without state mutation" % minute)
var deferred := service.create_request(&"deferred", facility.instance_id, [&"guest"], &"meal", 101)
_expect(service.enqueue_request(deferred, 101, false).is_success() and service.current_minute == 100 and deferred.created_at == 101 and deferred.enqueued_at == 101, "deferred registration preserves effective time without advancing committed time")
var before_duplicate := var_to_bytes(service.create_snapshot())
_expect(not service.enqueue_request(deferred, 101, false).is_success() and before_duplicate == var_to_bytes(service.create_snapshot()), "duplicate deferred registration is rejected atomically")