641 lines
27 KiB
GDScript
641 lines
27 KiB
GDScript
extends SceneTree
|
|
|
|
const POST_V2_RUNTIME_KEYS := [
|
|
&"kitchen_production",
|
|
&"elevators",
|
|
&"legacy_stairs",
|
|
&"movement_credits",
|
|
&"catalog_manifest",
|
|
&"progression",
|
|
&"hired_staff_candidate_ids",
|
|
&"operations",
|
|
&"excavation",
|
|
&"spatial_subslots",
|
|
&"route_projection",
|
|
&"facility_infrastructure",
|
|
]
|
|
const POST_V2_EVENT_TYPES := [
|
|
&"PATH_ACTOR_STUCK",
|
|
&"WORLD_TREE_RANK_ADVANCED",
|
|
&"STAFF_CANDIDATE_HIRED",
|
|
&"GUEST_AUTO_ARRIVED",
|
|
&"FACILITY_INFRASTRUCTURE_PROJECTED",
|
|
&"EXCAVATION_COMPLETED",
|
|
]
|
|
const POST_V2_COMMAND_TYPES := [
|
|
&"set_world_tree_rank",
|
|
&"update_progression_metrics",
|
|
&"complete_progression_quest",
|
|
&"hire_staff_candidate",
|
|
&"acknowledge_operations_pause",
|
|
&"start_excavation",
|
|
&"pause_excavation",
|
|
&"resume_excavation",
|
|
]
|
|
|
|
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 Path traffic integration")
|
|
_test_namespaced_lifecycle_and_density()
|
|
_test_congestion_credit_and_alternate_route()
|
|
_test_occupied_cell_graph_rebuild_is_rejected()
|
|
_test_stuck_guest_and_staff_recovery()
|
|
_test_event_recovery_path_transaction()
|
|
_test_current_snapshot_crosslinks_and_legacy_v2()
|
|
_test_dispatcher_request_ownership_crosslinks()
|
|
_test_twenty_four_hour_projection_integrity()
|
|
_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 Path traffic fixture %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_namespaced_lifecycle_and_density() -> void:
|
|
_reset_runtime(8_101)
|
|
var cell := Vector3i(-4, 0, WorldService.LAYER_FRONT)
|
|
_expect(_spawn_moving_guest(&"shared", cell, Vector3i(4, 0, 0)), "Guest with shared raw id spawns")
|
|
_expect(_register_idle_staff(&"shared", cell), "Staff with the same raw id registers")
|
|
_expect(
|
|
_runtime.path_service.has_actor_projection(&"guest:shared")
|
|
and _runtime.path_service.has_actor_projection(&"staff:shared"),
|
|
"Guest and Staff use collision-free namespaced Path actor ids"
|
|
)
|
|
_expect(
|
|
_runtime.path_service.get_current_density(cell) == 2,
|
|
"spawn/register projects both actors into the authoritative cell density"
|
|
)
|
|
var guest: GuestRuntime = _runtime.guest_service.get_guest(&"shared")
|
|
_runtime.guest_service.begin_leaving(&"shared", _clock.absolute_minute, &"TEST_EXIT")
|
|
_runtime.guest_service.despawn_guest(&"shared", _clock.absolute_minute)
|
|
_expect(guest != null and not _runtime.path_service.has_actor_projection(&"guest:shared"), "Guest despawn removes only its namespaced projection")
|
|
_expect(
|
|
_runtime.path_service.has_actor_projection(&"staff:shared")
|
|
and _runtime.path_service.get_current_density(cell) == 1,
|
|
"Guest despawn preserves the colliding raw Staff id and lowers density once"
|
|
)
|
|
_expect(_runtime.get_actor_movement_credit(&"guest:shared").is_empty(), "Guest despawn removes movement credit")
|
|
_runtime.staff_service.remove_staff(&"shared", &"TEST_REMOVE", true, _clock.absolute_minute)
|
|
_expect(
|
|
not _runtime.path_service.has_actor_projection(&"staff:shared")
|
|
and _runtime.path_service.get_current_density(cell) == 0,
|
|
"Staff removal cleans projection and density without going negative"
|
|
)
|
|
var invalid_spawn: CommandResult = _runtime.submit_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": &"invalid_cell",
|
|
&"guest_type_id": &"traveler",
|
|
&"options": {&"current_cell": Vector3i(0, 2, 0)},
|
|
},
|
|
&"world", &"invalid_cell", 10, -1, &"path.invalid.spawn"
|
|
)
|
|
_expect(
|
|
not invalid_spawn.is_success()
|
|
and not _runtime.guest_service.has_guest(&"invalid_cell")
|
|
and not _runtime.path_service.has_actor_projection(&"guest:invalid_cell")
|
|
and _runtime.get_actor_movement_credit(&"guest:invalid_cell").is_empty()
|
|
and not _has_recent_event(&"GUEST_SPAWNED", &"invalid_cell"),
|
|
"failed spawn leaves Actor, Path, credit, and lifecycle events uncommitted"
|
|
)
|
|
|
|
|
|
func _test_congestion_credit_and_alternate_route() -> void:
|
|
_reset_runtime(8_102)
|
|
var origin := Vector3i(-8, 0, WorldService.LAYER_FRONT)
|
|
var target := Vector3i(8, 0, WorldService.LAYER_FRONT)
|
|
_runtime.path_service.configure_cell_traffic(origin, 1, 0, false)
|
|
for index in range(5):
|
|
_expect(
|
|
_spawn_moving_guest(StringName("slow_%02d" % index), origin, target),
|
|
"congestion Guest %d spawns" % index
|
|
)
|
|
_expect(_runtime.path_service.get_current_density(origin) == 5, "five same-route actors create over-capacity density")
|
|
_clock.advance_one_minute()
|
|
var after_one: Array[Vector3i] = _guest_cells_with_prefix("slow_")
|
|
_expect(after_one.all(func(cell: Vector3i) -> bool: return cell == origin), "over-capacity speed credit prevents a first-minute burst")
|
|
_expect(
|
|
int(_runtime.get_actor_movement_credit(&"guest:slow_00").get(&"credit", -1)) == 250,
|
|
"Path speed multiplier is stored as deterministic fixed-point credit"
|
|
)
|
|
_clock.advance_minutes(3)
|
|
var after_four: Array[Vector3i] = _guest_cells_with_prefix("slow_")
|
|
_expect(after_four.all(func(cell: Vector3i) -> bool: return cell != origin), "minimum speed guarantees progress by the fourth congested minute")
|
|
_expect(_runtime.path_service.get_current_density(origin) == 0, "sequential committed movement lowers source density exactly")
|
|
|
|
_reset_runtime(8_103)
|
|
for x in range(0, 3):
|
|
var definition_id: StringName = &"guest_corridor" if x == 1 else &"guest_stairs"
|
|
var built: CommandResult = _runtime.submit_command(
|
|
&"build_facility", {&"definition_id": definition_id, &"origin": Vector3i(x, 1, WorldService.LAYER_FRONT)},
|
|
&"manager", &"", 0, -1, StringName("path.parallel.build.%d" % x)
|
|
)
|
|
_expect(built.is_success(), "parallel lane uses normal %s construction at x=%d" % [definition_id, x])
|
|
if not built.is_success():
|
|
return
|
|
_clock.advance_minutes(10)
|
|
_expect(_runtime.construction_service.get_job(built.details[&"job_id"]).state == ConstructionJob.State.COMPLETED, "parallel lane construction completes")
|
|
_expect(_runtime.path_service.get_vertical_walk_requires_connector(), "parallel lane retains strict floor policy")
|
|
var congested := Vector3i(1, 0, WorldService.LAYER_FRONT)
|
|
_runtime.path_service.configure_cell_traffic(congested, 1, 0, false)
|
|
# Two 150-cost stairs make the upper route 500; six occupants make the
|
|
# direct two-cell lane 575, so the stair detour remains strictly cheaper.
|
|
for blocker_index in range(6):
|
|
_expect(
|
|
_register_idle_staff(StringName("blocker_%d" % blocker_index), congested),
|
|
"alternate-route blocker %d registers" % blocker_index
|
|
)
|
|
_expect(
|
|
_spawn_moving_guest(&"route_picker", Vector3i(0, 0, 0), Vector3i(2, 0, 0)),
|
|
"route-selection Guest spawns"
|
|
)
|
|
var planned: Array[Vector3i] = _runtime.path_service.find_path(
|
|
Vector3i(0, 0, 0), Vector3i(2, 0, 0)
|
|
)
|
|
_expect(
|
|
planned.size() > 1 and planned[1] == Vector3i(0, 1, 0),
|
|
"congestion-aware A* selects the parallel lane before Runtime movement (%s)" % [planned]
|
|
)
|
|
_clock.advance_one_minute()
|
|
var picker: GuestRuntime = _runtime.guest_service.get_guest(&"route_picker")
|
|
_expect(picker.current_cell == Vector3i(0, 0, 0) and _runtime.get_actor_movement_credit(&"guest:route_picker")[&"credit"] == 1000, "first stair minute preserves incomplete movement credit")
|
|
_clock.advance_one_minute()
|
|
picker = _runtime.guest_service.get_guest(&"route_picker")
|
|
_expect(
|
|
picker != null and picker.current_cell == Vector3i(0, 1, 0),
|
|
"live congestion makes Runtime choose the cheaper parallel lane (actual %s)" % (
|
|
picker.current_cell if picker != null else "missing"
|
|
)
|
|
)
|
|
_expect(
|
|
_runtime.path_service.get_actor_projected_cell(&"guest:route_picker") == picker.current_cell,
|
|
"alternate-route movement commits Actor and Path projection together"
|
|
)
|
|
|
|
|
|
func _test_occupied_cell_graph_rebuild_is_rejected() -> void:
|
|
_reset_runtime(8_110)
|
|
var occupied := Vector3i(0, 0, WorldService.LAYER_FRONT)
|
|
_expect(_register_idle_staff(&"rebuild_guard", occupied), "graph-rebuild guard fixture registers")
|
|
var path_before: Dictionary = _runtime.path_service.create_snapshot()
|
|
_expect(_runtime.world_service.set_walkable(occupied, false), "fixture removes an occupied World cell")
|
|
_expect(
|
|
not _runtime.rebuild_path_graph_from_world(),
|
|
"Runtime rejects a graph rebuild that would evict a live actor projection"
|
|
)
|
|
_expect(
|
|
_runtime.path_service.create_snapshot() == path_before
|
|
and _runtime.path_service.get_actor_projected_cell(&"staff:rebuild_guard") == occupied
|
|
and not _runtime.get_actor_movement_credit(&"staff:rebuild_guard").is_empty(),
|
|
"rejected graph rebuild preserves Path projection, progress, density, and credit"
|
|
)
|
|
_expect(_runtime.world_service.set_walkable(occupied, true), "fixture restores the occupied World cell")
|
|
_expect(_runtime.rebuild_path_graph_from_world(), "safe topology rebuild succeeds after World repair")
|
|
|
|
|
|
func _test_stuck_guest_and_staff_recovery() -> void:
|
|
_reset_runtime(8_104)
|
|
_expect(
|
|
_spawn_moving_guest(&"stuck_guest", Vector3i(0, 0, 0), Vector3i(2, 0, 0)),
|
|
"stuck Guest fixture spawns"
|
|
)
|
|
_runtime.path_service.set_cell_temporary_block(Vector3i(1, 0, 0), true)
|
|
_clock.advance_minutes(PathService.STUCK_THRESHOLD_MINUTES - 1)
|
|
var guest: GuestRuntime = _runtime.guest_service.get_guest(&"stuck_guest")
|
|
_expect(guest != null and guest.state == GuestRuntime.State.MOVING, "Guest remains in movement recovery window before 30 minutes")
|
|
_clock.advance_one_minute()
|
|
guest = _runtime.guest_service.get_guest(&"stuck_guest")
|
|
_expect(guest != null and guest.state == GuestRuntime.State.LEAVING, "30-minute Path report enters existing Guest safe recovery")
|
|
_expect(
|
|
guest != null and guest.current_cell == Vector3i.ZERO,
|
|
"stuck recovery proposes a safe target without implicit teleport"
|
|
)
|
|
_expect(_has_recent_event(&"PATH_ACTOR_STUCK", &"guest:stuck_guest"), "Guest stuck recovery emits a structured Path DomainEvent")
|
|
|
|
_reset_runtime(8_105)
|
|
_expect(_register_idle_staff(&"stuck_staff", Vector3i(0, 0, 0)), "stuck Staff fixture registers")
|
|
var created: CommandResult = _runtime.staff_service.create_task(
|
|
&"inspect", 90, _clock.absolute_minute,
|
|
{&"target_cell": Vector3i(2, 0, 0), &"estimated_minutes": 5},
|
|
&"stuck_task"
|
|
)
|
|
var task: StaffTask = _runtime.staff_service.get_task(&"stuck_task")
|
|
var member: StaffRuntime = _runtime.staff_service.get_staff(&"stuck_staff")
|
|
var reserved: bool = created.is_success() \
|
|
and task.reserve(&"stuck_staff", [], _clock.absolute_minute) \
|
|
and member.reserve_task(&"stuck_task", _clock.absolute_minute) \
|
|
and task.begin_moving(_clock.absolute_minute)
|
|
_expect(reserved, "StaffTask enters a consistent MOVING ownership fixture")
|
|
_runtime.path_service.set_cell_temporary_block(Vector3i(1, 0, 0), true)
|
|
_clock.advance_minutes(PathService.STUCK_THRESHOLD_MINUTES)
|
|
task = _runtime.staff_service.get_task(&"stuck_task")
|
|
member = _runtime.staff_service.get_staff(&"stuck_staff")
|
|
_expect(task != null and task.state == StaffTask.State.CANCELLED, "30-minute Staff stall cancels its owned task safely")
|
|
_expect(member != null and member.current_task_id.is_empty(), "Staff stuck recovery releases task ownership")
|
|
_expect(_has_recent_event(&"PATH_ACTOR_STUCK", &"staff:stuck_staff"), "Staff stuck cancellation emits a structured Path DomainEvent")
|
|
|
|
|
|
func _test_event_recovery_path_transaction() -> void:
|
|
_reset_runtime(8_109)
|
|
_expect(
|
|
_spawn_moving_guest(&"event_mover", Vector3i(-2, 0, 0), Vector3i(2, 0, 0)),
|
|
"Event recovery movement fixture spawns"
|
|
)
|
|
var actor_before: Dictionary = _runtime.guest_service.create_snapshot()
|
|
var path_before: Dictionary = _runtime.path_service.create_snapshot()
|
|
var credit_before: Dictionary = _runtime.get_actor_movement_credit(&"guest:event_mover")
|
|
var events_before: Dictionary = _runtime.domain_events.create_snapshot()
|
|
var action: EventAction = EventAction.new().configure(
|
|
&"recovery_move",
|
|
EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY,
|
|
{&"entity_id": &"event_mover", &"cell": Vector3i(-1, 0, 0)}
|
|
)
|
|
var applied: Dictionary = _runtime._execute_event_action_boundary(
|
|
action, &"apply", {&"event_instance_id": &"event.fixture"}, null
|
|
)
|
|
var moved: GuestRuntime = _runtime.guest_service.get_guest(&"event_mover")
|
|
_expect(
|
|
bool(applied.get(&"ok", false))
|
|
and moved.current_cell == Vector3i(-1, 0, 0)
|
|
and _runtime.path_service.get_actor_projected_cell(&"guest:event_mover") == moved.current_cell
|
|
and int(_runtime.get_actor_movement_credit(&"guest:event_mover")[&"credit"]) == 0,
|
|
"explicit Event recovery commits Actor, Path, and credit together"
|
|
)
|
|
var rolled_back: Dictionary = _runtime._execute_event_action_boundary(
|
|
action, &"rollback", {}, applied.get(&"token", null)
|
|
)
|
|
_expect(
|
|
bool(rolled_back.get(&"ok", false))
|
|
and _runtime.guest_service.create_snapshot() == actor_before
|
|
and _runtime.path_service.create_snapshot() == path_before
|
|
and _runtime.get_actor_movement_credit(&"guest:event_mover") == credit_before
|
|
and _runtime.domain_events.create_snapshot() == events_before,
|
|
"Event rollback restores Actor, Path, credit, and DomainEvents atomically"
|
|
)
|
|
|
|
|
|
func _test_current_snapshot_crosslinks_and_legacy_v2() -> void:
|
|
_reset_runtime(8_106)
|
|
var origin := Vector3i(-3, 0, 0)
|
|
_runtime.path_service.configure_cell_traffic(origin, 1, 0, false)
|
|
for index in range(2):
|
|
_spawn_moving_guest(StringName("save_%d" % index), origin, Vector3i(3, 0, 0))
|
|
_clock.advance_one_minute()
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
|
|
_expect(int(runtime_data[&"runtime_schema_version"]) == 9, "Runtime snapshot advertises schema v9")
|
|
_expect(not runtime_data[&"movement_credits"].is_empty(), "mid-move snapshot persists fixed-point movement credits")
|
|
var digest := SimulationDigest.compute(snapshot)
|
|
_expect(_runtime.restore_snapshot(snapshot), "valid mid-move v9 snapshot restores")
|
|
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "mid-move v9 save/load is exact")
|
|
|
|
var corruptions: Array[Dictionary] = []
|
|
var bad_actor := snapshot.duplicate(true)
|
|
bad_actor[&"systems"][&"runtime"][&"path"][&"actor_cells"][0][&"cell"] = Vector3i(0, 0, 0)
|
|
corruptions.append({&"label": "actor cell", &"snapshot": bad_actor})
|
|
var bad_density := snapshot.duplicate(true)
|
|
for cell_entry: Dictionary in bad_density[&"systems"][&"runtime"][&"path"][&"cell_traffic"]:
|
|
if cell_entry[&"cell"] == origin:
|
|
cell_entry[&"current_density"] = int(cell_entry[&"current_density"]) + 1
|
|
break
|
|
corruptions.append({&"label": "density", &"snapshot": bad_density})
|
|
var bad_progress := snapshot.duplicate(true)
|
|
bad_progress[&"systems"][&"runtime"][&"path"][&"actor_progress"][0][&"last_cell"] = Vector3i(1, 0, 0)
|
|
corruptions.append({&"label": "progress", &"snapshot": bad_progress})
|
|
var bad_credit := snapshot.duplicate(true)
|
|
bad_credit[&"systems"][&"runtime"][&"movement_credits"][0][&"credit"] = 1_000
|
|
corruptions.append({&"label": "movement credit range", &"snapshot": bad_credit})
|
|
var bad_credit_time := snapshot.duplicate(true)
|
|
bad_credit_time[&"systems"][&"runtime"][&"movement_credits"][0][&"updated_at"] = _clock.absolute_minute + 1
|
|
corruptions.append({&"label": "movement credit time", &"snapshot": bad_credit_time})
|
|
var missing_path_cell := snapshot.duplicate(true)
|
|
missing_path_cell[&"systems"][&"runtime"][&"path"][&"cell_traffic"].pop_back()
|
|
corruptions.append({&"label": "Path/world cell-set", &"snapshot": missing_path_cell})
|
|
for corruption: Dictionary in corruptions:
|
|
var live_before: Dictionary = _runtime.create_snapshot()
|
|
_expect(
|
|
not _runtime.restore_snapshot(corruption[&"snapshot"])
|
|
and _runtime.create_snapshot() == live_before,
|
|
"corrupt %s crosslink is rejected atomically" % corruption[&"label"]
|
|
)
|
|
|
|
var legacy_v2 := snapshot.duplicate(true)
|
|
var legacy_data: Dictionary = legacy_v2[&"systems"][&"runtime"]
|
|
legacy_data[&"runtime_schema_version"] = 2
|
|
_strip_post_v2_runtime_state(legacy_data)
|
|
legacy_data[&"path"][&"schema_version"] = 3
|
|
legacy_data[&"path"].erase(&"vertical_walk_blockers")
|
|
legacy_data[&"path"].erase(&"vertical_walk_requires_connector")
|
|
legacy_data[&"path"].erase(&"transit_actor_ids")
|
|
legacy_data[&"path"][&"actor_cells"] = []
|
|
legacy_data[&"path"][&"actor_progress"] = []
|
|
for traffic: Dictionary in legacy_data[&"path"][&"cell_traffic"]:
|
|
traffic[&"current_density"] = 0
|
|
if traffic[&"cell"] == origin:
|
|
traffic[&"temporary_block"] = true
|
|
_expect(
|
|
_is_true_runtime_v2_shape(legacy_data),
|
|
"legacy fixture removes every v3-v9 aggregate and dedicated history instead of relabelling v9"
|
|
)
|
|
_expect(_runtime.restore_snapshot(legacy_v2), "legacy Runtime v2 deterministically rebuilds Path actors and credits")
|
|
var migrated: Dictionary = _runtime.create_snapshot()
|
|
_expect(
|
|
int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 9
|
|
and migrated[&"systems"][&"runtime"][&"path"][&"actor_cells"].size() == 2
|
|
and migrated[&"systems"][&"runtime"][&"movement_credits"].size() == 2
|
|
and bool(_runtime.path_service.get_cell_traffic(origin)[&"temporary_block"]),
|
|
"legacy migration emits canonical v9 ownership while preserving blocked traffic state"
|
|
)
|
|
|
|
|
|
func _test_dispatcher_request_ownership_crosslinks() -> void:
|
|
_reset_runtime(8_107)
|
|
var stock: CommandResult = _runtime.submit_command(
|
|
&"add_inventory_stack",
|
|
{&"owner_inventory_id": &"supplier", &"item_id": &"item/ore", &"quantity": 2, &"unit_weight": 3},
|
|
&"manager", &"supplier", 10, -1, &"path.runtime.dispatch.stock"
|
|
)
|
|
_expect(stock.is_success(), "dispatcher crosslink fixture stocks weighted items")
|
|
var requested: CommandResult = _runtime.submit_command(
|
|
&"create_logistics_request",
|
|
{
|
|
&"request_id": &"ownership_request",
|
|
&"source_inventory_id": &"supplier",
|
|
&"destination_inventory_id": &"destination",
|
|
&"item_requirements": {&"item/ore": 2},
|
|
},
|
|
&"manager", &"ownership_request", 10, -1, &"path.runtime.dispatch.request"
|
|
)
|
|
_expect(requested.is_success(), "dispatcher crosslink fixture creates a reserved request")
|
|
var dispatched: CommandResult = _runtime.submit_command(
|
|
&"dispatch_logistics",
|
|
{&"carriers": {
|
|
&"carrier_a": {&"capacity": 10, &"current_load": 0, &"path_minutes": 1},
|
|
}},
|
|
&"manager", &"ownership_request", 10, -1, &"path.runtime.dispatch.assign"
|
|
)
|
|
_expect(dispatched.is_success(), "dispatcher assigns request-ID ownership")
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.restore_snapshot(snapshot), "active dispatcher ownership snapshot restores")
|
|
var ownership_tamper: Dictionary = snapshot.duplicate(true)
|
|
ownership_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"][&"assignment_ownership"][0][&"carrier_id"] = "carrier_b"
|
|
var live_before: Dictionary = _runtime.create_snapshot()
|
|
_expect(
|
|
not _runtime.restore_snapshot(ownership_tamper) and _runtime.create_snapshot() == live_before,
|
|
"request-level carrier tamper is rejected despite unchanged aggregate load"
|
|
)
|
|
var coherent_dispatcher_tamper: Dictionary = snapshot.duplicate(true)
|
|
var tampered_dispatcher: Dictionary = coherent_dispatcher_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"]
|
|
tampered_dispatcher[&"assignment_ownership"][0][&"carrier_id"] = "carrier_b"
|
|
tampered_dispatcher[&"carrier_loads"][0][&"carrier_id"] = "carrier_b"
|
|
tampered_dispatcher[&"assignment_history"][0][&"carrier_id"] = "carrier_b"
|
|
_expect(
|
|
not _runtime.restore_snapshot(coherent_dispatcher_tamper)
|
|
and _runtime.create_snapshot() == live_before,
|
|
"Runtime rejects internally coherent Dispatcher carrier ownership that conflicts with its request"
|
|
)
|
|
var state_tamper: Dictionary = snapshot.duplicate(true)
|
|
state_tamper[&"systems"][&"runtime"][&"logistics_requests"][0][&"state"] = "COMPLETED"
|
|
_expect(
|
|
not _runtime.restore_snapshot(state_tamper) and _runtime.create_snapshot() == live_before,
|
|
"terminal-state tamper cannot retain active Dispatcher ownership"
|
|
)
|
|
var history_tamper: Dictionary = snapshot.duplicate(true)
|
|
history_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"][&"assignment_history"][0][&"weight"] = 3
|
|
_expect(
|
|
not _runtime.restore_snapshot(history_tamper) and _runtime.create_snapshot() == live_before,
|
|
"request-level history weight tamper is rejected atomically"
|
|
)
|
|
|
|
|
|
func _test_twenty_four_hour_projection_integrity() -> void:
|
|
_reset_runtime(8_108)
|
|
_expect(_register_idle_staff(&"day_guard", Vector3i(0, 0, 0)), "24-hour Staff projection registers")
|
|
_clock.advance_minutes(1_440)
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
_expect(
|
|
_runtime.path_service.has_actor_projection(&"staff:day_guard")
|
|
and _runtime.path_service.get_actor_projected_cell(&"staff:day_guard") == Vector3i.ZERO,
|
|
"24-hour idle processing leaves no Path actor orphan"
|
|
)
|
|
_expect(_runtime.restore_snapshot(snapshot), "24-hour Path/credit snapshot passes strict crosslink restore")
|
|
_expect(_runtime.create_snapshot() == snapshot, "24-hour Path traffic state restores exactly")
|
|
|
|
|
|
func _spawn_moving_guest(guest_id: StringName, cell: Vector3i, target: Vector3i) -> bool:
|
|
var result: CommandResult = _runtime.submit_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": guest_id,
|
|
&"guest_type_id": &"traveler",
|
|
&"gold": 100,
|
|
&"options": {
|
|
&"current_cell": cell,
|
|
&"state": GuestRuntime.State.MOVING,
|
|
&"state_entered_at": _clock.absolute_minute - 1,
|
|
&"planned_exit_at": _clock.absolute_minute + 2_000,
|
|
&"path_handle": {&"target_cell": target},
|
|
},
|
|
},
|
|
&"world", guest_id, 10, -1, StringName("path.spawn.%s" % guest_id)
|
|
)
|
|
return result.is_success()
|
|
|
|
|
|
func _register_idle_staff(staff_id: StringName, cell: Vector3i) -> bool:
|
|
var result: CommandResult = _runtime.submit_command(
|
|
&"register_staff",
|
|
{
|
|
&"staff_id": staff_id,
|
|
&"role_id": &"worker",
|
|
&"options": {
|
|
&"state": StaffRuntime.State.AVAILABLE,
|
|
&"current_cell": cell,
|
|
&"role_tags": [&"worker"],
|
|
&"permitted_task_tags": [&"work"],
|
|
},
|
|
},
|
|
&"manager", staff_id, 10, -1, StringName("path.staff.%s" % staff_id)
|
|
)
|
|
return result.is_success()
|
|
|
|
|
|
func _guest_cells_with_prefix(prefix: String) -> Array[Vector3i]:
|
|
var cells: Array[Vector3i] = []
|
|
for guest: GuestRuntime in _runtime.guest_service.get_guests():
|
|
if String(guest.guest_id).begins_with(prefix):
|
|
cells.append(guest.current_cell)
|
|
return cells
|
|
|
|
|
|
func _has_recent_event(event_type: StringName, actor_id: StringName) -> bool:
|
|
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
|
|
if event.event_type == event_type and actor_id in event.entity_ids:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _strip_post_v2_runtime_state(runtime_data: Dictionary) -> void:
|
|
for key: StringName in POST_V2_RUNTIME_KEYS:
|
|
runtime_data.erase(key)
|
|
runtime_data.erase(String(key))
|
|
_strip_post_v2_event_history(runtime_data)
|
|
_strip_post_v2_pending_commands(runtime_data)
|
|
|
|
|
|
func _strip_post_v2_event_history(runtime_data: Dictionary) -> void:
|
|
var stream_value: Variant = runtime_data.get(&"domain_events", null)
|
|
if not stream_value is Dictionary:
|
|
return
|
|
var stream: Dictionary = stream_value
|
|
var recent_value: Variant = stream.get("recent_events", null)
|
|
var published_value: Variant = stream.get("published_ids", null)
|
|
if not recent_value is Array or not published_value is Array:
|
|
return
|
|
var removed_event_ids: Dictionary = {}
|
|
var retained_events: Array = []
|
|
for raw_event: Variant in recent_value:
|
|
if raw_event is Dictionary and StringName(
|
|
(raw_event as Dictionary).get("event_type", "")
|
|
) in POST_V2_EVENT_TYPES:
|
|
removed_event_ids[StringName((raw_event as Dictionary).get("event_id", ""))] = true
|
|
continue
|
|
retained_events.append(raw_event)
|
|
var retained_published_ids: Array = []
|
|
for raw_event_id: Variant in published_value:
|
|
if not removed_event_ids.has(StringName(raw_event_id)):
|
|
retained_published_ids.append(raw_event_id)
|
|
stream["recent_events"] = retained_events
|
|
stream["published_ids"] = retained_published_ids
|
|
|
|
|
|
func _strip_post_v2_pending_commands(runtime_data: Dictionary) -> void:
|
|
var queue_value: Variant = runtime_data.get(&"command_queue", null)
|
|
if not queue_value is Dictionary:
|
|
return
|
|
var queue: Dictionary = queue_value
|
|
var removed_command_ids: Dictionary = {}
|
|
for queue_key: String in ["pending", "in_flight"]:
|
|
var commands_value: Variant = queue.get(queue_key, null)
|
|
if not commands_value is Array:
|
|
continue
|
|
var retained_commands: Array = []
|
|
for raw_command: Variant in commands_value:
|
|
if raw_command is Dictionary and StringName(
|
|
(raw_command as Dictionary).get("command_type", "")
|
|
) in POST_V2_COMMAND_TYPES:
|
|
removed_command_ids[int((raw_command as Dictionary).get("command_id", -1))] = true
|
|
continue
|
|
retained_commands.append(raw_command)
|
|
queue[queue_key] = retained_commands
|
|
if removed_command_ids.is_empty():
|
|
return
|
|
var accepted_value: Variant = queue.get("accepted", null)
|
|
if accepted_value is Array:
|
|
var retained_accepted: Array = []
|
|
for raw_metadata: Variant in accepted_value:
|
|
if not raw_metadata is Dictionary or not removed_command_ids.has(
|
|
int((raw_metadata as Dictionary).get("command_id", -1))
|
|
):
|
|
retained_accepted.append(raw_metadata)
|
|
queue["accepted"] = retained_accepted
|
|
var idempotency_value: Variant = queue.get("idempotency", null)
|
|
if idempotency_value is Dictionary:
|
|
var idempotency: Dictionary = idempotency_value
|
|
for key: Variant in idempotency.keys():
|
|
if removed_command_ids.has(int(idempotency[key])):
|
|
idempotency.erase(key)
|
|
|
|
|
|
func _is_true_runtime_v2_shape(runtime_data: Dictionary) -> bool:
|
|
if typeof(runtime_data.get(&"runtime_schema_version", null)) != TYPE_INT \
|
|
or int(runtime_data[&"runtime_schema_version"]) != 2:
|
|
return false
|
|
for key: StringName in POST_V2_RUNTIME_KEYS:
|
|
if runtime_data.has(key) or runtime_data.has(String(key)):
|
|
return false
|
|
var stream_value: Variant = runtime_data.get(&"domain_events", null)
|
|
if not stream_value is Dictionary:
|
|
return false
|
|
var recent_value: Variant = (stream_value as Dictionary).get("recent_events", null)
|
|
if not recent_value is Array:
|
|
return false
|
|
for raw_event: Variant in recent_value:
|
|
if raw_event is Dictionary and StringName(
|
|
(raw_event as Dictionary).get("event_type", "")
|
|
) in POST_V2_EVENT_TYPES:
|
|
return false
|
|
var queue_value: Variant = runtime_data.get(&"command_queue", null)
|
|
if not queue_value is Dictionary:
|
|
return false
|
|
for queue_key: String in ["pending", "in_flight"]:
|
|
var commands_value: Variant = (queue_value as Dictionary).get(queue_key, null)
|
|
if not commands_value is Array:
|
|
return false
|
|
for raw_command: Variant in commands_value:
|
|
if raw_command is Dictionary and StringName(
|
|
(raw_command as Dictionary).get("command_type", "")
|
|
) in POST_V2_COMMAND_TYPES:
|
|
return false
|
|
var staff_value: Variant = runtime_data.get(&"staff", null)
|
|
if staff_value is Dictionary:
|
|
var payroll_value: Variant = (staff_value as Dictionary).get(
|
|
&"payroll_settlements", []
|
|
)
|
|
if payroll_value is Array and not payroll_value.is_empty():
|
|
return false
|
|
var economy_value: Variant = runtime_data.get(&"economy", null)
|
|
if economy_value is Dictionary:
|
|
var entries_value: Variant = (economy_value as Dictionary).get(&"entries", [])
|
|
if entries_value is Array:
|
|
for raw_entry: Variant in entries_value:
|
|
if raw_entry is Dictionary and StringName(
|
|
(raw_entry as Dictionary).get(&"category", "")
|
|
) == &"WAGE":
|
|
return false
|
|
return true
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|