519 lines
26 KiB
GDScript
519 lines
26 KiB
GDScript
extends SceneTree
|
|
|
|
const PathServiceScript = preload("res://scripts/world/path_service.gd")
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _timing_samples: Array[int] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S1: congestion, elevator, and stuck recovery")
|
|
_test_cell_traffic_and_dynamic_route()
|
|
_test_cache_decrease_and_unblock_invalidation()
|
|
_test_query_timing_boundary()
|
|
_test_two_hundred_actor_chokepoint()
|
|
_test_stopped_elevator_fallback_and_no_route_reason()
|
|
_test_normal_connector_outage_snapshot_stability()
|
|
_test_bounded_elevator_queue_and_order()
|
|
_test_elevator_request_global_uniqueness()
|
|
_test_parallel_connector_access_cost()
|
|
_test_stuck_detection_and_explicit_recovery()
|
|
_test_actor_progress_cleanup_contract()
|
|
_test_relevant_no_route_diagnosis()
|
|
_test_snapshot_roundtrip_and_atomic_rejection()
|
|
_test_input_order_determinism()
|
|
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _test_cell_traffic_and_dynamic_route() -> void:
|
|
var service = PathServiceScript.new()
|
|
var cells := _diamond_cells()
|
|
_expect(service.rebuild_graph(cells), "traffic test graph rebuilds")
|
|
var start := Vector3i(0, 1, 0)
|
|
var goal := Vector3i(2, 1, 0)
|
|
var top_middle := Vector3i(1, 0, 0)
|
|
var bottom_middle := Vector3i(1, 2, 0)
|
|
var baseline: Array[Vector3i] = service.find_path(start, goal, service.FRONT)
|
|
_expect(baseline.has(top_middle), "equal-cost baseline uses the deterministic upper route")
|
|
_expect(
|
|
service.configure_cell_traffic(top_middle, 1, 25),
|
|
"per-cell capacity and dynamic cost are configurable"
|
|
)
|
|
for index: int in range(4):
|
|
_expect(
|
|
service.actor_enter_cell(StringName("crowd_%03d" % index), top_middle),
|
|
"soft collision admits crowd member %d" % index
|
|
)
|
|
var traffic := service.get_cell_traffic(top_middle)
|
|
_expect(int(traffic[&"base_capacity"]) == 1, "cell reports its base capacity")
|
|
_expect(int(traffic[&"current_density"]) == 4, "cell reports projected current density")
|
|
_expect(int(traffic[&"dynamic_cost"]) == 25, "cell reports configured dynamic cost")
|
|
_expect(service.get_congestion_cost(top_middle) == 225, "density excess costs 75 units per actor")
|
|
_expect(is_equal_approx(service.get_speed_multiplier(top_middle), 0.25), "soft collision speed clamps at 0.25")
|
|
var diverted: Array[Vector3i] = service.find_path(start, goal, service.FRONT)
|
|
_expect(diverted.has(bottom_middle), "A* avoids the dynamically congested route")
|
|
_expect(not diverted.has(top_middle), "locally invalidated cache never returns the stale congested route")
|
|
_expect(service.last_path_cost == 400, "reported route cost includes four orthogonal steps")
|
|
_expect(service.traffic_revision == 5, "one configuration and four entries advance traffic revision deterministically")
|
|
|
|
|
|
func _test_query_timing_boundary() -> void:
|
|
var service = PathServiceScript.new()
|
|
service.rebuild_graph([Vector3i(0, 0, 0), Vector3i(1, 0, 0)])
|
|
_timing_samples.clear()
|
|
service.path_query_measured.connect(Callable(self, "_capture_path_sample"))
|
|
service.find_path(Vector3i(0, 0, 0), Vector3i(1, 0, 0))
|
|
_expect(_timing_samples.size() == 1, "one public path query emits exactly one timing sample")
|
|
_expect(_timing_samples[0] >= 0, "path timing sample is non-negative")
|
|
service.find_path_report(Vector3i(0, 0, 0), Vector3i(1, 0, 0))
|
|
_expect(_timing_samples.size() == 2, "route report also emits exactly one sample through its single search")
|
|
|
|
|
|
func _test_cache_decrease_and_unblock_invalidation() -> void:
|
|
var service = PathServiceScript.new()
|
|
service.rebuild_graph(_diamond_cells())
|
|
var start := Vector3i(0, 1, 0)
|
|
var goal := Vector3i(2, 1, 0)
|
|
var top_middle := Vector3i(1, 0, 0)
|
|
var bottom_middle := Vector3i(1, 2, 0)
|
|
service.configure_cell_traffic(top_middle, 4, 500)
|
|
var expensive_route: Array[Vector3i] = service.find_path(start, goal)
|
|
_expect(expensive_route.has(bottom_middle), "high-cost upper alternative leaves lower route cached")
|
|
_expect(service.cached_path_count() == 1, "selected lower route is present in cache")
|
|
service.set_cell_dynamic_cost(top_middle, 0)
|
|
_expect(service.cached_path_count() == 0, "cost decrease broadly invalidates paths that did not touch the cheaper cell")
|
|
_expect(service.find_path(start, goal).has(top_middle), "query selects newly-cheaper non-cached alternative")
|
|
service.set_cell_temporary_block(top_middle, true)
|
|
_expect(service.find_path(start, goal).has(bottom_middle), "temporary block diverts route to lower path")
|
|
service.set_cell_temporary_block(top_middle, false)
|
|
_expect(service.cached_path_count() == 0, "block removal broadly invalidates the previously selected alternative")
|
|
_expect(service.find_path(start, goal).has(top_middle), "unblocked deterministic upper route becomes selectable immediately")
|
|
|
|
|
|
func _test_two_hundred_actor_chokepoint() -> void:
|
|
var service = PathServiceScript.new()
|
|
var entry := Vector3i(0, 0, 0)
|
|
var choke := Vector3i(1, 0, 0)
|
|
var exit_cell := Vector3i(2, 0, 0)
|
|
service.rebuild_graph([entry, choke, exit_cell])
|
|
service.configure_cell_traffic(choke, 4)
|
|
var started_usec := Time.get_ticks_usec()
|
|
var all_entered := true
|
|
for index: int in range(200):
|
|
all_entered = all_entered and service.actor_enter_cell(
|
|
StringName("guest_%03d" % index),
|
|
choke
|
|
)
|
|
var entry_elapsed_usec := Time.get_ticks_usec() - started_usec
|
|
_expect(all_entered, "all 200 actors can share the chokepoint without a hard-cap deadlock")
|
|
_expect(service.get_current_density(choke) == 200, "chokepoint density reaches 200 exactly")
|
|
_expect(service.get_congestion_cost(choke) == 14_700, "200-actor chokepoint congestion cost is exact")
|
|
_expect(is_equal_approx(service.get_speed_multiplier(choke), 0.25), "extreme density remains bounded by minimum speed")
|
|
var all_exited := true
|
|
for index: int in range(200):
|
|
var actor_id := StringName("guest_%03d" % index)
|
|
all_exited = all_exited and service.project_actor_position(actor_id, exit_cell)
|
|
all_exited = all_exited and service.actor_leave_cell(actor_id)
|
|
_expect(all_exited, "all 200 actors deterministically traverse and leave the chokepoint")
|
|
_expect(service.get_current_density(choke) == 0, "chokepoint density returns to zero")
|
|
_expect(service.get_current_density(exit_cell) == 0, "exit density returns to zero")
|
|
_expect(not service.actor_leave_cell(&"guest_000"), "duplicate leave is rejected")
|
|
_expect(service.get_current_density(choke) == 0, "rejected duplicate leave never creates negative density")
|
|
_expect(entry_elapsed_usec < 2_000_000, "200-actor admission completes within a two-second safety budget")
|
|
print(" i 200-actor admission: %d usec" % entry_elapsed_usec)
|
|
|
|
|
|
func _test_stopped_elevator_fallback_and_no_route_reason() -> void:
|
|
var service = PathServiceScript.new()
|
|
var front_start := Vector3i(0, 0, 0)
|
|
var front_fallback := Vector3i(1, 0, 0)
|
|
var back_goal := Vector3i(0, 0, 1)
|
|
var back_fallback := Vector3i(1, 0, 1)
|
|
service.rebuild_graph([front_start, front_fallback, back_goal, back_fallback])
|
|
_expect(
|
|
service.configure_elevator(&"main_lift", front_start, back_goal, {
|
|
&"cost": 100,
|
|
&"cabin_capacity": 4,
|
|
&"queue_capacity": 8,
|
|
}),
|
|
"main elevator configuration is accepted"
|
|
)
|
|
_expect(
|
|
service.configure_connector(
|
|
&"staff_stairs",
|
|
front_fallback,
|
|
back_fallback,
|
|
300,
|
|
true
|
|
),
|
|
"fallback stair connector is accepted"
|
|
)
|
|
var main_route: Array[Vector3i] = service.find_path(front_start, back_goal)
|
|
_expect(main_route == [front_start, back_goal], "operational elevator is selected while cheapest")
|
|
var revision_before_stop: int = service.graph_revision
|
|
_expect(
|
|
service.set_connector_operational_state(&"main_lift", service.CONNECTOR_STOPPED),
|
|
"elevator can enter stopped state"
|
|
)
|
|
_expect(service.graph_revision == revision_before_stop + 1, "connector state change advances graph revision")
|
|
var fallback_route: Array[Vector3i] = service.find_path(front_start, back_goal)
|
|
_expect(fallback_route.has(front_fallback), "stopped elevator triggers deterministic fallback routing")
|
|
_expect(fallback_route.has(back_fallback), "fallback route crosses the operational stairs")
|
|
_expect(not fallback_route.is_empty(), "stopped elevator does not make a reachable goal fail")
|
|
service.set_connector_operational_state(&"staff_stairs", service.CONNECTOR_OUTAGE)
|
|
var report := service.find_path_report(front_start, back_goal)
|
|
_expect(not bool(report[&"reachable"]), "route report explicitly marks unreachable goal")
|
|
_expect(report[&"reason"] == service.ROUTE_CONNECTOR_UNAVAILABLE, "no-route report names connector unavailability")
|
|
_expect(not service.last_error.is_empty(), "legacy last_error also carries an explicit no-route reason")
|
|
|
|
|
|
func _test_bounded_elevator_queue_and_order() -> void:
|
|
var service = PathServiceScript.new()
|
|
var from_cell := Vector3i(0, 0, 0)
|
|
var to_cell := Vector3i(0, 0, 1)
|
|
service.rebuild_graph([from_cell, to_cell])
|
|
service.configure_elevator(&"lift", from_cell, to_cell, {
|
|
&"cabin_capacity": 2,
|
|
&"queue_capacity": 3,
|
|
})
|
|
var normal_b := _request(&"r_normal_b", &"unit_b", 0, 1, &"NORMAL", 10)
|
|
var vip := _request(&"r_vip", &"unit_z", 0, 1, &"VIP", 10)
|
|
var normal_a := _request(&"r_normal_a", &"unit_a", 0, 1, &"NORMAL", 10)
|
|
_expect(bool(service.enqueue_elevator_request(&"lift", normal_b)[&"ok"]), "first request enters bounded queue")
|
|
_expect(bool(service.enqueue_elevator_request(&"lift", vip)[&"ok"]), "second request enters bounded queue")
|
|
_expect(bool(service.enqueue_elevator_request(&"lift", normal_a)[&"ok"]), "third request enters bounded queue")
|
|
var overflow := service.enqueue_elevator_request(
|
|
&"lift",
|
|
_request(&"r_overflow", &"unit_x", 0, 1, &"RESCUE", 10)
|
|
)
|
|
_expect(not bool(overflow[&"ok"]) and overflow[&"reason"] == &"QUEUE_FULL", "fourth request is rejected at queue capacity")
|
|
var ordered := service.get_elevator_queue(&"lift", 10)
|
|
_expect(ordered.size() == 3, "queue remains bounded after overflow rejection")
|
|
_expect(ordered[0][&"request_id"] == &"r_vip", "VIP precedes fresh normal requests")
|
|
_expect(ordered[1][&"request_id"] == &"r_normal_a", "same-priority tie breaks by unit id")
|
|
_expect(ordered[2][&"request_id"] == &"r_normal_b", "same-minute ordering is deterministic")
|
|
var starved_order := service.get_elevator_queue(&"lift", 70)
|
|
_expect(starved_order[0][&"request_id"] == &"r_normal_a", "60-minute normal request is promoted ahead of VIP")
|
|
var boarded := service.dequeue_elevator_boarding(&"lift", 0, 70)
|
|
_expect(boarded.size() == 2, "boarding never exceeds cabin capacity")
|
|
_expect(boarded[0][&"request_id"] == &"r_normal_a", "starvation promotion applies to boarding order")
|
|
_expect(boarded[1][&"request_id"] == &"r_normal_b", "second starved normal boards deterministically")
|
|
_expect(service.elevator_queue_size(&"lift") == 1, "unboarded VIP remains queued without loss")
|
|
service.set_elevator_operational(&"lift", false)
|
|
_expect(service.dequeue_elevator_boarding(&"lift", 0, 71).is_empty(), "outage elevator boards nobody")
|
|
_expect(service.elevator_queue_size(&"lift") == 1, "outage preserves queued requests")
|
|
|
|
|
|
func _test_normal_connector_outage_snapshot_stability() -> void:
|
|
var from_cell := Vector3i(0, 0, 0)
|
|
var to_cell := Vector3i(0, 0, 1)
|
|
var original = PathServiceScript.new()
|
|
original.rebuild_graph([from_cell, to_cell])
|
|
original.configure_connector(&"ordinary_door", from_cell, to_cell)
|
|
original.set_connector_operational_state(&"ordinary_door", original.CONNECTOR_OUTAGE)
|
|
var before := original.create_snapshot()
|
|
_expect(original.get_connector(&"ordinary_door")[&"cabin_state"] == original.CABIN_IDLE, "ordinary connector keeps canonical IDLE cabin placeholder during outage")
|
|
var restored = PathServiceScript.new()
|
|
_expect(restored.restore_snapshot(before), "outage connector snapshot restores")
|
|
_expect(restored.rebuild_graph([from_cell, to_cell]), "outage connector rebuilds against authoritative cells")
|
|
_expect(restored.get_connector(&"ordinary_door")[&"cabin_state"] == restored.CABIN_IDLE, "restore does not mutate ordinary connector cabin placeholder")
|
|
_expect(restored.create_snapshot() == before, "ordinary connector outage snapshot roundtrip preserves exact digest state")
|
|
|
|
|
|
func _test_elevator_request_global_uniqueness() -> void:
|
|
var service = PathServiceScript.new()
|
|
var a_from := Vector3i(0, 0, 0)
|
|
var a_to := Vector3i(0, 0, 1)
|
|
var b_from := Vector3i(2, 0, 0)
|
|
var b_to := Vector3i(2, 0, 1)
|
|
service.rebuild_graph([a_from, a_to, b_from, b_to])
|
|
service.configure_elevator(&"lift_a", a_from, a_to, {&"queue_capacity": 4})
|
|
service.configure_elevator(&"lift_b", b_from, b_to, {&"queue_capacity": 4})
|
|
var original_request := _request(&"global_request", &"shared_unit", 0, 1, &"NORMAL", 5)
|
|
_expect(bool(service.enqueue_elevator_request(&"lift_a", original_request)[&"ok"]), "first global request is accepted")
|
|
var cross_connector := service.enqueue_elevator_request(
|
|
&"lift_b",
|
|
_request(&"global_request", &"other_unit", 0, 1, &"NORMAL", 5)
|
|
)
|
|
_expect(not bool(cross_connector[&"ok"]) and cross_connector[&"reason"] == &"DUPLICATE_REQUEST", "request id cannot be reused across connectors")
|
|
var same_unit := service.enqueue_elevator_request(
|
|
&"lift_b",
|
|
_request(&"other_request", &"shared_unit", 0, 1, &"NORMAL", 5)
|
|
)
|
|
_expect(not bool(same_unit[&"ok"]) and same_unit[&"reason"] == &"UNIT_ALREADY_ACTIVE", "one unit cannot own multiple active elevator requests")
|
|
var boarded := service.dequeue_elevator_boarding(&"lift_a", 0, 5)
|
|
_expect(boarded.size() == 1, "original request boards exactly once")
|
|
_expect(service.get_active_elevator_request(&"shared_unit")[&"state"] == service.ELEVATOR_REQUEST_BOARDED, "boarding persists an active BOARDED unit index")
|
|
var boarded_reentry := service.enqueue_elevator_request(
|
|
&"lift_b",
|
|
_request(&"while_boarded", &"shared_unit", 0, 1, &"NORMAL", 6)
|
|
)
|
|
_expect(not bool(boarded_reentry[&"ok"]) and boarded_reentry[&"reason"] == &"UNIT_ALREADY_ACTIVE", "boarded unit cannot enqueue a second ride")
|
|
var mid_trip_snapshot := service.create_snapshot()
|
|
var restored = PathServiceScript.new()
|
|
_expect(restored.restore_snapshot(mid_trip_snapshot), "boarded request indexes restore")
|
|
_expect(restored.rebuild_graph([a_from, a_to, b_from, b_to]), "boarded request snapshot rebuilds")
|
|
_expect(restored.create_snapshot() == mid_trip_snapshot, "known and active request indexes roundtrip exactly")
|
|
_expect(restored.complete_elevator_trip(&"lift_a", &"global_request"), "explicit trip completion releases the unit index")
|
|
_expect(bool(restored.enqueue_elevator_request(&"lift_b", _request(&"new_trip", &"shared_unit", 0, 1, &"NORMAL", 7))[&"ok"]), "completed unit may enqueue a new request id")
|
|
var reused_retired := restored.enqueue_elevator_request(
|
|
&"lift_b",
|
|
_request(&"global_request", &"third_unit", 0, 1, &"NORMAL", 7)
|
|
)
|
|
_expect(not bool(reused_retired[&"ok"]), "boarded and completed request id remains permanently tombstoned")
|
|
|
|
|
|
func _test_parallel_connector_access_cost() -> void:
|
|
var service = PathServiceScript.new()
|
|
var from_cell := Vector3i(0, 0, 0)
|
|
var to_cell := Vector3i(0, 0, 1)
|
|
service.rebuild_graph([from_cell, to_cell])
|
|
service.configure_connector(&"a_staff_shortcut", from_cell, to_cell, 100, true, &"staff")
|
|
service.configure_connector(&"z_public_passage", from_cell, to_cell, 400)
|
|
var public_report := service.find_path_report(from_cell, to_cell)
|
|
_expect(bool(public_report[&"reachable"]), "public actor can use unrestricted parallel connector")
|
|
_expect(int(public_report[&"cost"]) == 400, "public route report excludes cheaper staff-only parallel edge")
|
|
var staff_report := service.find_path_report(from_cell, to_cell, null, [&"staff"])
|
|
_expect(int(staff_report[&"cost"]) == 100, "staff route report uses accessible restricted shortcut")
|
|
_expect(service.calculate_path_cost(public_report[&"path"]) == 400, "public path cost API honors default connector access")
|
|
_expect(service.calculate_path_cost(staff_report[&"path"], [&"staff"]) == 100, "profiled path cost API selects the exact accessible edge")
|
|
|
|
|
|
func _test_stuck_detection_and_explicit_recovery() -> void:
|
|
var service = PathServiceScript.new()
|
|
var left := Vector3i(0, 0, 0)
|
|
var occupied := Vector3i(2, 0, 0)
|
|
service.rebuild_graph([left, occupied])
|
|
service.actor_enter_cell(&"guest_stuck", occupied)
|
|
_expect(
|
|
bool(service.record_actor_progress(&"guest_stuck", Vector3i(1, 0, 0), 100)[&"ok"]),
|
|
"progress tracker accepts an actor observation"
|
|
)
|
|
var early := service.get_actor_stuck_report(&"guest_stuck", 129)
|
|
_expect(not bool(early[&"should_recover"]), "29 minutes without progress is not yet stuck")
|
|
var stuck := service.get_actor_stuck_report(&"guest_stuck", 130)
|
|
_expect(bool(stuck[&"should_recover"]), "30 minutes without progress triggers stuck recovery")
|
|
_expect(stuck[&"reason"] == &"STUCK", "stuck report carries an explicit reason")
|
|
_expect(bool(stuck[&"requires_explicit_move"]), "recovery requires an explicit caller action")
|
|
_expect(bool(stuck[&"recovery"][&"found"]), "stuck report provides a safe recovery candidate")
|
|
_expect(stuck[&"recovery"][&"cell"] == left, "nearest-cell tie uses deterministic coordinate order")
|
|
_expect(not bool(stuck[&"recovery"][&"implicit_move_applied"]), "PathService never teleports during stuck evaluation")
|
|
_expect(service.get_actor_projected_cell(&"guest_stuck") == occupied, "actor occupancy is unchanged by recovery proposal")
|
|
service.record_actor_progress(&"guest_stuck", occupied, 131)
|
|
var reset := service.get_actor_stuck_report(&"guest_stuck", 160)
|
|
_expect(not bool(reset[&"should_recover"]), "actual cell progress resets the 30-minute timer")
|
|
|
|
|
|
func _test_actor_progress_cleanup_contract() -> void:
|
|
var service = PathServiceScript.new()
|
|
var left := Vector3i(0, 0, 0)
|
|
var right := Vector3i(1, 0, 0)
|
|
service.rebuild_graph([left, right])
|
|
service.actor_enter_cell(&"despawned", left, 10)
|
|
_expect(service.actor_leave_cell(&"despawned"), "default leave removes occupancy")
|
|
_expect(not bool(service.get_actor_stuck_report(&"despawned", 40)[&"ok"]), "default leave also removes actor progress")
|
|
service.actor_enter_cell(&"preserved", left, 10)
|
|
_expect(service.actor_leave_cell(&"preserved", false), "leave can explicitly retain progress for a transfer")
|
|
_expect(bool(service.get_actor_stuck_report(&"preserved", 40)[&"ok"]), "opt-out cleanup retains transfer progress")
|
|
service.project_occupancy([
|
|
{&"actor_id": &"kept", &"cell": left},
|
|
{&"actor_id": &"removed", &"cell": right},
|
|
])
|
|
service.record_actor_progress(&"kept", left, 50)
|
|
service.record_actor_progress(&"removed", right, 50)
|
|
service.project_occupancy([{&"actor_id": &"kept", &"cell": left}])
|
|
_expect(bool(service.get_actor_stuck_report(&"kept", 50)[&"ok"]), "authoritative occupancy replacement preserves retained actor progress")
|
|
_expect(not bool(service.get_actor_stuck_report(&"removed", 50)[&"ok"]), "authoritative occupancy replacement cleans unprojected progress")
|
|
service.record_actor_progress(&"progress_only", right, 60)
|
|
_expect(service.despawn_actor(&"progress_only"), "despawn API cleans a progress-only actor")
|
|
_expect(not bool(service.get_actor_stuck_report(&"progress_only", 60)[&"ok"]), "despawn leaves no stale progress record")
|
|
|
|
|
|
func _test_relevant_no_route_diagnosis() -> void:
|
|
var service = PathServiceScript.new()
|
|
var start := Vector3i(0, 0, 0)
|
|
var goal := Vector3i(10, 0, 0)
|
|
var unrelated_a := Vector3i(20, 0, 0)
|
|
var unrelated_b := Vector3i(20, 0, 1)
|
|
service.rebuild_graph([start, goal, unrelated_a, unrelated_b])
|
|
service.set_cell_temporary_block(unrelated_a, true)
|
|
service.configure_connector(&"unrelated_lift", unrelated_a, unrelated_b)
|
|
service.set_connector_operational_state(&"unrelated_lift", service.CONNECTOR_OUTAGE)
|
|
var unrelated_report := service.find_path_report(start, goal)
|
|
_expect(unrelated_report[&"reason"] == service.ROUTE_GOAL_UNREACHABLE, "unrelated block and outage do not misclassify a disconnected route")
|
|
|
|
var blocked = PathServiceScript.new()
|
|
blocked.rebuild_graph([Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0)])
|
|
blocked.set_cell_temporary_block(Vector3i(1, 0, 0), true)
|
|
_expect(blocked.find_path_report(Vector3i(0, 0, 0), Vector3i(2, 0, 0))[&"reason"] == blocked.ROUTE_TEMPORARILY_BLOCKED, "relevant temporary block is diagnosed by auxiliary reachability")
|
|
|
|
var outage = PathServiceScript.new()
|
|
var outage_from := Vector3i(0, 0, 0)
|
|
var outage_to := Vector3i(0, 0, 1)
|
|
outage.rebuild_graph([outage_from, outage_to])
|
|
outage.configure_connector(&"relevant_door", outage_from, outage_to)
|
|
outage.set_connector_operational_state(&"relevant_door", outage.CONNECTOR_OUTAGE)
|
|
_expect(outage.find_path_report(outage_from, outage_to)[&"reason"] == outage.ROUTE_CONNECTOR_UNAVAILABLE, "relevant stopped connector is diagnosed precisely")
|
|
|
|
|
|
func _test_snapshot_roundtrip_and_atomic_rejection() -> void:
|
|
var original = PathServiceScript.new()
|
|
var from_cell := Vector3i(0, 0, 0)
|
|
var to_cell := Vector3i(0, 0, 1)
|
|
original.rebuild_graph([from_cell, to_cell])
|
|
original.configure_cell_traffic(from_cell, 2, 7)
|
|
original.actor_enter_cell(&"guest_a", from_cell, 40)
|
|
original.configure_elevator(&"lift_save", from_cell, to_cell, {
|
|
&"cabin_capacity": 3,
|
|
&"queue_capacity": 4,
|
|
&"travel_minutes_per_floor": 2,
|
|
})
|
|
original.enqueue_elevator_request(
|
|
&"lift_save",
|
|
_request(&"saved_request", &"guest_a", 0, 1, &"NORMAL", 40)
|
|
)
|
|
original.record_actor_progress(&"off_graph_actor", Vector3i(4, 4, 0), 12)
|
|
var snapshot := original.create_snapshot()
|
|
_expect(int(snapshot[&"schema_version"]) == 4, "shaft-safe traffic snapshot uses schema version 4")
|
|
_expect(snapshot.has(&"cell_traffic") and snapshot.has(&"actor_cells"), "snapshot persists traffic and occupancy projection")
|
|
_expect(not snapshot.has(&"graph") and not snapshot.has(&"cache"), "snapshot still excludes derived graph and cache")
|
|
var restored = PathServiceScript.new()
|
|
_expect(restored.restore_snapshot(snapshot), "v4 snapshot restores before authoritative world graph")
|
|
_expect(restored.rebuild_graph([from_cell, to_cell]), "restored traffic validates against authoritative cells")
|
|
_expect(restored.create_snapshot() == snapshot, "v4 snapshot roundtrip is exact")
|
|
_expect(restored.get_current_density(from_cell) == 1, "restored density is derived consistently from actors")
|
|
_expect(restored.elevator_queue_size(&"lift_save") == 1, "restored elevator retains its request queue")
|
|
|
|
var before_corrupt := restored.create_snapshot()
|
|
var corrupt := snapshot.duplicate(true)
|
|
corrupt[&"cell_traffic"][0][&"current_density"] = 99
|
|
_expect(not restored.restore_snapshot(corrupt), "corrupt density snapshot is rejected")
|
|
_expect(restored.create_snapshot() == before_corrupt, "failed restore is atomic and preserves live state")
|
|
var corrupt_index := snapshot.duplicate(true)
|
|
corrupt_index[&"active_elevator_units"][0][&"state"] = &"INVALID"
|
|
_expect(not restored.restore_snapshot(corrupt_index), "corrupt elevator active index is rejected")
|
|
_expect(restored.create_snapshot() == before_corrupt, "active-index rejection is also fully atomic")
|
|
var v2_snapshot := snapshot.duplicate(true)
|
|
v2_snapshot[&"schema_version"] = 2
|
|
v2_snapshot.erase(&"known_elevator_request_ids")
|
|
v2_snapshot.erase(&"active_elevator_units")
|
|
v2_snapshot.erase(&"vertical_walk_blockers")
|
|
var v2_restored = PathServiceScript.new()
|
|
_expect(v2_restored.restore_snapshot(v2_snapshot), "schema v2 snapshot remains readable")
|
|
_expect(v2_restored.rebuild_graph([from_cell, to_cell]), "schema v2 traffic validates against authoritative cells")
|
|
_expect(v2_restored.get_active_elevator_request(&"guest_a")[&"state"] == v2_restored.ELEVATOR_REQUEST_QUEUED, "v2 queued requests derive new active indexes deterministically")
|
|
|
|
var legacy_connector := {
|
|
&"id": &"legacy_door",
|
|
&"from": from_cell,
|
|
&"to": to_cell,
|
|
&"cost": 150,
|
|
&"bidirectional": true,
|
|
&"access_id": &"",
|
|
&"enabled": true,
|
|
}
|
|
var legacy = PathServiceScript.new()
|
|
_expect(
|
|
legacy.restore_snapshot({&"schema_version": 1, &"connectors": [legacy_connector]}),
|
|
"legacy connector-only snapshot remains readable"
|
|
)
|
|
_expect(legacy.rebuild_graph([from_cell, to_cell]), "legacy restore rebuilds against current world cells")
|
|
_expect(legacy.find_path(from_cell, to_cell).size() == 2, "legacy connector remains operational")
|
|
|
|
|
|
func _test_input_order_determinism() -> void:
|
|
var first = _build_ordered_service(false)
|
|
var reversed = _build_ordered_service(true)
|
|
_expect(first.create_snapshot() == reversed.create_snapshot(), "actor and request insertion order does not change canonical snapshot")
|
|
var first_path: Array[Vector3i] = first.find_path(Vector3i(0, 0, 0), Vector3i(0, 0, 1))
|
|
var reversed_path: Array[Vector3i] = reversed.find_path(Vector3i(0, 0, 0), Vector3i(0, 0, 1))
|
|
_expect(first_path == reversed_path, "input order does not change route selection")
|
|
_expect(
|
|
first.get_elevator_queue(&"deterministic_lift", 20) == reversed.get_elevator_queue(&"deterministic_lift", 20),
|
|
"input order does not change elevator queue order"
|
|
)
|
|
_expect(first.get_current_density(Vector3i(1, 0, 0)) == 3, "canonical projection retains all actors")
|
|
|
|
|
|
func _build_ordered_service(reverse_input: bool):
|
|
var service = PathServiceScript.new()
|
|
var cells := [Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(0, 0, 1)]
|
|
if reverse_input:
|
|
cells.reverse()
|
|
service.rebuild_graph(cells)
|
|
service.configure_elevator(&"deterministic_lift", Vector3i(0, 0, 0), Vector3i(0, 0, 1), {
|
|
&"queue_capacity": 5,
|
|
})
|
|
var occupancy := [
|
|
{&"actor_id": &"actor_c", &"cell": Vector3i(1, 0, 0)},
|
|
{&"actor_id": &"actor_a", &"cell": Vector3i(1, 0, 0)},
|
|
{&"actor_id": &"actor_b", &"cell": Vector3i(1, 0, 0)},
|
|
]
|
|
var requests := [
|
|
_request(&"req_c", &"unit_c", 0, 1, &"NORMAL", 20),
|
|
_request(&"req_a", &"unit_a", 0, 1, &"NORMAL", 20),
|
|
_request(&"req_b", &"unit_b", 0, 1, &"NORMAL", 20),
|
|
]
|
|
if reverse_input:
|
|
occupancy.reverse()
|
|
requests.reverse()
|
|
service.project_occupancy(occupancy)
|
|
for request: Dictionary in requests:
|
|
service.enqueue_elevator_request(&"deterministic_lift", request)
|
|
return service
|
|
|
|
|
|
func _diamond_cells() -> Array[Vector3i]:
|
|
return [
|
|
Vector3i(0, 1, 0),
|
|
Vector3i(0, 0, 0),
|
|
Vector3i(1, 0, 0),
|
|
Vector3i(2, 0, 0),
|
|
Vector3i(2, 1, 0),
|
|
Vector3i(0, 2, 0),
|
|
Vector3i(1, 2, 0),
|
|
Vector3i(2, 2, 0),
|
|
]
|
|
|
|
|
|
func _request(
|
|
request_id: StringName,
|
|
unit_id: StringName,
|
|
floor_from: int,
|
|
floor_to: int,
|
|
priority: StringName,
|
|
enqueued_at: int
|
|
) -> Dictionary:
|
|
return {
|
|
&"request_id": request_id,
|
|
&"unit_id": unit_id,
|
|
&"floor_from": floor_from,
|
|
&"floor_to": floor_to,
|
|
&"direction": &"UP" if floor_to > floor_from else &"DOWN",
|
|
&"priority": priority,
|
|
&"enqueued_at": enqueued_at,
|
|
}
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|
|
|
|
|
|
func _capture_path_sample(duration_us: int) -> void:
|
|
_timing_samples.append(duration_us)
|