313 lines
15 KiB
GDScript
313 lines
15 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()
|
|
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() == 6, "all simultaneous state reasons are retained")
|
|
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 after repair")
|
|
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)
|