Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,752 @@
|
||||
extends SceneTree
|
||||
|
||||
const FacilityDefinitionType = preload("res://scripts/data/facility_definition.gd")
|
||||
const PlacementReportType = preload("res://scripts/construction/placement_report.gd")
|
||||
const ConstructionJobType = preload("res://scripts/construction/construction_job.gd")
|
||||
const ConstructionServiceType = preload("res://scripts/construction/construction_service.gd")
|
||||
const WorldServiceType = preload("res://scripts/world/world_service.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
class FakeResourceState:
|
||||
extends RefCounted
|
||||
|
||||
var resources := {&"gold": 1_000, &"materials": 100}
|
||||
var mutate_then_fail_spend := false
|
||||
var fail_apply := false
|
||||
var fail_restore := false
|
||||
var reentrant_service: Variant
|
||||
var reentrant_attempted := false
|
||||
var reentrant_succeeded := false
|
||||
|
||||
func get_resource(resource_name: StringName) -> int:
|
||||
return int(resources.get(resource_name, 0))
|
||||
|
||||
func can_afford(cost: Dictionary) -> bool:
|
||||
for resource_name: Variant in cost:
|
||||
if get_resource(StringName(resource_name)) < int(cost[resource_name]):
|
||||
return false
|
||||
return true
|
||||
|
||||
func spend(cost: Dictionary) -> bool:
|
||||
if reentrant_service != null and not reentrant_attempted:
|
||||
reentrant_attempted = true
|
||||
var nested: Variant = reentrant_service.start_construction(
|
||||
_valid_definition_for_reentry(),
|
||||
Vector3i(2, 2, 0)
|
||||
)
|
||||
reentrant_succeeded = nested.is_success()
|
||||
if mutate_then_fail_spend:
|
||||
resources[&"gold"] = int(resources[&"gold"]) - int(cost.get(&"gold", 0))
|
||||
return false
|
||||
if not can_afford(cost):
|
||||
return false
|
||||
var deltas: Dictionary = {}
|
||||
for resource_name: Variant in cost:
|
||||
deltas[StringName(resource_name)] = -int(cost[resource_name])
|
||||
return apply_resource_deltas(deltas)
|
||||
|
||||
func apply_resource_deltas(deltas: Dictionary) -> bool:
|
||||
if fail_apply:
|
||||
return false
|
||||
var next := resources.duplicate(true)
|
||||
for resource_name: Variant in deltas:
|
||||
var key := StringName(resource_name)
|
||||
next[key] = int(next.get(key, 0)) + int(deltas[resource_name])
|
||||
if int(next[key]) < 0:
|
||||
return false
|
||||
resources = next
|
||||
return true
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {&"resources": resources.duplicate(true)}
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
if fail_restore:
|
||||
return false
|
||||
if not snapshot.get(&"resources", null) is Dictionary:
|
||||
return false
|
||||
resources = snapshot[&"resources"].duplicate(true)
|
||||
return true
|
||||
|
||||
func _valid_definition_for_reentry() -> Dictionary:
|
||||
return {
|
||||
&"id": &"nested_room",
|
||||
&"footprint": Vector2i.ONE,
|
||||
&"zone": FacilityDefinitionType.MapZone.ANY,
|
||||
&"layer": FacilityDefinitionType.ViewLayer.FRONT,
|
||||
&"construction_cost": {&"gold": 100, &"materials": 10},
|
||||
&"build_minutes": 60,
|
||||
&"weight_per_cell": 1,
|
||||
}
|
||||
|
||||
|
||||
class FakeWorld:
|
||||
extends RefCounted
|
||||
|
||||
var min_x := 0
|
||||
var max_x := 8
|
||||
var min_y := -2
|
||||
var max_y := 8
|
||||
var occupancy: Dictionary = {}
|
||||
var unsupported_cells: Array[Vector3i] = []
|
||||
var fail_next_occupy := false
|
||||
var fail_next_release := false
|
||||
var fail_all_occupy := false
|
||||
var excavation: Dictionary = {}
|
||||
var forced_cells: Array[Vector3i] = []
|
||||
|
||||
func cells_for_footprint(
|
||||
origin: Vector3i,
|
||||
footprint: Vector2i,
|
||||
rotation: int,
|
||||
_layer: int
|
||||
) -> Array[Vector3i]:
|
||||
if not forced_cells.is_empty():
|
||||
return forced_cells.duplicate()
|
||||
var width := footprint.x
|
||||
var height := footprint.y
|
||||
if rotation % 180 != 0:
|
||||
width = footprint.y
|
||||
height = footprint.x
|
||||
var cells: Array[Vector3i] = []
|
||||
for y_offset in height:
|
||||
for x_offset in width:
|
||||
cells.append(origin + Vector3i(x_offset, y_offset, 0))
|
||||
return cells
|
||||
|
||||
func is_in_bounds(cell: Vector3i) -> bool:
|
||||
return cell.x >= min_x and cell.x <= max_x and cell.y >= min_y and cell.y <= max_y
|
||||
|
||||
func get_zone(cell: Vector3i) -> int:
|
||||
if cell.y > 0:
|
||||
return FacilityDefinitionType.MapZone.ABOVE_GROUND
|
||||
if cell.y < 0:
|
||||
return FacilityDefinitionType.MapZone.UNDERGROUND
|
||||
return FacilityDefinitionType.MapZone.BOUNDARY
|
||||
|
||||
func get_occupant(cell: Vector3i) -> Variant:
|
||||
return occupancy.get(cell, &"")
|
||||
|
||||
func is_excavated(cell: Vector3i) -> bool:
|
||||
return bool(excavation.get(Vector2i(cell.x, cell.y), true))
|
||||
|
||||
func validate_support(_cells: Array[Vector3i], _weight_per_cell: int) -> Dictionary:
|
||||
return {
|
||||
&"ok": unsupported_cells.is_empty(),
|
||||
&"unsupported_cells": unsupported_cells.duplicate(),
|
||||
&"reason": "Support capacity is insufficient.",
|
||||
}
|
||||
|
||||
func occupy(occupant_id: StringName, cells: Array[Vector3i], _kind: StringName) -> bool:
|
||||
if fail_all_occupy:
|
||||
return false
|
||||
if fail_next_occupy:
|
||||
fail_next_occupy = false
|
||||
return false
|
||||
for cell: Vector3i in cells:
|
||||
if not String(occupancy.get(cell, &"")).is_empty():
|
||||
return false
|
||||
for cell: Vector3i in cells:
|
||||
occupancy[cell] = occupant_id
|
||||
return true
|
||||
|
||||
func release(occupant_id: StringName) -> bool:
|
||||
if fail_next_release:
|
||||
fail_next_release = false
|
||||
return false
|
||||
var cells: Array[Vector3i] = []
|
||||
for cell: Vector3i in occupancy:
|
||||
if occupancy[cell] == occupant_id:
|
||||
cells.append(cell)
|
||||
if cells.is_empty():
|
||||
return false
|
||||
for cell: Vector3i in cells:
|
||||
occupancy.erase(cell)
|
||||
return true
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S1: Construction Service")
|
||||
_test_report_collects_every_reason()
|
||||
_test_start_is_atomic()
|
||||
_test_cancellation_refund()
|
||||
_test_completion_and_demolition()
|
||||
_test_completion_rollback_and_retry()
|
||||
_test_completion_rollback_failure_quarantines()
|
||||
_test_progress_replay_is_idempotent()
|
||||
_test_deferred_completion_commit_and_reentry()
|
||||
_test_deferred_failure_discard_and_restore()
|
||||
_test_ordinary_advance_notifications_unchanged()
|
||||
_test_snapshot_round_trip()
|
||||
_test_snapshot_strictness_and_v1_migration()
|
||||
_test_definition_type_spoof_and_reentry()
|
||||
_test_failed_rollback_quarantines_service()
|
||||
_test_real_world_service_integration()
|
||||
_test_underground_requires_excavation()
|
||||
_test_every_footprint_cell_must_match_zone()
|
||||
_test_boundary_facility_anchor_rule()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_report_collects_every_reason() -> void:
|
||||
var world := FakeWorld.new()
|
||||
world.max_x = 2
|
||||
var state := FakeResourceState.new()
|
||||
state.resources = {&"gold": 0, &"materials": 0}
|
||||
var definition := _definition(
|
||||
&"invalid_location",
|
||||
Vector2i(2, 1),
|
||||
FacilityDefinitionType.MapZone.ABOVE_GROUND,
|
||||
FacilityDefinitionType.ViewLayer.FRONT,
|
||||
{&"gold": 100, &"materials": 10}
|
||||
)
|
||||
var origin := Vector3i(2, -1, FacilityDefinitionType.ViewLayer.BACK)
|
||||
world.occupancy[origin] = &"existing"
|
||||
world.unsupported_cells = [origin]
|
||||
var service := ConstructionServiceType.new(world, state)
|
||||
var report = service.validate_placement(definition, origin)
|
||||
|
||||
_expect(not report.can_build, "invalid placement is rejected")
|
||||
_expect(report.has_error(PlacementReportType.ERROR_OUT_OF_BOUNDS), "bounds error is reported")
|
||||
_expect(report.has_error(PlacementReportType.ERROR_ZONE_MISMATCH), "zone error is reported")
|
||||
_expect(report.has_error(PlacementReportType.ERROR_LAYER_MISMATCH), "layer error is reported")
|
||||
_expect(report.has_error(PlacementReportType.ERROR_OVERLAP), "overlap error is reported")
|
||||
_expect(report.has_error(PlacementReportType.ERROR_UNSUPPORTED), "support error is reported")
|
||||
_expect(
|
||||
report.has_error(PlacementReportType.ERROR_INSUFFICIENT_RESOURCES),
|
||||
"cost error is reported"
|
||||
)
|
||||
_expect(report.reasons.size() == 6, "all independent placement failures are retained")
|
||||
|
||||
|
||||
func _test_start_is_atomic() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var state := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, state)
|
||||
var definition := _valid_definition()
|
||||
var resources_before := state.resources.duplicate(true)
|
||||
world.fail_next_occupy = true
|
||||
var failed = service.start_construction(definition, Vector3i(1, 1, 0))
|
||||
_expect(not failed.is_success(), "failed world reservation rejects construction")
|
||||
_expect(state.resources == resources_before, "failed reservation rolls resource payment back")
|
||||
_expect(world.occupancy.is_empty(), "failed reservation leaves no occupied cell")
|
||||
_expect(service.get_jobs().is_empty(), "failed reservation creates no job")
|
||||
|
||||
var started = service.start_construction(definition, Vector3i(1, 1, 0), 0, 4, 600)
|
||||
_expect(started.is_success(), "valid construction starts")
|
||||
var job = service.get_job(started.details[&"job_id"])
|
||||
_expect(job != null and job.state == ConstructionJobType.State.PLANNED, "new job starts PLANNED")
|
||||
_expect(state.resources == {&"gold": 900, &"materials": 90}, "full construction cost is paid once")
|
||||
_expect(world.get_occupant(Vector3i(1, 1, 0)) == job.job_id, "job atomically reserves its cells")
|
||||
|
||||
|
||||
func _test_cancellation_refund() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var state := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, state)
|
||||
var first = service.start_construction(_valid_definition(), Vector3i(1, 1, 0), 0, 4)
|
||||
var first_job_id: StringName = first.details[&"job_id"]
|
||||
var cancelled = service.cancel_job(first_job_id)
|
||||
_expect(cancelled.is_success(), "zero-progress job cancels")
|
||||
_expect(state.resources == {&"gold": 1_000, &"materials": 100}, "zero-progress cancellation refunds all")
|
||||
_expect(world.occupancy.is_empty(), "cancellation releases reserved cells")
|
||||
|
||||
var second = service.start_construction(_valid_definition(), Vector3i(2, 2, 0), 0, 4)
|
||||
var second_job_id: StringName = second.details[&"job_id"]
|
||||
service.progress_job(second_job_id)
|
||||
var job = service.get_job(second_job_id)
|
||||
_expect(job.progress == 2_500 and job.state == ConstructionJobType.State.BUILDING, "one minute advances exact integer progress")
|
||||
var partial = service.cancel_job(second_job_id)
|
||||
_expect(partial.details[&"refund"] == {&"gold": 75, &"materials": 7}, "partial cancellation refunds the unbuilt ratio")
|
||||
_expect(state.resources == {&"gold": 975, &"materials": 97}, "partial refund is applied once")
|
||||
|
||||
|
||||
func _test_completion_and_demolition() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var state := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, state)
|
||||
var started = service.start_construction(_valid_definition(), Vector3i(3, 3, 0), 0, 2)
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
var first_tick = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 601)
|
||||
_expect(first_tick.is_success() and service.get_job(job_id).progress == 5_000, "first build minute reaches half progress")
|
||||
var second_tick = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 602)
|
||||
var job = service.get_job(job_id)
|
||||
_expect(second_tick.is_success() and job.state == ConstructionJobType.State.COMPLETED, "final minute completes the job")
|
||||
var instance_id: StringName = second_tick.details[&"instance_id"]
|
||||
_expect(not service.get_facility(instance_id).is_empty(), "completion creates a facility instance")
|
||||
_expect(world.get_occupant(Vector3i(3, 3, 0)) == instance_id, "completion replaces job reservation with facility occupancy")
|
||||
|
||||
var demolished = service.demolish_facility(instance_id)
|
||||
_expect(demolished.is_success(), "completed facility can be demolished")
|
||||
_expect(world.occupancy.is_empty(), "demolition releases facility cells")
|
||||
_expect(state.resources == {&"gold": 900, &"materials": 92}, "demolition returns 25 percent of materials")
|
||||
|
||||
|
||||
func _test_completion_rollback_and_retry() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var service := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var started = service.start_construction(_valid_definition(), Vector3i(3, 3, 0), 0, 2)
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 10)
|
||||
world.fail_next_occupy = true
|
||||
var failed = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 11)
|
||||
var job = service.get_job(job_id)
|
||||
_expect(not failed.is_success() and failed.details[&"rolled_back"], "failed completion reports a verified world rollback")
|
||||
_expect(job.state == ConstructionJobType.State.BLOCKED and job.progress == ConstructionJobType.MAX_PROGRESS, "interrupted completion retains all finished work")
|
||||
_expect(world.get_occupant(Vector3i(3, 3, 0)) == job_id, "failed facility transfer restores the construction reservation")
|
||||
var retry = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 12)
|
||||
_expect(retry.is_success() and service.get_job(job_id).state == ConstructionJobType.State.COMPLETED, "a later minute deterministically retries the blocked completion")
|
||||
|
||||
|
||||
func _test_completion_rollback_failure_quarantines() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var service := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var job_id: StringName = service.start_construction(_valid_definition(), Vector3i(3, 3, 0), 0, 2).details[&"job_id"]
|
||||
service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 30)
|
||||
world.fail_all_occupy = true
|
||||
var failed = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 31)
|
||||
_expect(not failed.is_success() and failed.details[&"rollback_failed"], "completion exposes failure to restore its reservation")
|
||||
_expect(service.is_quarantined(), "unrecoverable completion rollback quarantines construction mutations")
|
||||
_expect(not service.cancel_job(job_id).is_success(), "quarantined completion state cannot be mutated further")
|
||||
|
||||
|
||||
func _test_progress_replay_is_idempotent() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var service := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var job_id: StringName = service.start_construction(_valid_definition(), Vector3i(1, 1, 0), 0, 4).details[&"job_id"]
|
||||
service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 20)
|
||||
var progress_once := service.get_job(job_id).progress
|
||||
var replay = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 20)
|
||||
_expect(replay.is_success() and replay.is_replay, "same-minute construction retry is an explicit replay")
|
||||
_expect(service.get_job(job_id).progress == progress_once, "same-minute construction retry cannot add work twice")
|
||||
|
||||
|
||||
func _test_deferred_completion_commit_and_reentry() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var resources := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, resources)
|
||||
var definition := _valid_definition()
|
||||
var started = service.start_construction(definition, Vector3i(1, 1, 0), 0, 1)
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
var before_advance: Dictionary = service.create_snapshot()
|
||||
var order: Array[StringName] = []
|
||||
var delivered_jobs: Array[Dictionary] = []
|
||||
var delivered_facilities: Array[Dictionary] = []
|
||||
var reentry: Dictionary = {}
|
||||
var job_changed_callback := func(job: ConstructionJob) -> void:
|
||||
order.append(&"job_changed")
|
||||
delivered_jobs.append(job.create_snapshot())
|
||||
job.assigned_builder_ids.append(&"listener_tamper")
|
||||
var nested_advance: Array[CommandResult] = service.advance_one_minute(102)
|
||||
reentry[&"advance_rejected"] = (
|
||||
nested_advance.size() == 1 and not nested_advance[0].is_success()
|
||||
)
|
||||
reentry[&"commit_rejected"] = not service.commit_deferred_notifications()
|
||||
reentry[&"discard_rejected"] = not service.discard_deferred_notifications()
|
||||
reentry[&"snapshot_rejected"] = service.create_snapshot().is_empty()
|
||||
reentry[&"restore_rejected"] = not service.restore_snapshot(before_advance)
|
||||
reentry[&"mutation_rejected"] = not service.start_construction(
|
||||
definition, Vector3i(5, 5, 0)
|
||||
).is_success()
|
||||
var facility_completed_callback := func(instance: Dictionary) -> void:
|
||||
order.append(&"facility_completed")
|
||||
delivered_facilities.append(instance.duplicate(true))
|
||||
(instance[&"occupied_cells"] as Array).clear()
|
||||
instance[&"definition_id"] = &"listener_tamper"
|
||||
service.job_changed.connect(job_changed_callback)
|
||||
service.facility_completed.connect(facility_completed_callback)
|
||||
|
||||
var advanced: Array[CommandResult] = service.advance_one_minute(101, {}, true)
|
||||
_expect(
|
||||
advanced.size() == 1 and advanced[0].is_success(),
|
||||
"deferred advance completes construction state"
|
||||
)
|
||||
var instance_id := StringName(advanced[0].details.get(&"instance_id", &""))
|
||||
_expect(
|
||||
order.is_empty() and delivered_jobs.is_empty() and delivered_facilities.is_empty(),
|
||||
"deferred completion emits no raw signal before commit"
|
||||
)
|
||||
_expect(service.has_deferred_notifications(), "deferred completion exposes a pending notification batch")
|
||||
_expect(service.create_snapshot().is_empty(), "pending notifications fail closed for snapshot capture")
|
||||
_expect(
|
||||
not service.restore_snapshot(before_advance),
|
||||
"pending notifications reject snapshot restore"
|
||||
)
|
||||
var minute_before_rejected_advance := service.current_minute
|
||||
var pending_advance: Array[CommandResult] = service.advance_one_minute(102)
|
||||
_expect(
|
||||
pending_advance.size() == 1
|
||||
and not pending_advance[0].is_success()
|
||||
and pending_advance[0].details.get(&"code", &"") == &"DEFERRED_NOTIFICATIONS_PENDING"
|
||||
and service.current_minute == minute_before_rejected_advance,
|
||||
"a pending batch rejects another advance before changing its minute"
|
||||
)
|
||||
_expect(
|
||||
not service.start_construction(definition, Vector3i(5, 5, 0)).is_success(),
|
||||
"a pending batch rejects unrelated mutation until its disposition is chosen"
|
||||
)
|
||||
# get_job historically returns the live aggregate. Mutating it after staging
|
||||
# proves the queued notification captured its own defensive point-in-time copy.
|
||||
service.get_job(job_id).assigned_builder_ids.append(&"late_live_builder")
|
||||
_expect(service.commit_deferred_notifications(), "deferred notifications commit explicitly")
|
||||
_expect(
|
||||
order == [&"job_changed", &"facility_completed"]
|
||||
and delivered_jobs.size() == 1
|
||||
and delivered_facilities.size() == 1,
|
||||
"commit emits completion signals exactly once in Construction order"
|
||||
)
|
||||
_expect(
|
||||
delivered_jobs[0][&"assigned_builder_ids"].is_empty()
|
||||
and service.get_job(job_id).assigned_builder_ids == [&"late_live_builder"],
|
||||
"queued and delivered ConstructionJob payloads are defensive copies"
|
||||
)
|
||||
var live_facility: Dictionary = service.get_facility(instance_id)
|
||||
_expect(
|
||||
live_facility.get(&"definition_id", &"") == definition.id
|
||||
and live_facility.get(&"occupied_cells", []) == [Vector3i(1, 1, 0)],
|
||||
"listener mutation of a deferred facility payload cannot alter live state"
|
||||
)
|
||||
_expect(
|
||||
bool(reentry.get(&"advance_rejected", false))
|
||||
and bool(reentry.get(&"commit_rejected", false))
|
||||
and bool(reentry.get(&"discard_rejected", false))
|
||||
and bool(reentry.get(&"snapshot_rejected", false))
|
||||
and bool(reentry.get(&"restore_rejected", false))
|
||||
and bool(reentry.get(&"mutation_rejected", false)),
|
||||
"signal callbacks cannot reenter notification dispatch, restore, snapshot, or mutation"
|
||||
)
|
||||
var committed_order := order.duplicate()
|
||||
_expect(
|
||||
not service.has_deferred_notifications()
|
||||
and service.commit_deferred_notifications()
|
||||
and order == committed_order,
|
||||
"an empty repeat commit succeeds without emitting twice"
|
||||
)
|
||||
service.job_changed.disconnect(job_changed_callback)
|
||||
service.facility_completed.disconnect(facility_completed_callback)
|
||||
|
||||
|
||||
func _test_deferred_failure_discard_and_restore() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var service := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var started = service.start_construction(_valid_definition(), Vector3i(2, 2, 0), 0, 1)
|
||||
var before_advance: Dictionary = service.create_snapshot()
|
||||
var changed_count := [0]
|
||||
var completed_count := [0]
|
||||
var job_changed_callback := func(_job: ConstructionJob) -> void:
|
||||
changed_count[0] += 1
|
||||
var facility_completed_callback := func(_instance: Dictionary) -> void:
|
||||
completed_count[0] += 1
|
||||
service.job_changed.connect(job_changed_callback)
|
||||
service.facility_completed.connect(facility_completed_callback)
|
||||
world.fail_next_occupy = true
|
||||
var advanced: Array[CommandResult] = service.advance_one_minute(201, {}, true)
|
||||
_expect(
|
||||
advanced.size() == 1 and not advanced[0].is_success(),
|
||||
"a deferred completion retains its internal failure result"
|
||||
)
|
||||
_expect(
|
||||
service.has_deferred_notifications()
|
||||
and changed_count[0] == 0
|
||||
and completed_count[0] == 0,
|
||||
"notifications queued before an internal failure remain silent and pending"
|
||||
)
|
||||
_expect(
|
||||
service.create_snapshot().is_empty()
|
||||
and not service.restore_snapshot(before_advance),
|
||||
"failed deferred advances still block snapshot and restore while pending"
|
||||
)
|
||||
_expect(service.discard_deferred_notifications(), "a failed deferred batch can be discarded")
|
||||
_expect(
|
||||
not service.has_deferred_notifications()
|
||||
and service.discard_deferred_notifications()
|
||||
and service.commit_deferred_notifications()
|
||||
and changed_count[0] == 0
|
||||
and completed_count[0] == 0,
|
||||
"discard is replay-safe and never emits the rejected batch"
|
||||
)
|
||||
_expect(
|
||||
service.restore_snapshot(before_advance)
|
||||
and service.create_snapshot() == before_advance,
|
||||
"discard permits the caller to restore Construction after failure"
|
||||
)
|
||||
service.job_changed.disconnect(job_changed_callback)
|
||||
service.facility_completed.disconnect(facility_completed_callback)
|
||||
|
||||
|
||||
func _test_ordinary_advance_notifications_unchanged() -> void:
|
||||
var service := ConstructionServiceType.new(FakeWorld.new(), FakeResourceState.new())
|
||||
var job_id: StringName = service.start_construction(
|
||||
_valid_definition(), Vector3i(3, 3, 0), 0, 1
|
||||
).details[&"job_id"]
|
||||
var order: Array[StringName] = []
|
||||
var delivered_jobs: Array[ConstructionJob] = []
|
||||
var job_changed_callback := func(job: ConstructionJob) -> void:
|
||||
order.append(&"job_changed")
|
||||
delivered_jobs.append(job)
|
||||
var facility_completed_callback := func(_instance: Dictionary) -> void:
|
||||
order.append(&"facility_completed")
|
||||
service.job_changed.connect(job_changed_callback)
|
||||
service.facility_completed.connect(facility_completed_callback)
|
||||
var advanced: Array[CommandResult] = service.advance_one_minute(301, {})
|
||||
_expect(
|
||||
advanced.size() == 1 and advanced[0].is_success(),
|
||||
"the existing two-argument advance call still completes normally"
|
||||
)
|
||||
_expect(
|
||||
order == [&"job_changed", &"facility_completed"]
|
||||
and delivered_jobs.size() == 1,
|
||||
"ordinary advance still emits both signals immediately in prior order"
|
||||
)
|
||||
_expect(
|
||||
delivered_jobs[0] == service.get_job(job_id),
|
||||
"ordinary job_changed retains its historical live-job payload"
|
||||
)
|
||||
_expect(
|
||||
not service.has_deferred_notifications() and not service.create_snapshot().is_empty(),
|
||||
"ordinary advance creates no ephemeral pending batch"
|
||||
)
|
||||
service.job_changed.disconnect(job_changed_callback)
|
||||
service.facility_completed.disconnect(facility_completed_callback)
|
||||
|
||||
|
||||
func _test_snapshot_round_trip() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var state := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, state)
|
||||
var started = service.start_construction(_valid_definition(), Vector3i(4, 4, 0), 0, 8, 700)
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
service.assign_builders(job_id, [&"builder_b", &"builder_a"])
|
||||
service.progress_job(job_id, 5_000, 701)
|
||||
var snapshot := service.create_snapshot()
|
||||
var restored := ConstructionServiceType.new(world, state)
|
||||
_expect(restored.restore_snapshot(snapshot), "valid construction snapshot restores")
|
||||
_expect(restored.create_snapshot() == snapshot, "snapshot restore preserves all construction state")
|
||||
var before_invalid := restored.create_snapshot()
|
||||
_expect(not restored.restore_snapshot({&"schema_version": 999}), "unsupported snapshot schema is rejected")
|
||||
_expect(restored.create_snapshot() == before_invalid, "rejected restore does not mutate live state")
|
||||
|
||||
|
||||
func _test_snapshot_strictness_and_v1_migration() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var source := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var job_id: StringName = source.start_construction(_valid_definition(), Vector3i(4, 4, 0), 0, 8, 700).details[&"job_id"]
|
||||
source.progress_job(job_id, 5_000, 701)
|
||||
var snapshot := source.create_snapshot()
|
||||
var restored := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var unknown := snapshot.duplicate(true)
|
||||
unknown[&"unknown"] = 1
|
||||
_expect(not restored.restore_snapshot(unknown), "current construction schema rejects unknown top-level keys")
|
||||
var type_spoof := snapshot.duplicate(true)
|
||||
type_spoof[&"schema_version"] = "2"
|
||||
_expect(not restored.restore_snapshot(type_spoof), "current construction schema rejects a string version")
|
||||
var cost_spoof := snapshot.duplicate(true)
|
||||
cost_spoof[&"jobs"][0][&"construction_cost"][&"gold"] = "100"
|
||||
_expect(not restored.restore_snapshot(cost_spoof), "current construction schema rejects numeric-string costs")
|
||||
var derived_tamper := snapshot.duplicate(true)
|
||||
derived_tamper[&"jobs"][0][&"progress"] += 1
|
||||
_expect(not restored.restore_snapshot(derived_tamper), "current construction schema rejects derived progress tampering")
|
||||
var cell_tamper := snapshot.duplicate(true)
|
||||
cell_tamper[&"jobs"][0][&"reserved_cells"] = [[5, 4, 0]]
|
||||
_expect(not restored.restore_snapshot(cell_tamper), "construction restore rejects world reservation cross-link tampering")
|
||||
var high_water_tamper := snapshot.duplicate(true)
|
||||
high_water_tamper[&"next_job_sequence"] = 1
|
||||
_expect(not restored.restore_snapshot(high_water_tamper), "construction restore rejects a sequence below the ID high-water mark")
|
||||
var before := restored.create_snapshot()
|
||||
_expect(before[&"jobs"].is_empty(), "rejected construction snapshots leave the target untouched")
|
||||
|
||||
var legacy := snapshot.duplicate(true)
|
||||
legacy[&"schema_version"] = source.LEGACY_SNAPSHOT_VERSION
|
||||
legacy.erase(&"quarantined")
|
||||
legacy.erase(&"quarantine_reason")
|
||||
for saved_job: Dictionary in legacy[&"jobs"]:
|
||||
saved_job.erase(&"last_progress_at")
|
||||
_expect(restored.restore_snapshot(legacy), "legacy v1 construction snapshot migrates through its exact old shape")
|
||||
_expect(restored.create_snapshot()[&"schema_version"] == source.SNAPSHOT_VERSION, "legacy construction state is re-emitted as strict v2")
|
||||
_expect(restored.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 701).is_replay, "v1 migration conservatively prevents replaying prior work")
|
||||
|
||||
|
||||
func _test_definition_type_spoof_and_reentry() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var resources := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, resources)
|
||||
var spoofed := {
|
||||
&"id": &"spoofed",
|
||||
&"footprint": Vector2i.ONE,
|
||||
&"zone": FacilityDefinitionType.MapZone.ANY,
|
||||
&"layer": FacilityDefinitionType.ViewLayer.FRONT,
|
||||
&"construction_cost": {&"gold": "100", &"materials": 10},
|
||||
&"build_minutes": 60,
|
||||
}
|
||||
_expect(not service.validate_placement(spoofed, Vector3i(1, 1, 0)).can_build, "numeric-string definition costs are rejected instead of normalized")
|
||||
resources.reentrant_service = service
|
||||
var started = service.start_construction(_valid_definition(), Vector3i(1, 1, 0))
|
||||
_expect(started.is_success() and resources.reentrant_attempted, "resource callback attempts a nested construction command")
|
||||
_expect(not resources.reentrant_succeeded and service.get_jobs().size() == 1, "reentrant construction mutation cannot create or charge a second job")
|
||||
resources.reentrant_service = null
|
||||
|
||||
|
||||
func _test_failed_rollback_quarantines_service() -> void:
|
||||
var world := FakeWorld.new()
|
||||
var resources := FakeResourceState.new()
|
||||
resources.mutate_then_fail_spend = true
|
||||
resources.fail_restore = true
|
||||
var service := ConstructionServiceType.new(world, resources)
|
||||
var failed = service.start_construction(_valid_definition(), Vector3i(1, 1, 0))
|
||||
_expect(not failed.is_success() and failed.details[&"rollback_failed"], "failed construction payment reports an unrecoverable rollback")
|
||||
_expect(service.is_quarantined(), "unrecoverable construction rollback quarantines the service")
|
||||
_expect(not service.start_construction(_valid_definition(), Vector3i(2, 2, 0)).is_success(), "quarantined construction service refuses later mutations")
|
||||
|
||||
|
||||
func _test_real_world_service_integration() -> void:
|
||||
var world = WorldServiceType.new()
|
||||
world.reset({&"min_x": 0, &"max_x": 5, &"min_y": 0, &"max_y": 5})
|
||||
world.set_support(Vector3i(1, 1, 0), 0, true)
|
||||
var state := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, state)
|
||||
var definition := _definition(
|
||||
&"rotated_room",
|
||||
Vector2i(2, 1),
|
||||
FacilityDefinitionType.MapZone.ABOVE_GROUND,
|
||||
FacilityDefinitionType.ViewLayer.FRONT,
|
||||
{&"gold": 100, &"materials": 10}
|
||||
)
|
||||
var expected_cells: Array[Vector3i] = [Vector3i(1, 1, 0), Vector3i(1, 2, 0)]
|
||||
var report = service.validate_placement(definition, Vector3i(1, 1, 0), 90)
|
||||
_expect(report.can_build, "real WorldService accepts enum-to-StringName zone matching")
|
||||
_expect(report.cells == expected_cells, "90-degree placement swaps rectangular footprint axes")
|
||||
var report_270 = service.validate_placement(definition, Vector3i(1, 1, 0), 270)
|
||||
_expect(report_270.cells == expected_cells, "270-degree placement swaps rectangular footprint axes")
|
||||
|
||||
var started = service.start_construction(definition, Vector3i(1, 1, 0), 90, 4)
|
||||
_expect(started.is_success(), "real WorldService occupancy signature starts construction")
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
_expect(world.get_entity_cells(job_id) == expected_cells, "real WorldService stores the complete rotated reservation")
|
||||
_expect(service.cancel_job(job_id).is_success(), "real WorldService release signature cancels construction")
|
||||
_expect(world.get_entity_cells(job_id).is_empty(), "real WorldService release removes the reservation")
|
||||
|
||||
var restarted = service.start_construction(definition, Vector3i(1, 1, 0), 270, 1)
|
||||
var restarted_job_id: StringName = restarted.details[&"job_id"]
|
||||
var completed = service.progress_job(restarted_job_id, ConstructionJobType.NORMAL_WORK_RATE, 900)
|
||||
_expect(completed.is_success(), "real WorldService completes a construction reservation")
|
||||
var instance_id: StringName = completed.details[&"instance_id"]
|
||||
_expect(world.get_entity_cells(instance_id) == expected_cells, "completion transfers real world occupancy to the facility")
|
||||
_expect(service.demolish_facility(instance_id).is_success(), "real WorldService facility can be demolished")
|
||||
_expect(world.get_entity_cells(instance_id).is_empty(), "real WorldService demolition releases facility occupancy")
|
||||
|
||||
|
||||
func _test_underground_requires_excavation() -> void:
|
||||
var world = WorldServiceType.new()
|
||||
world.reset({&"min_x": 0, &"max_x": 3, &"min_y": -2, &"max_y": 0})
|
||||
world.set_support(Vector3i(0, -1, 0), 0, true)
|
||||
var state := FakeResourceState.new()
|
||||
var service := ConstructionServiceType.new(world, state)
|
||||
var definition := _definition(
|
||||
&"underground_room",
|
||||
Vector2i(2, 1),
|
||||
FacilityDefinitionType.MapZone.UNDERGROUND,
|
||||
FacilityDefinitionType.ViewLayer.FRONT,
|
||||
{&"gold": 100, &"materials": 10}
|
||||
)
|
||||
var blocked = service.validate_placement(definition, Vector3i(0, -1, 0))
|
||||
_expect(not blocked.can_build, "underground facility cannot occupy solid earth")
|
||||
_expect(blocked.has_error(PlacementReportType.ERROR_NOT_EXCAVATED), "placement report identifies unexcavated footprint cells")
|
||||
_expect(blocked.unexcavated_cells == [Vector3i(0, -1, 0), Vector3i(1, -1, 0)], "every blocked underground footprint cell is reported")
|
||||
world.set_excavated(Vector3i(0, -1, 0), true)
|
||||
var half_open = service.validate_placement(definition, Vector3i(0, -1, 0))
|
||||
_expect(not half_open.can_build and half_open.unexcavated_cells == [Vector3i(1, -1, 0)], "partial excavation is still insufficient")
|
||||
world.set_excavated(Vector3i(1, -1, 1), true)
|
||||
var ready = service.validate_placement(definition, Vector3i(0, -1, 0))
|
||||
_expect(ready.can_build, "fully excavated spatial cells permit underground construction on either layer")
|
||||
|
||||
|
||||
func _test_every_footprint_cell_must_match_zone() -> void:
|
||||
var world = WorldServiceType.new()
|
||||
world.reset({&"min_x": 0, &"max_x": 2, &"min_y": 0, &"max_y": 2})
|
||||
world.set_support(Vector3i(0, 0, 0), 0, true)
|
||||
var service := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var definition := _definition(
|
||||
&"above_ground_tower",
|
||||
Vector2i(1, 2),
|
||||
FacilityDefinitionType.MapZone.ABOVE_GROUND,
|
||||
FacilityDefinitionType.ViewLayer.FRONT,
|
||||
{&"gold": 100, &"materials": 10}
|
||||
)
|
||||
var crossing = service.validate_placement(definition, Vector3i(0, 0, 0))
|
||||
_expect(not crossing.can_build, "a footprint cannot straddle the boundary and above-ground zones")
|
||||
_expect(crossing.zone_mismatch_cells == [Vector3i(0, 0, 0)], "zone validation reports the exact mismatching footprint cell")
|
||||
var valid = service.validate_placement(definition, Vector3i(0, 1, 0))
|
||||
_expect(valid.can_build, "the same full footprint is valid when every cell matches its zone")
|
||||
|
||||
|
||||
func _test_boundary_facility_anchor_rule() -> void:
|
||||
var world := FakeWorld.new()
|
||||
world.max_x = 8
|
||||
var service := ConstructionServiceType.new(world, FakeResourceState.new())
|
||||
var reception := _definition(
|
||||
&"boundary_reception",
|
||||
Vector2i(4, 2),
|
||||
FacilityDefinitionType.MapZone.BOUNDARY,
|
||||
FacilityDefinitionType.ViewLayer.FRONT,
|
||||
{&"gold": 100, &"materials": 10}
|
||||
)
|
||||
var anchored = service.validate_placement(reception, Vector3i(0, 0, 0))
|
||||
_expect(anchored.can_build, "boundary reception anchored at y=0 may extend into above-ground cells")
|
||||
var above_anchor = service.validate_placement(reception, Vector3i(0, 1, 0))
|
||||
_expect(not above_anchor.can_build and above_anchor.has_error(PlacementReportType.ERROR_ZONE_MISMATCH), "boundary reception rejects an above-ground y=1 anchor")
|
||||
var below_anchor = service.validate_placement(reception, Vector3i(0, -1, 0))
|
||||
_expect(not below_anchor.can_build and below_anchor.has_error(PlacementReportType.ERROR_ZONE_MISMATCH), "boundary reception rejects an underground y=-1 anchor")
|
||||
world.forced_cells = [
|
||||
Vector3i(0, -1, 0), Vector3i(1, -1, 0), Vector3i(2, -1, 0), Vector3i(3, -1, 0),
|
||||
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0), Vector3i(3, 0, 0),
|
||||
]
|
||||
var underground_spill = service.validate_placement(reception, Vector3i(0, 0, 0))
|
||||
_expect(not underground_spill.can_build, "boundary footprint may not spill below y=0 even with a valid anchor")
|
||||
_expect(underground_spill.zone_mismatch_cells.has(Vector3i(0, -1, 0)), "boundary spill reports the exact underground cells")
|
||||
|
||||
|
||||
func _definition(
|
||||
id: StringName,
|
||||
footprint: Vector2i,
|
||||
zone: int,
|
||||
layer: int,
|
||||
cost: Dictionary
|
||||
) -> FacilityDefinition:
|
||||
var definition := FacilityDefinitionType.new()
|
||||
definition.id = id
|
||||
definition.display_name = String(id)
|
||||
definition.footprint = footprint
|
||||
definition.zone = zone
|
||||
definition.layer = layer
|
||||
definition.construction_cost = cost.duplicate(true)
|
||||
return definition
|
||||
|
||||
|
||||
func _valid_definition() -> FacilityDefinition:
|
||||
return _definition(
|
||||
&"test_room",
|
||||
Vector2i(1, 1),
|
||||
FacilityDefinitionType.MapZone.ANY,
|
||||
FacilityDefinitionType.ViewLayer.FRONT,
|
||||
{&"gold": 100, &"materials": 10}
|
||||
)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1wplc6ml6vei
|
||||
@@ -0,0 +1,285 @@
|
||||
extends SceneTree
|
||||
|
||||
const ExcavationServiceType = preload("res://scripts/construction/excavation_service.gd")
|
||||
const WorldServiceType = preload("res://scripts/world/world_service.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
class FakeResourceState:
|
||||
extends RefCounted
|
||||
|
||||
var resources := {&"gold": 1_000, &"materials": 100}
|
||||
var mutate_then_fail_spend := false
|
||||
var fail_restore := false
|
||||
var reentrant_service: Variant
|
||||
var reentrant_attempted := false
|
||||
var reentrant_succeeded := false
|
||||
|
||||
func get_resource(resource_name: StringName) -> int:
|
||||
return int(resources.get(resource_name, 0))
|
||||
|
||||
func spend(cost: Dictionary) -> bool:
|
||||
if reentrant_service != null and not reentrant_attempted:
|
||||
reentrant_attempted = true
|
||||
var nested: Variant = reentrant_service.start_excavation(Vector3i(1, -1, 0), 1)
|
||||
reentrant_succeeded = nested.is_success()
|
||||
if mutate_then_fail_spend:
|
||||
resources[&"gold"] = int(resources[&"gold"]) - int(cost.get(&"gold", 0))
|
||||
return false
|
||||
for resource_name: Variant in cost:
|
||||
if get_resource(StringName(resource_name)) < int(cost[resource_name]):
|
||||
return false
|
||||
for resource_name: Variant in cost:
|
||||
var key := StringName(resource_name)
|
||||
resources[key] = int(resources[key]) - int(cost[resource_name])
|
||||
return true
|
||||
|
||||
func apply_resource_deltas(deltas: Dictionary) -> bool:
|
||||
var candidate := resources.duplicate(true)
|
||||
for resource_name: Variant in deltas:
|
||||
var key := StringName(resource_name)
|
||||
candidate[key] = int(candidate.get(key, 0)) + int(deltas[resource_name])
|
||||
if int(candidate[key]) < 0:
|
||||
return false
|
||||
resources = candidate
|
||||
return true
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {&"resources": resources.duplicate(true)}
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
if fail_restore:
|
||||
return false
|
||||
if not snapshot.get(&"resources", null) is Dictionary:
|
||||
return false
|
||||
resources = (snapshot[&"resources"] as Dictionary).duplicate(true)
|
||||
return true
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S1: Excavation Service")
|
||||
_test_rules_adjacency_and_two_layer_unlock()
|
||||
_test_disconnected_island_is_rejected()
|
||||
_test_pause_resume_preserves_progress()
|
||||
_test_hard_width_requires_support()
|
||||
_test_hard_cluster_is_not_a_four_cell_span()
|
||||
_test_progress_replay_is_idempotent()
|
||||
_test_snapshot_round_trip_and_atomic_rejection()
|
||||
_test_snapshot_schema_strictness_and_v1_migration()
|
||||
_test_reentry_and_failed_rollback_quarantine()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_rules_adjacency_and_two_layer_unlock() -> void:
|
||||
var world := _world(0, 3, -3, 0)
|
||||
var resources := FakeResourceState.new()
|
||||
var service := ExcavationServiceType.new(world, resources)
|
||||
_expect(service.get_rule(0) == {&"gold": 20, &"materials": 1, &"minutes": 30}, "hardness 0 uses the canonical cost and time")
|
||||
_expect(service.get_rule(1) == {&"gold": 50, &"materials": 1, &"minutes": 60}, "hardness 1 uses the canonical cost and time")
|
||||
_expect(service.get_rule(2) == {&"gold": 100, &"materials": 2, &"minutes": 120}, "hardness 2 uses the canonical cost and time")
|
||||
_expect(service.get_rule(3) == {&"gold": 200, &"materials": 4, &"minutes": 240}, "hardness 3 uses the canonical cost and time")
|
||||
_expect(service.set_hardness(Vector2i(0, -1), 1), "underground hardness can be assigned before work")
|
||||
var report := service.validate_excavation(Vector3i(0, -1, 1))
|
||||
_expect(report[&"ok"] and report[&"entrance_connected"], "first underground cell connects to the boundary entrance")
|
||||
_expect(report[&"cost"] == {&"gold": 50, &"materials": 1} and report[&"build_minutes"] == 60, "validation exposes exact hardness tuning")
|
||||
var started := service.start_excavation(Vector3i(0, -1, 0), 360)
|
||||
_expect(started.is_success(), "valid connected excavation starts")
|
||||
_expect(resources.resources == {&"gold": 950, &"materials": 99}, "excavation payment is charged exactly once")
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
for minute: int in range(360, 420):
|
||||
service.progress_job(job_id, service.NORMAL_WORK_RATE, minute)
|
||||
_expect(world.is_excavated(Vector3i(0, -1, 0)), "completed excavation unlocks FRONT")
|
||||
_expect(world.is_excavated(Vector3i(0, -1, 1)), "the same completed excavation unlocks BACK")
|
||||
_expect(service.get_job(job_id)[&"state"] == service.STATE_COMPLETED, "completed job becomes terminal")
|
||||
_expect(not service.validate_excavation(Vector3i(0, -1, 0))[&"ok"], "an excavated cell cannot be charged twice")
|
||||
_expect(service.validate_invariants()[&"ok"], "completed excavation retains valid invariants")
|
||||
|
||||
|
||||
func _test_disconnected_island_is_rejected() -> void:
|
||||
var world := _world(0, 4, -4, 0)
|
||||
var resources := FakeResourceState.new()
|
||||
var service := ExcavationServiceType.new(world, resources)
|
||||
world.set_excavated(Vector3i(4, -4, 0), true)
|
||||
var report := service.validate_excavation(Vector3i(3, -4, 0))
|
||||
_expect(not report[&"ok"], "an island next to an excavated cell is still rejected")
|
||||
_expect(report[&"errors"].has(service.ERROR_NO_ENTRANCE_CONNECTION), "island rejection identifies the missing entrance connection")
|
||||
_expect(resources.resources == {&"gold": 1_000, &"materials": 100}, "failed validation changes no resources")
|
||||
var no_neighbor := service.validate_excavation(Vector3i(1, -3, 0))
|
||||
_expect(no_neighbor[&"errors"].has(service.ERROR_NO_EXCAVATED_NEIGHBOR), "a cell without any excavated neighbor has a distinct reason")
|
||||
|
||||
|
||||
func _test_pause_resume_preserves_progress() -> void:
|
||||
var world := _world(0, 2, -2, 0)
|
||||
var resources := FakeResourceState.new()
|
||||
var service := ExcavationServiceType.new(world, resources)
|
||||
var started := service.start_excavation(Vector3i(0, -1, 0), 100)
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
for minute: int in range(100, 110):
|
||||
service.progress_job(job_id, service.NORMAL_WORK_RATE, minute)
|
||||
var progress_before := int(service.get_job(job_id)[&"progress"])
|
||||
_expect(progress_before > 0 and progress_before < service.MAX_PROGRESS, "work accumulates as deterministic fixed-point progress")
|
||||
_expect(service.pause_job(job_id).is_success(), "active excavation can be paused")
|
||||
service.advance_one_minute(111)
|
||||
_expect(service.get_job(job_id)[&"progress"] == progress_before, "paused time does not discard or advance progress")
|
||||
_expect(service.resume_job(job_id).is_success(), "connected paused excavation can resume")
|
||||
for minute: int in range(112, 132):
|
||||
service.progress_job(job_id, service.NORMAL_WORK_RATE, minute)
|
||||
_expect(service.get_job(job_id)[&"state"] == service.STATE_COMPLETED, "resumed work completes from retained progress")
|
||||
_expect(service.pause_job(job_id).details[&"code"] == &"TERMINAL_STATE", "completed excavation cannot return to a mutable state")
|
||||
|
||||
|
||||
func _test_hard_width_requires_support() -> void:
|
||||
var world := _world(0, 4, -1, 0)
|
||||
var resources := FakeResourceState.new()
|
||||
var service := ExcavationServiceType.new(world, resources)
|
||||
for x: int in range(4):
|
||||
_expect(service.set_hardness(Vector2i(x, -1), 2), "hard cell %d is configured" % x)
|
||||
for x: int in range(3):
|
||||
world.set_excavated(Vector3i(x, -1, 0), true)
|
||||
var unsupported := service.validate_excavation(Vector3i(3, -1, 0))
|
||||
_expect(unsupported[&"support_required"], "four connected hardness-2 cells trigger support")
|
||||
_expect(unsupported[&"errors"].has(service.ERROR_SUPPORT_REQUIRED), "unsupported hard span cannot start")
|
||||
world.set_support(Vector3i(2, -1, 1), 1)
|
||||
var supported := service.validate_excavation(Vector3i(3, -1, 0))
|
||||
_expect(supported[&"ok"] and supported[&"support_required"], "nearby support makes the same hard span legal")
|
||||
_expect(service.start_excavation(Vector3i(3, -1, 1)).is_success(), "supported hard excavation can reserve its spatial cell")
|
||||
_expect(not service.start_excavation(Vector3i(3, -1, 0)).is_success(), "FRONT and BACK cannot reserve the same spatial excavation twice")
|
||||
|
||||
|
||||
func _test_hard_cluster_is_not_a_four_cell_span() -> void:
|
||||
var world := _world(0, 1, -2, 0)
|
||||
var service := ExcavationServiceType.new(world, FakeResourceState.new())
|
||||
for position: Vector2i in [Vector2i(0, -1), Vector2i(1, -1), Vector2i(0, -2), Vector2i(1, -2)]:
|
||||
service.set_hardness(position, 2)
|
||||
world.set_excavated(Vector3i(0, -1, 0), true)
|
||||
world.set_excavated(Vector3i(1, -1, 0), true)
|
||||
world.set_excavated(Vector3i(0, -2, 0), true)
|
||||
var report := service.validate_excavation(Vector3i(1, -2, 0))
|
||||
_expect(report[&"ok"], "a connected 2x2 hard cluster remains excavatable without support")
|
||||
_expect(not report[&"support_required"], "support is based on four collinear cells, not component cardinality")
|
||||
|
||||
|
||||
func _test_progress_replay_is_idempotent() -> void:
|
||||
var world := _world(0, 2, -2, 0)
|
||||
var service := ExcavationServiceType.new(world, FakeResourceState.new())
|
||||
var started := service.start_excavation(Vector3i(0, -1, 0), 5)
|
||||
var job_id: StringName = started.details[&"job_id"]
|
||||
var first := service.progress_job(job_id, service.NORMAL_WORK_RATE, 10)
|
||||
var progress_once := int(service.get_job(job_id)[&"progress"])
|
||||
var duplicate := service.progress_job(job_id, service.NORMAL_WORK_RATE, 10)
|
||||
_expect(first.is_success() and duplicate.is_success() and duplicate.is_replay, "same-minute excavation retry is an explicit replay")
|
||||
_expect(service.get_job(job_id)[&"progress"] == progress_once, "same-minute retry cannot add work twice")
|
||||
var snapshot := service.create_snapshot()
|
||||
var restored := ExcavationServiceType.new(world, FakeResourceState.new())
|
||||
_expect(restored.restore_snapshot(snapshot), "last processed excavation minute survives snapshot restore")
|
||||
var restored_retry := restored.progress_job(job_id, service.NORMAL_WORK_RATE, 10)
|
||||
_expect(restored_retry.is_replay and restored.get_job(job_id)[&"progress"] == progress_once, "restored service also rejects the already-applied minute")
|
||||
|
||||
|
||||
func _test_snapshot_round_trip_and_atomic_rejection() -> void:
|
||||
var world := _world(0, 3, -2, 0)
|
||||
var resources := FakeResourceState.new()
|
||||
var source := ExcavationServiceType.new(world, resources)
|
||||
source.set_hardness(Vector2i(0, -1), 1)
|
||||
var completed_id: StringName = source.start_excavation(Vector3i(0, -1, 0), 10).details[&"job_id"]
|
||||
for minute: int in range(10, 70):
|
||||
source.progress_job(completed_id, source.NORMAL_WORK_RATE, minute)
|
||||
source.set_hardness(Vector2i(0, -2), 3)
|
||||
var active_id: StringName = source.start_excavation(Vector3i(0, -2, 0), 70).details[&"job_id"]
|
||||
for minute: int in range(70, 75):
|
||||
source.progress_job(active_id, source.NORMAL_WORK_RATE, minute)
|
||||
var snapshot := source.create_snapshot()
|
||||
var restored := ExcavationServiceType.new(world, resources)
|
||||
_expect(restored.restore_snapshot(snapshot), "valid excavation snapshot restores against the same world")
|
||||
_expect(restored.create_snapshot() == snapshot, "excavation snapshot round trip is exact")
|
||||
_expect(restored.validate_invariants()[&"ok"], "restored active and completed jobs satisfy cross-links")
|
||||
|
||||
var before := restored.create_snapshot()
|
||||
var corrupt := snapshot.duplicate(true)
|
||||
corrupt[&"jobs"][1][&"progress"] = 9_999
|
||||
_expect(not restored.restore_snapshot(corrupt), "derived progress tampering is rejected")
|
||||
_expect(restored.create_snapshot() == before, "failed excavation restore preserves the live state atomically")
|
||||
var duplicate := snapshot.duplicate(true)
|
||||
duplicate[&"jobs"].append(duplicate[&"jobs"][1].duplicate(true))
|
||||
_expect(not restored.restore_snapshot(duplicate), "duplicate job identity is rejected")
|
||||
_expect(restored.create_snapshot() == before, "all rejected snapshot shapes remain non-mutating")
|
||||
|
||||
|
||||
func _test_snapshot_schema_strictness_and_v1_migration() -> void:
|
||||
var world := _world(0, 2, -2, 0)
|
||||
var source := ExcavationServiceType.new(world, FakeResourceState.new())
|
||||
var job_id: StringName = source.start_excavation(Vector3i(0, -1, 0), 10).details[&"job_id"]
|
||||
source.progress_job(job_id, source.NORMAL_WORK_RATE, 11)
|
||||
var snapshot := source.create_snapshot()
|
||||
var restored := ExcavationServiceType.new(world, FakeResourceState.new())
|
||||
var unknown := snapshot.duplicate(true)
|
||||
unknown[&"unknown"] = 1
|
||||
_expect(not restored.restore_snapshot(unknown), "current excavation schema rejects unknown top-level keys")
|
||||
var type_spoof := snapshot.duplicate(true)
|
||||
type_spoof[&"schema_version"] = "2"
|
||||
_expect(not restored.restore_snapshot(type_spoof), "current excavation schema rejects a string version")
|
||||
var cost_spoof := snapshot.duplicate(true)
|
||||
cost_spoof[&"jobs"][0][&"cost"][&"gold"] = "20"
|
||||
_expect(not restored.restore_snapshot(cost_spoof), "current excavation schema rejects numeric-string costs")
|
||||
var derived_tamper := snapshot.duplicate(true)
|
||||
derived_tamper[&"jobs"][0][&"build_minutes"] = 31
|
||||
_expect(not restored.restore_snapshot(derived_tamper), "current excavation schema rejects canonical-rule tampering")
|
||||
var legacy := snapshot.duplicate(true)
|
||||
legacy[&"schema_version"] = source.LEGACY_SNAPSHOT_VERSION
|
||||
legacy.erase(&"quarantined")
|
||||
legacy.erase(&"quarantine_reason")
|
||||
for saved_job: Dictionary in legacy[&"jobs"]:
|
||||
saved_job.erase(&"last_progress_at")
|
||||
_expect(restored.restore_snapshot(legacy), "legacy v1 excavation snapshot migrates through its exact old shape")
|
||||
_expect(restored.create_snapshot()[&"schema_version"] == source.SNAPSHOT_VERSION, "legacy excavation state is re-emitted as strict v2")
|
||||
_expect(restored.progress_job(job_id, source.NORMAL_WORK_RATE, 11).is_replay, "v1 migration conservatively prevents replaying prior progress")
|
||||
|
||||
|
||||
func _test_reentry_and_failed_rollback_quarantine() -> void:
|
||||
var world := _world(0, 2, -2, 0)
|
||||
var reentrant_resources := FakeResourceState.new()
|
||||
var service := ExcavationServiceType.new(world, reentrant_resources)
|
||||
reentrant_resources.reentrant_service = service
|
||||
var started := service.start_excavation(Vector3i(0, -1, 0), 1)
|
||||
_expect(started.is_success() and reentrant_resources.reentrant_attempted, "resource callback attempts a nested excavation command")
|
||||
_expect(not reentrant_resources.reentrant_succeeded and service.get_jobs().size() == 1, "reentrant excavation mutation is rejected before a second charge or reservation")
|
||||
reentrant_resources.reentrant_service = null
|
||||
|
||||
var broken_resources := FakeResourceState.new()
|
||||
broken_resources.mutate_then_fail_spend = true
|
||||
broken_resources.fail_restore = true
|
||||
var broken := ExcavationServiceType.new(_world(0, 1, -1, 0), broken_resources)
|
||||
var failed := broken.start_excavation(Vector3i(0, -1, 0), 2)
|
||||
_expect(not failed.is_success() and failed.details[&"rollback_failed"], "failed excavation payment reports an unrecoverable rollback")
|
||||
_expect(broken.is_quarantined(), "unrecoverable excavation rollback quarantines the service")
|
||||
_expect(not broken.start_excavation(Vector3i(1, -1, 0), 3).is_success(), "quarantined excavation service refuses later mutations")
|
||||
|
||||
|
||||
func _world(min_x: int, max_x: int, min_y: int, max_y: int) -> WorldService:
|
||||
var world := WorldServiceType.new()
|
||||
world.reset({
|
||||
&"min_x": min_x,
|
||||
&"max_x": max_x,
|
||||
&"min_y": min_y,
|
||||
&"max_y": max_y,
|
||||
})
|
||||
return world
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5tbd0rocd1ir
|
||||
@@ -0,0 +1,571 @@
|
||||
extends SceneTree
|
||||
|
||||
const ProjectorType = preload("res://scripts/world/facility_infrastructure_projector.gd")
|
||||
const AdapterType = preload("res://scripts/world/route_subslot_path_adapter.gd")
|
||||
const SpatialType = preload("res://scripts/world/spatial_subslot_service.gd")
|
||||
const PathType = preload("res://scripts/world/path_service.gd")
|
||||
const WorldType = preload("res://scripts/world/world_service.gd")
|
||||
const FacilityDefinitionType = preload("res://scripts/data/facility_definition.gd")
|
||||
const CommandResultType = preload("res://scripts/core/command_result.gd")
|
||||
|
||||
const GUEST_CORRIDOR = preload("res://data/facilities/guest_corridor.tres")
|
||||
const SERVICE_CORRIDOR = preload("res://data/facilities/service_corridor.tres")
|
||||
const STAFF_DOOR = preload("res://data/facilities/staff_door.tres")
|
||||
const GUEST_ELEVATOR = preload("res://data/facilities/guest_elevator.tres")
|
||||
const SERVICE_ELEVATOR = preload("res://data/facilities/service_elevator.tres")
|
||||
|
||||
|
||||
class ControlledPathService:
|
||||
extends PathService
|
||||
|
||||
var fail_next_rebuild_after_mutation := false
|
||||
var fail_next_connector_after_mutation := false
|
||||
var fail_next_remove_after_mutation := false
|
||||
var reject_restore := false
|
||||
var reenter_next_connector := false
|
||||
var tamper_projector_next_connector := false
|
||||
var projector: Variant = null
|
||||
var reentry_result: Variant = null
|
||||
|
||||
func rebuild_graph(passable_cells: Variant) -> bool:
|
||||
var should_fail := fail_next_rebuild_after_mutation
|
||||
fail_next_rebuild_after_mutation = false
|
||||
var result := super.rebuild_graph(passable_cells)
|
||||
if should_fail:
|
||||
last_error = "Injected graph rebuild failure after mutation."
|
||||
return false
|
||||
return result
|
||||
|
||||
func configure_connector(
|
||||
connector_id: StringName,
|
||||
from_cell: Vector3i,
|
||||
to_cell: Vector3i,
|
||||
cost := DEFAULT_CONNECTOR_COST,
|
||||
bidirectional := true,
|
||||
access_id: StringName = &"",
|
||||
enabled := true,
|
||||
options: Dictionary = {}
|
||||
) -> bool:
|
||||
var should_fail := fail_next_connector_after_mutation
|
||||
fail_next_connector_after_mutation = false
|
||||
var should_reenter := reenter_next_connector
|
||||
reenter_next_connector = false
|
||||
var should_tamper := tamper_projector_next_connector
|
||||
tamper_projector_next_connector = false
|
||||
var result := super.configure_connector(
|
||||
connector_id, from_cell, to_cell, cost, bidirectional, access_id, enabled, options
|
||||
)
|
||||
if should_reenter and projector != null:
|
||||
reentry_result = projector.unproject_facility(&"nested_unproject", &"facility_nested")
|
||||
if should_tamper and projector != null:
|
||||
projector.projection_revision += 100
|
||||
if should_fail:
|
||||
last_error = "Injected connector failure after mutation."
|
||||
return false
|
||||
return result
|
||||
|
||||
func remove_connector(connector_id: StringName) -> bool:
|
||||
var should_fail := fail_next_remove_after_mutation
|
||||
fail_next_remove_after_mutation = false
|
||||
var result := super.remove_connector(connector_id)
|
||||
if should_fail:
|
||||
last_error = "Injected connector removal failure after mutation."
|
||||
return false
|
||||
return result
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
if reject_restore:
|
||||
last_error = "Injected Path snapshot restore rejection."
|
||||
return false
|
||||
return super.restore_snapshot(snapshot)
|
||||
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S1: completed-facility infrastructure projection")
|
||||
_test_corridors_and_primary_world_coexistence()
|
||||
_test_staff_door_access_and_stable_ids()
|
||||
_test_project_unproject_replay_and_defensive_queries()
|
||||
_test_vertical_topology_rejects_unchanged()
|
||||
_test_definition_and_instance_crosslink_validation()
|
||||
_test_project_and_unproject_failures_roll_back_exactly()
|
||||
_test_callback_reentry_tamper_and_quarantine_recovery()
|
||||
_test_strict_snapshot_round_trip_and_atomic_rejection()
|
||||
_test_configuration_and_stale_dependency_boundaries()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_corridors_and_primary_world_coexistence() -> void:
|
||||
var front_start := Vector3i(0, 0, PathType.FRONT)
|
||||
var front_mid := Vector3i(1, 0, PathType.FRONT)
|
||||
var front_goal := Vector3i(2, 0, PathType.FRONT)
|
||||
var back_start := Vector3i(0, 2, PathType.BACK)
|
||||
var back_mid := Vector3i(1, 2, PathType.BACK)
|
||||
var back_goal := Vector3i(2, 2, PathType.BACK)
|
||||
var context := _context([front_start, front_goal, back_start, back_goal])
|
||||
var world := WorldType.new()
|
||||
world.reset({&"min_x": -2, &"max_x": 4, &"min_y": -2, &"max_y": 4})
|
||||
_expect(world.occupy(&"facility_00000001", [front_mid], &"facility"), "completed facility occupies the primary World slot")
|
||||
var guest_instance := _instance(GUEST_CORRIDOR, &"facility_00000001", front_mid, 10, &"construction_00000001")
|
||||
var guest_result = context.projector.project_facility(&"project_guest", GUEST_CORRIDOR, guest_instance)
|
||||
_expect(guest_result.is_success(), "guest corridor projects through the atomic boundary")
|
||||
_expect(context.path.find_path(front_start, front_goal) == [front_start, front_mid, front_goal], "guest corridor makes the real FRONT Path reachable")
|
||||
_expect(world.get_occupant(front_mid) == &"facility_00000001", "ROUTE subslot projection preserves primary World occupancy")
|
||||
var guest_projection: Dictionary = context.projector.get_projection(&"facility_00000001")
|
||||
_expect(guest_projection[&"layer"] == PathType.FRONT and not guest_projection[&"staff_only"], "guest corridor metadata preserves FRONT guest meaning")
|
||||
|
||||
var service_instance := _instance(SERVICE_CORRIDOR, &"facility_00000002", back_mid, 11, &"construction_00000002")
|
||||
var service_result = context.projector.project_facility(&"project_service", SERVICE_CORRIDOR, service_instance)
|
||||
_expect(service_result.is_success(), "service corridor projects through the same boundary")
|
||||
_expect(context.path.find_path(back_start, back_goal) == [back_start, back_mid, back_goal], "service corridor makes the real BACK Path reachable")
|
||||
_expect(context.path.find_path(back_start, back_goal, [PathType.FRONT]).is_empty(), "FRONT-only guest profile cannot use the staff-only BACK corridor")
|
||||
var service_projection: Dictionary = context.projector.get_projection(&"facility_00000002")
|
||||
_expect(service_projection[&"layer"] == PathType.BACK and service_projection[&"staff_only"], "service corridor retains explicit staff-only semantics")
|
||||
_expect(context.path.find_path(front_start, back_goal, [PathType.FRONT, PathType.BACK]).is_empty(), "FRONT and BACK remain separated without a staff door")
|
||||
_expect(context.projector.projection_revision == 2, "corridor projections advance one deterministic revision each")
|
||||
_expect(context.projector.validate_invariants()[&"ok"], "corridor route/entity/cell crosslinks are valid")
|
||||
|
||||
|
||||
func _test_staff_door_access_and_stable_ids() -> void:
|
||||
var front := Vector3i(5, 0, PathType.FRONT)
|
||||
var back := Vector3i(5, 0, PathType.BACK)
|
||||
var context := _context([front])
|
||||
var instance := _instance(STAFF_DOOR, &"facility_00000003", back, 20, &"construction_00000003")
|
||||
var result = context.projector.project_facility(&"project_staff_door", STAFF_DOOR, instance)
|
||||
_expect(result.is_success(), "staff door projects its BACK endpoint and connector atomically")
|
||||
var projection: Dictionary = context.projector.get_projection(&"facility_00000003")
|
||||
_expect(projection[&"route_entity_id"] == &"infra.route:facility_00000003", "staff door route entity ID is stable")
|
||||
_expect(projection[&"connector_id"] == &"infra.connector:facility_00000003", "staff door connector ID is stable")
|
||||
_expect(context.source.get_entity_cells(projection[&"route_entity_id"]) == [back], "staff door owns exactly its BACK ROUTE endpoint")
|
||||
var connector: Dictionary = context.path.get_connector(projection[&"connector_id"])
|
||||
_expect(connector[&"from"] == front and connector[&"to"] == back, "connector joins the same coordinate across FRONT/BACK")
|
||||
_expect(connector[&"cost"] == 120 and connector[&"bidirectional"] and connector[&"access_id"] == &"staff", "connector is bidirectional, cost 120, and staff-gated")
|
||||
_expect(context.path.find_path(front, back, [PathType.FRONT, PathType.BACK], []).is_empty(), "guest access cannot traverse the staff door")
|
||||
_expect(context.path.last_no_route_reason != context.path.ROUTE_OK, "guest denial reports a real no-route reason rather than fake reachability")
|
||||
_expect(context.path.find_path(front, back, [PathType.FRONT, PathType.BACK], [&"staff"]) == [front, back], "staff connector access traverses the real door")
|
||||
_expect(context.path.last_path_cost == 120, "staff door route report uses the exact specification cost")
|
||||
_expect(context.path.calculate_path_cost([front, back], [&"staff"]) == 120, "explicit Path cost calculation preserves staff-door cost 120")
|
||||
_expect(context.projector.validate_invariants()[&"ok"], "staff-door connector and route crosslinks are exact")
|
||||
|
||||
|
||||
func _test_project_unproject_replay_and_defensive_queries() -> void:
|
||||
var cell := Vector3i(1, 4, PathType.FRONT)
|
||||
var context := _context([])
|
||||
var instance := _instance(GUEST_CORRIDOR, &"facility_00000004", cell, 30, &"construction_00000004")
|
||||
var first = context.projector.project_facility(&"idem_project", GUEST_CORRIDOR, instance)
|
||||
var after_projector: Dictionary = context.projector.create_snapshot()
|
||||
var after_source: Dictionary = context.source.create_snapshot()
|
||||
var after_adapter: Dictionary = context.adapter.create_snapshot()
|
||||
var after_path: Dictionary = context.path.create_snapshot()
|
||||
var replay = context.projector.project_facility(&"idem_project", GUEST_CORRIDOR, instance)
|
||||
_expect(first.is_success() and replay.is_success() and replay.is_replay, "same project request and fingerprint replays")
|
||||
_expect(replay.details == first.details, "project replay preserves exact result details")
|
||||
_expect(context.projector.create_snapshot() == after_projector, "project replay does not advance projector state")
|
||||
_expect(context.source.create_snapshot() == after_source and context.adapter.create_snapshot() == after_adapter and context.path.create_snapshot() == after_path, "project replay leaves all three dependencies exact")
|
||||
var changed_instance := instance.duplicate(true)
|
||||
changed_instance[&"completed_at"] = 31
|
||||
var conflict = context.projector.project_facility(&"idem_project", GUEST_CORRIDOR, changed_instance)
|
||||
_expect(conflict.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "same request ID with changed completed input conflicts")
|
||||
var duplicate = context.projector.project_facility(&"another_project", GUEST_CORRIDOR, instance)
|
||||
_expect(duplicate.code == CommandResultType.Code.DUPLICATE_COMMAND_ID, "another request cannot double-project one facility")
|
||||
|
||||
var returned: Dictionary = context.projector.get_projection(&"facility_00000004")
|
||||
returned[&"cells"].clear()
|
||||
var ids: Array[StringName] = context.projector.get_projection_ids()
|
||||
ids.clear()
|
||||
var status: Dictionary = context.projector.get_status()
|
||||
status[&"projection_count"] = 99
|
||||
_expect(context.projector.get_projection(&"facility_00000004")[&"cells"] == [cell], "projection query is a defensive deep copy")
|
||||
_expect(context.projector.get_projection_ids() == [&"facility_00000004"], "projection ID query is defensive and lexical")
|
||||
_expect(context.projector.get_status()[&"projection_count"] == 1, "status query is defensive")
|
||||
|
||||
var removed = context.projector.unproject_facility(&"idem_remove", &"facility_00000004")
|
||||
_expect(removed.is_success(), "facility infrastructure unprojects atomically")
|
||||
_expect(not context.source.has_entity(&"infra.route:facility_00000004") and not context.path.has_cell(cell), "unproject removes the route from both source and real Path")
|
||||
var removed_snapshot: Dictionary = context.projector.create_snapshot()
|
||||
var remove_replay = context.projector.unproject_facility(&"idem_remove", &"facility_00000004")
|
||||
_expect(remove_replay.is_success() and remove_replay.is_replay, "same unproject request replays after the entity is gone")
|
||||
_expect(context.projector.create_snapshot() == removed_snapshot, "unproject replay is mutation-free")
|
||||
var remove_conflict = context.projector.unproject_facility(&"idem_remove", &"facility_other")
|
||||
_expect(remove_conflict.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "unproject request conflict is rejected")
|
||||
_expect(context.projector.projection_revision == 2, "project and unproject each advance exactly once")
|
||||
|
||||
|
||||
func _test_vertical_topology_rejects_unchanged() -> void:
|
||||
var context := _context([Vector3i(0, 0, PathType.FRONT)])
|
||||
var before_projector: Dictionary = context.projector.create_snapshot()
|
||||
var before_source: Dictionary = context.source.create_snapshot()
|
||||
var before_adapter: Dictionary = context.adapter.create_snapshot()
|
||||
var before_path: Dictionary = context.path.create_snapshot()
|
||||
var guest_instance := _instance(GUEST_ELEVATOR, &"facility_00000005", Vector3i(4, 4, PathType.FRONT), 40, &"construction_00000005")
|
||||
var guest = context.projector.project_facility(&"guest_elevator", GUEST_ELEVATOR, guest_instance)
|
||||
_expect(guest.code == CommandResultType.Code.VALIDATION_FAILED and guest.details[&"reason"] == context.projector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY, "guest elevator explicitly reports unsupported vertical topology")
|
||||
var service_instance := _instance(SERVICE_ELEVATOR, &"facility_00000006", Vector3i(8, 4, PathType.BACK), 41, &"construction_00000006")
|
||||
var service = context.projector.project_facility(&"service_elevator", SERVICE_ELEVATOR, service_instance)
|
||||
_expect(service.code == CommandResultType.Code.VALIDATION_FAILED and service.details[&"reason"] == context.projector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY, "service elevator explicitly reports unsupported vertical topology")
|
||||
_expect(context.projector.create_snapshot() == before_projector, "unsupported vertical facilities leave projector state unchanged")
|
||||
_expect(context.source.create_snapshot() == before_source and context.adapter.create_snapshot() == before_adapter and context.path.create_snapshot() == before_path, "unsupported vertical facilities leave all authoritative dependencies unchanged")
|
||||
_expect(context.projector.get_processed_request_ids().is_empty(), "unsupported vertical rejection does not consume request IDs")
|
||||
|
||||
|
||||
func _test_definition_and_instance_crosslink_validation() -> void:
|
||||
var context := _context([])
|
||||
var base_instance := _instance(GUEST_CORRIDOR, &"facility_00000007", Vector3i(0, 6, PathType.FRONT), 50, &"construction_00000007")
|
||||
var before := _all_snapshots(context)
|
||||
var wrong_layer: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
|
||||
wrong_layer.layer = FacilityDefinitionType.ViewLayer.BACK
|
||||
_expect(not context.projector.project_facility(&"bad_definition_layer", wrong_layer, base_instance).is_success(), "definition layer mismatch is rejected")
|
||||
var wrong_tags: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
|
||||
wrong_tags.tags = [&"corridor", &"movement", &"route_subslot", &"schema_facility_proxy"]
|
||||
_expect(not context.projector.project_facility(&"bad_definition_tags", wrong_tags, base_instance).is_success(), "definition tag mismatch is rejected")
|
||||
var wrong_pass: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
|
||||
wrong_pass.pass_through = false
|
||||
_expect(not context.projector.project_facility(&"bad_definition_pass", wrong_pass, base_instance).is_success(), "definition pass-through mismatch is rejected")
|
||||
var unknown: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
|
||||
unknown.id = &"unknown_proxy"
|
||||
_expect(not context.projector.project_facility(&"unknown_definition", unknown, base_instance).is_success(), "unknown proxy definition is rejected")
|
||||
|
||||
var extra := base_instance.duplicate(true)
|
||||
extra[&"extra"] = true
|
||||
_expect(not context.projector.project_facility(&"instance_extra", GUEST_CORRIDOR, extra).is_success(), "completed instance with an extra field is rejected")
|
||||
var wrong_definition := base_instance.duplicate(true)
|
||||
wrong_definition[&"definition_id"] = &"service_corridor"
|
||||
_expect(not context.projector.project_facility(&"instance_definition", GUEST_CORRIDOR, wrong_definition).is_success(), "instance-to-definition ID mismatch is rejected")
|
||||
var wrong_state := base_instance.duplicate(true)
|
||||
wrong_state[&"state"] = &"PLANNED"
|
||||
_expect(not context.projector.project_facility(&"instance_state", GUEST_CORRIDOR, wrong_state).is_success(), "non-ACTIVE completed instance is rejected")
|
||||
var wrong_layer_instance := base_instance.duplicate(true)
|
||||
wrong_layer_instance[&"layer"] = PathType.BACK
|
||||
_expect(not context.projector.project_facility(&"instance_layer", GUEST_CORRIDOR, wrong_layer_instance).is_success(), "instance layer crosslink mismatch is rejected")
|
||||
var wrong_cells := base_instance.duplicate(true)
|
||||
wrong_cells[&"occupied_cells"] = [Vector3i(1, 6, PathType.FRONT)]
|
||||
_expect(not context.projector.project_facility(&"instance_cells", GUEST_CORRIDOR, wrong_cells).is_success(), "instance cells outside its definition footprint are rejected")
|
||||
var duplicate_cells := base_instance.duplicate(true)
|
||||
duplicate_cells[&"occupied_cells"] = [base_instance[&"origin"], base_instance[&"origin"]]
|
||||
_expect(not context.projector.project_facility(&"instance_duplicate_cells", GUEST_CORRIDOR, duplicate_cells).is_success(), "duplicate instance cells are rejected")
|
||||
var float_cost := base_instance.duplicate(true)
|
||||
float_cost[&"construction_cost"] = {&"gold": 20.0}
|
||||
_expect(not context.projector.project_facility(&"instance_cost_type", GUEST_CORRIDOR, float_cost).is_success(), "non-integer construction cost is rejected")
|
||||
var negative_cost := base_instance.duplicate(true)
|
||||
negative_cost[&"construction_cost"] = {&"gold": -1}
|
||||
_expect(not context.projector.project_facility(&"instance_cost_negative", GUEST_CORRIDOR, negative_cost).is_success(), "negative construction cost is rejected")
|
||||
var bad_request = context.projector.project_facility(4, GUEST_CORRIDOR, base_instance)
|
||||
_expect(bad_request.code == CommandResultType.Code.INVALID_PAYLOAD, "numeric request ID is rejected rather than coerced")
|
||||
_expect(_all_snapshots(context) == before, "all definition/instance validation failures preserve every service exactly")
|
||||
|
||||
|
||||
func _test_project_and_unproject_failures_roll_back_exactly() -> void:
|
||||
var path := ControlledPathService.new()
|
||||
var context := _context([Vector3i(0, 8, PathType.FRONT)], path)
|
||||
path.projector = context.projector
|
||||
var guest := _instance(GUEST_CORRIDOR, &"facility_00000008", Vector3i(1, 8, PathType.FRONT), 60, &"construction_00000008")
|
||||
var before := _all_snapshots(context)
|
||||
path.fail_next_rebuild_after_mutation = true
|
||||
var route_failure = context.projector.project_facility(&"retry_route_failure", GUEST_CORRIDOR, guest)
|
||||
_assert_exact_rollback(context, before, route_failure, "route adapter Path rebuild failure")
|
||||
_expect(context.projector.project_facility(&"retry_route_failure", GUEST_CORRIDOR, guest).is_success(), "failed project request can retry after exact rollback")
|
||||
|
||||
var door := _instance(STAFF_DOOR, &"facility_00000009", Vector3i(3, 8, PathType.BACK), 61, &"construction_00000009")
|
||||
before = _all_snapshots(context)
|
||||
path.fail_next_connector_after_mutation = true
|
||||
var connector_failure = context.projector.project_facility(&"retry_connector_failure", STAFF_DOOR, door)
|
||||
_assert_exact_rollback(context, before, connector_failure, "staff connector apply failure")
|
||||
_expect(not context.source.has_entity(&"infra.route:facility_00000009") and not context.path.has_connector(&"infra.connector:facility_00000009"), "failed staff-door projection leaves neither route nor connector")
|
||||
_expect(context.projector.project_facility(&"retry_connector_failure", STAFF_DOOR, door).is_success(), "failed staff-door request retries after exact rollback")
|
||||
|
||||
before = _all_snapshots(context)
|
||||
path.fail_next_remove_after_mutation = true
|
||||
var remove_failure = context.projector.unproject_facility(&"retry_remove_failure", &"facility_00000009")
|
||||
_assert_exact_rollback(context, before, remove_failure, "staff connector removal failure")
|
||||
_expect(context.source.has_entity(&"infra.route:facility_00000009") and context.path.has_connector(&"infra.connector:facility_00000009"), "failed unproject restores route and connector")
|
||||
_expect(context.projector.unproject_facility(&"retry_remove_failure", &"facility_00000009").is_success(), "failed unproject request retries after exact rollback")
|
||||
path.projector = null
|
||||
|
||||
|
||||
func _test_callback_reentry_tamper_and_quarantine_recovery() -> void:
|
||||
var path := ControlledPathService.new()
|
||||
var front := Vector3i(5, 10, PathType.FRONT)
|
||||
var context := _context([front], path)
|
||||
path.projector = context.projector
|
||||
var first_door := _instance(STAFF_DOOR, &"facility_00000010", Vector3i(5, 10, PathType.BACK), 70, &"construction_00000010")
|
||||
path.reenter_next_connector = true
|
||||
var outer = context.projector.project_facility(&"outer_reentry", STAFF_DOOR, first_door)
|
||||
_expect(outer.is_success(), "outer staff-door transaction commits when callback only attempts reentry")
|
||||
_expect(path.reentry_result != null and path.reentry_result.code == CommandResultType.Code.TRANSACTION_CLOSED, "Path callback reentry is rejected")
|
||||
_expect(context.projector.projection_revision == 1, "reentry cannot advance a nested projector revision")
|
||||
|
||||
var second_door := _instance(STAFF_DOOR, &"facility_00000011", Vector3i(7, 10, PathType.BACK), 71, &"construction_00000011")
|
||||
var before := _all_snapshots(context)
|
||||
path.tamper_projector_next_connector = true
|
||||
var tampered = context.projector.project_facility(&"callback_tamper", STAFF_DOOR, second_door)
|
||||
_assert_exact_rollback(context, before, tampered, "Path callback projector tamper")
|
||||
_expect(context.projector.projection_revision == 1 and not context.projector.has_projection(&"facility_00000011"), "callback tamper cannot leak owned metadata")
|
||||
|
||||
var recovery := _all_snapshots(context)
|
||||
path.fail_next_connector_after_mutation = true
|
||||
path.reject_restore = true
|
||||
var uncertain_door := _instance(STAFF_DOOR, &"facility_00000012", Vector3i(9, 10, PathType.BACK), 72, &"construction_00000012")
|
||||
var uncertain = context.projector.project_facility(&"uncertain_project", STAFF_DOOR, uncertain_door)
|
||||
_expect(not uncertain.is_success() and not uncertain.details[&"rollback_ok"], "uncertain dependency rollback reports its uncertainty")
|
||||
_expect(context.projector.is_quarantined() and context.projector.get_quarantine_reason() == context.projector.QUARANTINE_TRANSACTION_ROLLBACK, "uncertain rollback quarantines the projector")
|
||||
_expect(context.projector.project_facility(&"blocked_quarantine", GUEST_CORRIDOR, _instance(GUEST_CORRIDOR, &"facility_00000013", Vector3i(11, 10, 0), 73, &"construction_00000013")).code == CommandResultType.Code.TRANSACTION_CLOSED, "quarantine blocks further projection")
|
||||
|
||||
path.reject_restore = false
|
||||
_expect(context.source.restore_snapshot(recovery.source), "quarantine recovery restores the Spatial snapshot")
|
||||
_restore_path_for_test(context.path, recovery.path_cells, recovery.path)
|
||||
_expect(context.adapter.restore_snapshot(recovery.adapter), "quarantine recovery restores the Route adapter snapshot")
|
||||
_expect(context.projector.restore_snapshot(recovery.projector), "matching projector snapshot clears quarantine")
|
||||
_expect(not context.projector.is_quarantined() and context.projector.validate_invariants()[&"ok"], "recovered projector returns to a valid available state")
|
||||
path.projector = null
|
||||
|
||||
|
||||
func _test_strict_snapshot_round_trip_and_atomic_rejection() -> void:
|
||||
var base := [
|
||||
Vector3i(0, 12, PathType.FRONT),
|
||||
Vector3i(4, 12, PathType.FRONT),
|
||||
]
|
||||
var context := _context(base)
|
||||
var guest := _instance(GUEST_CORRIDOR, &"facility_00000020", Vector3i(1, 12, 0), 80, &"construction_00000020")
|
||||
var door := _instance(STAFF_DOOR, &"facility_00000021", Vector3i(3, 12, 1), 81, &"construction_00000021")
|
||||
context.projector.project_facility(&"z_project", GUEST_CORRIDOR, guest)
|
||||
context.projector.project_facility(&"a_project", STAFF_DOOR, door)
|
||||
context.projector.unproject_facility(&"m_unproject", &"facility_00000020")
|
||||
var snapshot: Dictionary = context.projector.create_snapshot()
|
||||
_expect(snapshot[&"projections"].map(func(row: Dictionary) -> String: return row[&"instance_id"]) == ["facility_00000021"], "snapshot stores only current projections in lexical order")
|
||||
_expect(snapshot[&"processed_requests"].map(func(row: Dictionary) -> String: return row[&"request_id"]) == ["a_project", "m_unproject", "z_project"], "snapshot history is lexical independently of revision order")
|
||||
_expect(snapshot[&"projection_revision"] == 3, "snapshot preserves contiguous successful revision history")
|
||||
|
||||
var restored_source := SpatialType.new()
|
||||
var restored_path := PathType.new()
|
||||
var restored_adapter := AdapterType.new()
|
||||
_expect(restored_adapter.configure(restored_source, restored_path, base), "round-trip target adapter configures empty dependencies")
|
||||
var restored_projector := ProjectorType.new()
|
||||
_expect(restored_projector.configure(restored_adapter, restored_source, restored_path), "round-trip target projector configures before dependency restore")
|
||||
_expect(restored_source.restore_snapshot(context.source.create_snapshot()), "round-trip target restores Spatial state")
|
||||
_restore_path_for_test(restored_path, _path_cells(context.path), context.path.create_snapshot())
|
||||
_expect(restored_adapter.restore_snapshot(context.adapter.create_snapshot()), "round-trip target restores Route adapter state")
|
||||
_expect(restored_projector.restore_snapshot(snapshot), "strict current projector snapshot restores against matching dependencies")
|
||||
_expect(restored_projector.create_snapshot() == snapshot, "projector snapshot round trip is exact")
|
||||
_expect(restored_projector.validate_invariants()[&"ok"], "restored history/projection/dependency crosslinks validate")
|
||||
var replay_source: Dictionary = restored_source.create_snapshot()
|
||||
var replay_adapter: Dictionary = restored_adapter.create_snapshot()
|
||||
var replay_path: Dictionary = restored_path.create_snapshot()
|
||||
var restored_replay = restored_projector.project_facility(&"a_project", STAFF_DOOR, door)
|
||||
_expect(restored_replay.is_success() and restored_replay.is_replay, "restored project history replays with the same canonical input")
|
||||
_expect(restored_projector.create_snapshot() == snapshot and restored_source.create_snapshot() == replay_source and restored_adapter.create_snapshot() == replay_adapter and restored_path.create_snapshot() == replay_path, "restored replay leaves metadata and dependencies unchanged")
|
||||
var restored_context := {
|
||||
&"source": restored_source,
|
||||
&"path": restored_path,
|
||||
&"adapter": restored_adapter,
|
||||
&"projector": restored_projector,
|
||||
}
|
||||
var before := _all_snapshots(restored_context)
|
||||
|
||||
var wrong_version: Dictionary = snapshot.duplicate(true)
|
||||
wrong_version[&"schema_version"] = 2
|
||||
_atomic_snapshot_reject(restored_context, wrong_version, before, "unknown snapshot version")
|
||||
var version_type: Dictionary = snapshot.duplicate(true)
|
||||
version_type[&"schema_version"] = "1"
|
||||
_atomic_snapshot_reject(restored_context, version_type, before, "string snapshot version")
|
||||
var unknown_field: Dictionary = snapshot.duplicate(true)
|
||||
unknown_field[&"extra"] = true
|
||||
_atomic_snapshot_reject(restored_context, unknown_field, before, "unknown root field")
|
||||
var revision_type: Dictionary = snapshot.duplicate(true)
|
||||
revision_type[&"projection_revision"] = 3.0
|
||||
_atomic_snapshot_reject(restored_context, revision_type, before, "floating projection revision")
|
||||
var projection_order: Dictionary = snapshot.duplicate(true)
|
||||
projection_order[&"projections"].append(projection_order[&"projections"][0].duplicate(true))
|
||||
_atomic_snapshot_reject(restored_context, projection_order, before, "duplicate projection order")
|
||||
var request_order: Dictionary = snapshot.duplicate(true)
|
||||
request_order[&"processed_requests"].reverse()
|
||||
_atomic_snapshot_reject(restored_context, request_order, before, "unordered request history")
|
||||
var fingerprint: Dictionary = snapshot.duplicate(true)
|
||||
fingerprint[&"processed_requests"][0][&"fingerprint"] = "0".repeat(64)
|
||||
_atomic_snapshot_reject(restored_context, fingerprint, before, "tampered request fingerprint")
|
||||
var result_projection: Dictionary = snapshot.duplicate(true)
|
||||
result_projection[&"processed_requests"][0][&"details"][&"projection"][&"staff_only"] = false
|
||||
_atomic_snapshot_reject(restored_context, result_projection, before, "tampered result projection semantics")
|
||||
var history_revision: Dictionary = snapshot.duplicate(true)
|
||||
history_revision[&"processed_requests"][0][&"details"][&"projection_revision"] = 3
|
||||
_atomic_snapshot_reject(restored_context, history_revision, before, "duplicate history revision")
|
||||
var final_crosslink: Dictionary = snapshot.duplicate(true)
|
||||
final_crosslink[&"projections"].clear()
|
||||
_atomic_snapshot_reject(restored_context, final_crosslink, before, "history-to-current projection mismatch")
|
||||
var record_fingerprint: Dictionary = snapshot.duplicate(true)
|
||||
record_fingerprint[&"projections"][0][&"record_fingerprint"] = "bad"
|
||||
_atomic_snapshot_reject(restored_context, record_fingerprint, before, "tampered projection record fingerprint")
|
||||
var extra_projection_field: Dictionary = snapshot.duplicate(true)
|
||||
extra_projection_field[&"projections"][0][&"extra"] = true
|
||||
_atomic_snapshot_reject(restored_context, extra_projection_field, before, "unknown projection field")
|
||||
var deep: Dictionary = snapshot.duplicate(true)
|
||||
var deep_value: Variant = []
|
||||
for _index: int in range(restored_projector.MAX_SNAPSHOT_DEPTH + 2):
|
||||
deep_value = [deep_value]
|
||||
deep[&"processed_requests"] = deep_value
|
||||
_atomic_snapshot_reject(restored_context, deep, before, "excessive snapshot depth")
|
||||
|
||||
var connector_id := &"infra.connector:facility_00000021"
|
||||
var connector_before := restored_path.get_connector(connector_id)
|
||||
_expect(restored_path.configure_connector(connector_id, connector_before[&"from"], connector_before[&"to"], 2, true, &"staff"), "test can introduce a live connector crosslink mismatch")
|
||||
var stale_owned_before := restored_projector.create_snapshot()
|
||||
var dependencies_before_restore := {
|
||||
&"source": restored_source.create_snapshot(),
|
||||
&"adapter": restored_adapter.create_snapshot(),
|
||||
&"path": restored_path.create_snapshot(),
|
||||
}
|
||||
_expect(not restored_projector.restore_snapshot(snapshot), "valid metadata snapshot rejects a mismatched live staff connector")
|
||||
_expect(restored_projector.create_snapshot() == stale_owned_before, "live-crosslink rejection is projector-atomic")
|
||||
_expect(restored_source.create_snapshot() == dependencies_before_restore.source and restored_adapter.create_snapshot() == dependencies_before_restore.adapter and restored_path.create_snapshot() == dependencies_before_restore.path, "live-crosslink rejection preserves all dependency snapshots")
|
||||
|
||||
|
||||
func _test_configuration_and_stale_dependency_boundaries() -> void:
|
||||
var source_a := SpatialType.new()
|
||||
var source_b := SpatialType.new()
|
||||
var path_a := PathType.new()
|
||||
var path_b := PathType.new()
|
||||
var adapter := AdapterType.new()
|
||||
adapter.configure(source_a, path_a, [])
|
||||
var projector := ProjectorType.new()
|
||||
_expect(not projector.configure(adapter, source_b, path_a), "configuration rejects a different Spatial instance with equal state")
|
||||
_expect(not projector.configure(adapter, source_a, path_b), "configuration rejects a different Path instance with equal state")
|
||||
_expect(projector.configure(adapter, source_a, path_a), "configuration accepts the exact adapter binding")
|
||||
_expect(not projector.configure(adapter, source_a, path_a), "configured projector rejects reconfiguration")
|
||||
|
||||
var context := _context([])
|
||||
var instance := _instance(GUEST_CORRIDOR, &"facility_00000030", Vector3i(1, 14, 0), 90, &"construction_00000030")
|
||||
context.projector.project_facility(&"stale_project", GUEST_CORRIDOR, instance)
|
||||
var before := _all_snapshots(context)
|
||||
_expect(context.source.place(&"external_route", context.source.CATEGORY_ROUTE, [Vector3i(2, 14, 0)]).is_success(), "external ROUTE mutation can stale the lower adapter")
|
||||
var source_after_external: Dictionary = context.source.create_snapshot()
|
||||
var blocked = context.projector.unproject_facility(&"blocked_stale", &"facility_00000030")
|
||||
_expect(blocked.code == CommandResultType.Code.TRANSACTION_CLOSED, "stale dependencies block projector mutation")
|
||||
_expect(context.projector.create_snapshot() == before.projector and context.adapter.create_snapshot() == before.adapter and context.path.create_snapshot() == before.path, "stale rejection preserves projector, adapter, and Path state")
|
||||
_expect(context.source.create_snapshot() == source_after_external, "stale rejection does not undo an external source mutation")
|
||||
_expect(context.projector.is_projection_stale(), "projector exposes stale dependency status")
|
||||
|
||||
var collision_source := SpatialType.new()
|
||||
var collision_path := PathType.new()
|
||||
var collision_adapter := AdapterType.new()
|
||||
collision_adapter.configure(collision_source, collision_path, [])
|
||||
collision_adapter.place_route(&"preexisting_managed", &"infra.route:foreign", [Vector3i(4, 14, 0)])
|
||||
var collision_projector := ProjectorType.new()
|
||||
_expect(not collision_projector.configure(collision_adapter, collision_source, collision_path), "configuration rejects an untracked managed-ID namespace collision")
|
||||
|
||||
|
||||
func _context(base_cells: Array, custom_path: Variant = null) -> Dictionary:
|
||||
var source := SpatialType.new()
|
||||
var path: PathService = custom_path if custom_path != null else PathType.new()
|
||||
var adapter := AdapterType.new()
|
||||
_expect(adapter.configure(source, path, base_cells), "test context configures Route adapter")
|
||||
var projector := ProjectorType.new()
|
||||
_expect(projector.configure(adapter, source, path), "test context configures Facility projector")
|
||||
return {
|
||||
&"source": source,
|
||||
&"path": path,
|
||||
&"adapter": adapter,
|
||||
&"projector": projector,
|
||||
}
|
||||
|
||||
|
||||
func _instance(
|
||||
definition: FacilityDefinition,
|
||||
instance_id: StringName,
|
||||
origin: Vector3i,
|
||||
completed_at: int,
|
||||
source_job_id: StringName,
|
||||
rotation: int = 0
|
||||
) -> Dictionary:
|
||||
var dimensions := definition.footprint
|
||||
if rotation in [90, 270]:
|
||||
dimensions = Vector2i(definition.footprint.y, definition.footprint.x)
|
||||
var cells: Array[Vector3i] = []
|
||||
for y_offset: int in range(dimensions.y):
|
||||
for x_offset: int in range(dimensions.x):
|
||||
cells.append(origin + Vector3i(x_offset, y_offset, 0))
|
||||
return {
|
||||
&"instance_id": instance_id,
|
||||
&"definition_id": definition.id,
|
||||
&"origin": origin,
|
||||
&"rotation": rotation,
|
||||
&"layer": definition.layer,
|
||||
&"occupied_cells": cells,
|
||||
&"construction_cost": definition.construction_cost.duplicate(true),
|
||||
&"source_job_id": source_job_id,
|
||||
&"state": &"ACTIVE",
|
||||
&"completed_at": completed_at,
|
||||
}
|
||||
|
||||
|
||||
func _all_snapshots(context: Dictionary) -> Dictionary:
|
||||
return {
|
||||
&"projector": context.projector.create_snapshot(),
|
||||
&"source": context.source.create_snapshot(),
|
||||
&"adapter": context.adapter.create_snapshot(),
|
||||
&"path": context.path.create_snapshot(),
|
||||
&"path_cells": _path_cells(context.path),
|
||||
}
|
||||
|
||||
|
||||
func _path_cells(path: PathService) -> Array[Vector3i]:
|
||||
var result: Array[Vector3i] = []
|
||||
for row: Variant in path.create_snapshot().get(&"cell_traffic", []):
|
||||
if row is Dictionary and row.get(&"cell", null) is Vector3i:
|
||||
result.append(row[&"cell"])
|
||||
result.sort_custom(func(left: Vector3i, right: Vector3i) -> bool:
|
||||
if left.y != right.y:
|
||||
return left.y < right.y
|
||||
if left.x != right.x:
|
||||
return left.x < right.x
|
||||
return left.z < right.z
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func _restore_path_for_test(path: PathService, cells: Array, snapshot: Dictionary) -> void:
|
||||
path.rebuild_graph(cells)
|
||||
path.restore_snapshot(snapshot)
|
||||
path.rebuild_graph(cells)
|
||||
|
||||
|
||||
func _assert_exact_rollback(
|
||||
context: Dictionary,
|
||||
before: Dictionary,
|
||||
result: CommandResult,
|
||||
label: String
|
||||
) -> void:
|
||||
_expect(not result.is_success() and bool(result.details.get(&"rollback_ok", false)), "%s reports exact rollback" % label)
|
||||
_expect(context.projector.create_snapshot() == before.projector, "%s restores exact projector snapshot" % label)
|
||||
_expect(context.source.create_snapshot() == before.source, "%s restores exact Spatial snapshot" % label)
|
||||
_expect(context.adapter.create_snapshot() == before.adapter, "%s restores exact Route adapter snapshot" % label)
|
||||
_expect(context.path.create_snapshot() == before.path, "%s restores exact Path snapshot" % label)
|
||||
|
||||
|
||||
func _atomic_snapshot_reject(
|
||||
context: Dictionary,
|
||||
candidate: Dictionary,
|
||||
before: Dictionary,
|
||||
label: String
|
||||
) -> void:
|
||||
_expect(not context.projector.restore_snapshot(candidate), "%s is rejected" % label)
|
||||
_expect(context.projector.create_snapshot() == before.projector, "%s rejection is projector-atomic" % label)
|
||||
_expect(context.source.create_snapshot() == before.source and context.adapter.create_snapshot() == before.adapter and context.path.create_snapshot() == before.path, "%s rejection leaves every dependency untouched" % label)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dabxeosrueu1a
|
||||
@@ -0,0 +1,517 @@
|
||||
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"]) == 3, "request-safe traffic snapshot uses schema version 3")
|
||||
_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), "v3 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, "v3 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")
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://qbem45x6fu4h
|
||||
@@ -0,0 +1,182 @@
|
||||
extends SceneTree
|
||||
|
||||
const PathServiceScript = preload("res://scripts/world/path_service.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S1: deterministic PathService")
|
||||
_test_deterministic_tie_break()
|
||||
_test_layer_separation_and_connector()
|
||||
_test_connector_access_and_direction()
|
||||
_test_cache_invalidation_and_revision()
|
||||
_test_connector_only_snapshot()
|
||||
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_deterministic_tie_break() -> void:
|
||||
var service = PathServiceScript.new()
|
||||
var passable := [
|
||||
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0),
|
||||
Vector3i(0, 1, 0), Vector3i(2, 1, 0),
|
||||
Vector3i(0, 2, 0), Vector3i(1, 2, 0), Vector3i(2, 2, 0),
|
||||
]
|
||||
_expect(service.rebuild_graph(passable), "array of Vector3i cells rebuilds the path graph")
|
||||
var expected: Array[Vector3i] = [
|
||||
Vector3i(0, 1, 0),
|
||||
Vector3i(0, 0, 0),
|
||||
Vector3i(1, 0, 0),
|
||||
Vector3i(2, 0, 0),
|
||||
Vector3i(2, 1, 0),
|
||||
]
|
||||
var first: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), service.FRONT)
|
||||
var second: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), service.FRONT)
|
||||
_expect(first == expected, "equal-cost routes use coordinate dictionary order as the tie break")
|
||||
_expect(second == first, "the cached route exactly reproduces the calculated route")
|
||||
_expect(service.cache_hits == 1 and service.cache_misses == 1, "path cache records one miss followed by one hit")
|
||||
|
||||
|
||||
func _test_layer_separation_and_connector() -> void:
|
||||
var service = PathServiceScript.new()
|
||||
var passable_set := {}
|
||||
for layer in [service.FRONT, service.BACK]:
|
||||
for x in range(3):
|
||||
passable_set[Vector3i(x, 0, layer)] = true
|
||||
_expect(service.rebuild_graph(passable_set), "Dictionary set can supply passable cells without a WorldService dependency")
|
||||
_expect(
|
||||
service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1)).is_empty(),
|
||||
"FRONT and BACK never connect through ordinary cardinal edges"
|
||||
)
|
||||
_expect(
|
||||
service.configure_connector(
|
||||
&"service_door",
|
||||
Vector3i(2, 0, 0),
|
||||
Vector3i(0, 0, 1),
|
||||
120,
|
||||
true
|
||||
),
|
||||
"an explicit layer connector is accepted"
|
||||
)
|
||||
var connected_path: Array[Vector3i] = service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1))
|
||||
_expect(
|
||||
connected_path.has(Vector3i(2, 0, 0)) and connected_path.has(Vector3i(0, 0, 1)),
|
||||
"an explicit connector joins the two isolated layers"
|
||||
)
|
||||
_expect(
|
||||
service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1), service.FRONT).is_empty(),
|
||||
"an access-layer restriction rejects a route that enters BACK"
|
||||
)
|
||||
|
||||
|
||||
func _test_connector_access_and_direction() -> void:
|
||||
var service = PathServiceScript.new()
|
||||
var cells := [Vector3i(0, 0, 0), Vector3i(0, 2, 0)]
|
||||
service.rebuild_graph(cells)
|
||||
service.configure_connector(
|
||||
&"staff_stairs",
|
||||
Vector3i(0, 0, 0),
|
||||
Vector3i(0, 2, 0),
|
||||
150,
|
||||
false,
|
||||
&"staff"
|
||||
)
|
||||
_expect(
|
||||
service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0)).is_empty(),
|
||||
"a restricted connector requires its access id"
|
||||
)
|
||||
var staff_profile := {
|
||||
&"allowed_layers": [service.FRONT],
|
||||
&"connector_access_ids": [&"staff"],
|
||||
}
|
||||
_expect(
|
||||
service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0), staff_profile).size() == 2,
|
||||
"an access profile enables its matching connector"
|
||||
)
|
||||
_expect(
|
||||
service.find_path(Vector3i(0, 2, 0), Vector3i(0, 0, 0), staff_profile).is_empty(),
|
||||
"a one-way connector cannot be traversed in reverse"
|
||||
)
|
||||
|
||||
|
||||
func _test_cache_invalidation_and_revision() -> void:
|
||||
var service = PathServiceScript.new()
|
||||
var passable := [
|
||||
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0),
|
||||
Vector3i(0, 1, 0), Vector3i(2, 1, 0),
|
||||
Vector3i(0, 2, 0), Vector3i(1, 2, 0), Vector3i(2, 2, 0),
|
||||
]
|
||||
service.rebuild_graph(passable)
|
||||
var initial_revision: int = service.graph_revision
|
||||
service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
|
||||
_expect(service.cached_path_count() == 1, "a calculated path is cached")
|
||||
_expect(
|
||||
service.invalidate_cache_for_cells([Vector3i(1, 0, 0)]) == 1,
|
||||
"local invalidation removes a cached path that crosses an affected cell"
|
||||
)
|
||||
_expect(service.cached_path_count() == 0, "local invalidation leaves no matching stale cache entry")
|
||||
service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
|
||||
_expect(service.invalidate_all_cache() == 1, "full invalidation reports and removes every cache entry")
|
||||
|
||||
passable.erase(Vector3i(1, 0, 0))
|
||||
service.rebuild_graph(passable)
|
||||
_expect(service.graph_revision == initial_revision + 1, "a changed topology advances graph_revision exactly once")
|
||||
var rebuilt_path: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
|
||||
_expect(rebuilt_path.has(Vector3i(1, 2, 0)), "rebuild discards the old route and uses the remaining corridor")
|
||||
|
||||
|
||||
func _test_connector_only_snapshot() -> void:
|
||||
var cells := [Vector3i(0, 0, 0), Vector3i(0, 0, 1)]
|
||||
var original = PathServiceScript.new()
|
||||
original.rebuild_graph(cells)
|
||||
original.configure_connector(
|
||||
&"lift_a",
|
||||
Vector3i(0, 0, 0),
|
||||
Vector3i(0, 0, 1),
|
||||
250,
|
||||
true,
|
||||
&"lift_pass"
|
||||
)
|
||||
original.find_path(
|
||||
Vector3i(0, 0, 0),
|
||||
Vector3i(0, 0, 1),
|
||||
null,
|
||||
[&"lift_pass"]
|
||||
)
|
||||
var snapshot: Dictionary = original.create_snapshot()
|
||||
_expect(
|
||||
snapshot.has(&"connectors") and not snapshot.has(&"graph") and not snapshot.has(&"cache") and not snapshot.has(&"passable_cells"),
|
||||
"snapshot persists connector configuration but no derived graph or cache"
|
||||
)
|
||||
|
||||
var restored = PathServiceScript.new()
|
||||
_expect(restored.restore_snapshot(snapshot), "connector configuration restores independently of world cells")
|
||||
_expect(restored.rebuild_graph(cells), "restored service rebuilds its graph from authoritative passable cells")
|
||||
_expect(
|
||||
restored.find_path(
|
||||
Vector3i(0, 0, 0),
|
||||
Vector3i(0, 0, 1),
|
||||
null,
|
||||
[&"lift_pass"]
|
||||
).size() == 2,
|
||||
"restored connector produces the same cross-layer route after rebuild"
|
||||
)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bbqw1dome2hdp
|
||||
@@ -0,0 +1,496 @@
|
||||
extends SceneTree
|
||||
|
||||
const AdapterType = preload("res://scripts/world/route_subslot_path_adapter.gd")
|
||||
const SpatialType = preload("res://scripts/world/spatial_subslot_service.gd")
|
||||
const PathType = preload("res://scripts/world/path_service.gd")
|
||||
const WorldType = preload("res://scripts/world/world_service.gd")
|
||||
const CommandResultType = preload("res://scripts/core/command_result.gd")
|
||||
|
||||
|
||||
class ControlledPathService:
|
||||
extends PathService
|
||||
|
||||
var fail_next_rebuild_after_mutation := false
|
||||
var reject_restore := false
|
||||
var reenter_next_rebuild := false
|
||||
var tamper_adapter_next_rebuild := false
|
||||
var adapter: Variant = null
|
||||
var reentry_result: Variant = null
|
||||
|
||||
func rebuild_graph(passable_cells: Variant) -> bool:
|
||||
var should_fail := fail_next_rebuild_after_mutation
|
||||
fail_next_rebuild_after_mutation = false
|
||||
var should_reenter := reenter_next_rebuild
|
||||
reenter_next_rebuild = false
|
||||
var should_tamper := tamper_adapter_next_rebuild
|
||||
tamper_adapter_next_rebuild = false
|
||||
var rebuilt := super.rebuild_graph(passable_cells)
|
||||
if should_reenter and adapter != null:
|
||||
reentry_result = adapter.place_route(
|
||||
&"nested_path_request",
|
||||
&"nested_path_route",
|
||||
[Vector3i(40, 0, 0)]
|
||||
)
|
||||
if should_tamper and adapter != null:
|
||||
adapter.projection_revision += 99
|
||||
if should_fail:
|
||||
last_error = "Injected Path rebuild failure after mutation."
|
||||
return false
|
||||
return rebuilt
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
if reject_restore:
|
||||
last_error = "Injected Path restore failure."
|
||||
return false
|
||||
return super.restore_snapshot(snapshot)
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S1: atomic ROUTE subslot Path projection")
|
||||
_test_basic_projection_and_world_coexistence()
|
||||
_test_replace_and_layer_separation()
|
||||
_test_idempotency_and_defensive_queries()
|
||||
_test_stale_detection_and_synchronization()
|
||||
_test_path_failure_rolls_both_services_back()
|
||||
_test_callback_reentry_and_tamper_defense()
|
||||
_test_uncertain_rollback_quarantines_and_recovers()
|
||||
_test_strict_snapshot_round_trip_and_atomic_rejection()
|
||||
_test_strict_input_and_route_only_boundary()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_basic_projection_and_world_coexistence() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := PathType.new()
|
||||
var world := WorldType.new()
|
||||
world.reset({&"min_x": -2, &"max_x": 4, &"min_y": -2, &"max_y": 2})
|
||||
var start := Vector3i(0, 0, 0)
|
||||
var bridge := Vector3i(1, 0, 0)
|
||||
var goal := Vector3i(2, 0, 0)
|
||||
_expect(world.occupy(&"major_facility", [bridge], &"facility_large"), "major World occupancy exists independently")
|
||||
var adapter := AdapterType.new()
|
||||
_expect(adapter.configure(source, path, [goal, start]), "base walkable cells configure the initial Path projection")
|
||||
_expect(path.find_path(start, goal).is_empty(), "disconnected base cells are initially unreachable")
|
||||
var placed = adapter.place_route(&"route_place_1", &"route_bridge", [bridge])
|
||||
_expect(placed.is_success(), "ROUTE placement and Path rebuild commit together")
|
||||
_expect(path.find_path(start, goal) == [start, bridge, goal], "ROUTE subslot makes the real Path graph reachable")
|
||||
_expect(world.get_occupant(bridge) == &"major_facility", "ROUTE projection preserves primary World occupancy")
|
||||
_expect(adapter.source_revision == 1 and adapter.projection_revision == 2, "source and projection revisions advance deterministically")
|
||||
_expect(not adapter.is_projection_stale(), "committed projection is fresh")
|
||||
_expect(adapter.remove_route(&"route_remove_1", &"route_bridge").is_success(), "ROUTE removal and Path rebuild commit together")
|
||||
_expect(path.find_path(start, goal).is_empty(), "removing the ROUTE makes the real Path graph unreachable again")
|
||||
_expect(adapter.validate_invariants()[&"ok"], "basic projection crosslinks remain valid")
|
||||
|
||||
|
||||
func _test_replace_and_layer_separation() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := PathType.new()
|
||||
var front_start := Vector3i(0, 2, 0)
|
||||
var front_mid := Vector3i(1, 2, 0)
|
||||
var front_goal := Vector3i(2, 2, 0)
|
||||
var back_start := Vector3i(0, 2, 1)
|
||||
var back_mid := Vector3i(1, 2, 1)
|
||||
var back_goal := Vector3i(2, 2, 1)
|
||||
var adapter := AdapterType.new()
|
||||
_expect(
|
||||
adapter.configure(source, path, [back_goal, front_goal, back_start, front_start]),
|
||||
"two-layer base projection configures in canonical order"
|
||||
)
|
||||
_expect(
|
||||
adapter.place_route(&"layers_place", &"dual_layer_route", [back_mid, front_mid]).is_success(),
|
||||
"one ROUTE entity can project cells on both layers"
|
||||
)
|
||||
_expect(path.find_path(front_start, front_goal) == [front_start, front_mid, front_goal], "FRONT route is reachable")
|
||||
_expect(path.find_path(back_start, back_goal) == [back_start, back_mid, back_goal], "BACK route is reachable")
|
||||
_expect(path.find_path(front_start, back_goal).is_empty(), "FRONT and BACK remain disconnected without a connector")
|
||||
var replaced = adapter.replace_route(&"layers_replace", &"dual_layer_route", [front_mid])
|
||||
_expect(replaced.is_success() and replaced.details[&"changed"], "ROUTE replacement commits atomically")
|
||||
_expect(path.find_path(front_start, front_goal).size() == 3, "replacement retains the FRONT route")
|
||||
_expect(path.find_path(back_start, back_goal).is_empty(), "replacement removes the BACK projection")
|
||||
|
||||
var base_route := adapter.place_route(&"base_overlap_place", &"base_overlap_route", [front_start])
|
||||
_expect(base_route.is_success(), "ROUTE may overlap a base walkable cell")
|
||||
_expect(adapter.remove_route(&"base_overlap_remove", &"base_overlap_route").is_success(), "base-overlap ROUTE removes cleanly")
|
||||
_expect(path.has_cell(front_start), "removing a ROUTE never removes the overlapping base cell")
|
||||
_expect(adapter.validate_invariants()[&"ok"], "replacement and layer projection crosslinks remain valid")
|
||||
|
||||
|
||||
func _test_idempotency_and_defensive_queries() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := PathType.new()
|
||||
var adapter := AdapterType.new()
|
||||
var start := Vector3i(0, 0, 0)
|
||||
var middle := Vector3i(1, 0, 0)
|
||||
var goal := Vector3i(2, 0, 0)
|
||||
adapter.configure(source, path, [goal, start])
|
||||
var first = adapter.place_route(&"z_request", &"route_idem", [middle])
|
||||
var after_first_adapter := adapter.create_snapshot()
|
||||
var after_first_source := source.create_snapshot()
|
||||
var after_first_path := path.create_snapshot()
|
||||
var replay = adapter.place_route(&"z_request", &"route_idem", [middle])
|
||||
_expect(first.is_success() and replay.is_success() and replay.is_replay, "same request ID and fingerprint replays")
|
||||
_expect(replay.details == first.details, "idempotent replay preserves exact result details")
|
||||
_expect(adapter.create_snapshot() == after_first_adapter, "replay does not advance adapter state")
|
||||
_expect(source.create_snapshot() == after_first_source and path.create_snapshot() == after_first_path, "replay does not touch either authoritative service")
|
||||
var conflict = adapter.place_route(&"z_request", &"route_idem", [Vector3i(1, 1, 0)])
|
||||
_expect(conflict.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "same request ID with another fingerprint conflicts")
|
||||
var before_noop_revision := adapter.projection_revision
|
||||
var noop = adapter.replace_route(&"a_request", &"route_idem", [middle])
|
||||
_expect(noop.is_success() and not noop.details[&"changed"], "canonical-equivalent replace is a successful no-op")
|
||||
_expect(adapter.projection_revision == before_noop_revision, "no-op request does not advance projection revision")
|
||||
_expect(adapter.get_processed_request_ids() == [&"a_request", &"z_request"], "processed request IDs are lexical")
|
||||
|
||||
var returned_base := adapter.get_base_walkable_cells()
|
||||
var returned_route := adapter.get_projected_route_cells()
|
||||
var returned_walkable := adapter.get_projected_walkable_cells()
|
||||
var returned_status := adapter.get_projection_status()
|
||||
returned_base.clear()
|
||||
returned_route.clear()
|
||||
returned_walkable.clear()
|
||||
returned_status[&"source_changed"] = true
|
||||
_expect(adapter.get_base_walkable_cells().size() == 2, "base-cell query is defensive")
|
||||
_expect(adapter.get_projected_route_cells() == [middle], "route projection query is defensive")
|
||||
_expect(adapter.get_projected_walkable_cells().size() == 3, "walkable projection query is defensive")
|
||||
_expect(not adapter.get_projection_status()[&"source_changed"], "projection status query is defensive")
|
||||
|
||||
|
||||
func _test_stale_detection_and_synchronization() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := PathType.new()
|
||||
var adapter := AdapterType.new()
|
||||
var start := Vector3i(0, 0, 0)
|
||||
var middle := Vector3i(1, 0, 0)
|
||||
var goal := Vector3i(2, 0, 0)
|
||||
var base := [start, goal]
|
||||
adapter.configure(source, path, base)
|
||||
_expect(source.place(&"external_route", source.CATEGORY_ROUTE, [middle]).is_success(), "direct source mutation can occur outside the adapter boundary")
|
||||
var stale_status := adapter.get_projection_status()
|
||||
_expect(stale_status[&"stale"] and stale_status[&"source_changed"], "direct ROUTE mutation is detected as stale")
|
||||
var blocked = adapter.place_route(&"blocked_while_stale", &"blocked_route", [Vector3i(3, 0, 0)])
|
||||
_expect(blocked.code == CommandResultType.Code.TRANSACTION_CLOSED, "route mutation is blocked while projection is stale")
|
||||
var synchronized = adapter.synchronize_projection(&"sync_source_1")
|
||||
_expect(synchronized.is_success() and synchronized.details[&"changed"], "explicit synchronization projects the external ROUTE change")
|
||||
_expect(path.find_path(start, goal) == [start, middle, goal], "source synchronization repairs real Path reachability")
|
||||
_expect(adapter.source_revision == source.get_revision_for_category(source.CATEGORY_ROUTE), "synchronization records the exact source revision")
|
||||
var sync_replay = adapter.synchronize_projection(&"sync_source_1")
|
||||
_expect(sync_replay.is_success() and sync_replay.is_replay, "synchronization request is idempotently replayed")
|
||||
|
||||
path.rebuild_graph(base)
|
||||
_expect(adapter.get_projection_status()[&"path_changed"], "direct Path topology drift is detected")
|
||||
var repaired = adapter.synchronize_projection(&"sync_path_2")
|
||||
_expect(repaired.is_success() and repaired.details[&"changed"], "synchronization repairs direct Path drift")
|
||||
_expect(path.find_path(start, goal).size() == 3 and not adapter.is_projection_stale(), "Path drift repair restores a fresh route")
|
||||
var projection_before_decoration := adapter.projection_revision
|
||||
_expect(source.place(&"external_decor", source.CATEGORY_DECORATION, [middle]).is_success(), "non-ROUTE source category changes independently")
|
||||
_expect(not adapter.is_projection_stale(), "DECORATION revision does not stale the ROUTE projection")
|
||||
_expect(adapter.projection_revision == projection_before_decoration, "unrelated source mutation does not advance projection revision")
|
||||
|
||||
var base_source := SpatialType.new()
|
||||
var base_path := PathType.new()
|
||||
var base_adapter := AdapterType.new()
|
||||
base_adapter.configure(base_source, base_path, base)
|
||||
var base_added = base_adapter.synchronize_projection(&"base_add", [goal, middle, start])
|
||||
_expect(base_added.is_success() and base_added.details[&"changed"], "synchronization atomically replaces the base walkable set")
|
||||
_expect(base_path.find_path(start, goal).size() == 3, "new base walkability participates in the rebuilt graph")
|
||||
_expect(base_adapter.get_base_walkable_cells() == [start, middle, goal], "updated base cells are canonical and queryable")
|
||||
var base_removed = base_adapter.synchronize_projection(&"base_remove", base)
|
||||
_expect(base_removed.is_success() and base_removed.details[&"changed"], "base walkability can be removed atomically")
|
||||
_expect(base_path.find_path(start, goal).is_empty(), "removed base walkability disappears from the graph")
|
||||
_expect(base_adapter.validate_invariants()[&"ok"], "dynamic base synchronization preserves crosslinks")
|
||||
|
||||
|
||||
func _test_path_failure_rolls_both_services_back() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := ControlledPathService.new()
|
||||
var adapter := AdapterType.new()
|
||||
path.adapter = adapter
|
||||
var start := Vector3i(0, 0, 0)
|
||||
var middle := Vector3i(1, 0, 0)
|
||||
var alternate := Vector3i(1, 1, 0)
|
||||
var goal := Vector3i(2, 0, 0)
|
||||
adapter.configure(source, path, [start, goal])
|
||||
|
||||
var before_adapter := adapter.create_snapshot()
|
||||
var before_source := source.create_snapshot()
|
||||
var before_path := path.create_snapshot()
|
||||
path.fail_next_rebuild_after_mutation = true
|
||||
var failed_place = adapter.place_route(&"retryable_place", &"route_atomic", [middle])
|
||||
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, failed_place, "failed place")
|
||||
_expect(not adapter.is_quarantined(), "exact place rollback does not quarantine")
|
||||
_expect(not source.has_entity(&"route_atomic") and path.find_path(start, goal).is_empty(), "failed place leaves neither side applied")
|
||||
_expect(adapter.place_route(&"retryable_place", &"route_atomic", [middle]).is_success(), "failed request ID can be retried after exact rollback")
|
||||
|
||||
before_adapter = adapter.create_snapshot()
|
||||
before_source = source.create_snapshot()
|
||||
before_path = path.create_snapshot()
|
||||
path.fail_next_rebuild_after_mutation = true
|
||||
var failed_replace = adapter.replace_route(&"retryable_replace", &"route_atomic", [alternate])
|
||||
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, failed_replace, "failed replace")
|
||||
_expect(source.get_entity_cells(&"route_atomic") == [middle], "failed replacement restores the original ROUTE cells")
|
||||
_expect(adapter.replace_route(&"retryable_replace", &"route_atomic", [alternate]).is_success(), "replacement retries after exact rollback")
|
||||
|
||||
before_adapter = adapter.create_snapshot()
|
||||
before_source = source.create_snapshot()
|
||||
before_path = path.create_snapshot()
|
||||
path.fail_next_rebuild_after_mutation = true
|
||||
var failed_remove = adapter.remove_route(&"retryable_remove", &"route_atomic")
|
||||
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, failed_remove, "failed remove")
|
||||
_expect(source.has_entity(&"route_atomic"), "failed removal restores the ROUTE entity")
|
||||
_expect(adapter.remove_route(&"retryable_remove", &"route_atomic").is_success(), "removal retries after exact rollback")
|
||||
path.adapter = null
|
||||
|
||||
|
||||
func _test_callback_reentry_and_tamper_defense() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := ControlledPathService.new()
|
||||
var adapter := AdapterType.new()
|
||||
path.adapter = adapter
|
||||
adapter.configure(source, path, [Vector3i(0, 0, 0), Vector3i(2, 0, 0)])
|
||||
path.reenter_next_rebuild = true
|
||||
var outer = adapter.place_route(&"outer_path_callback", &"outer_route", [Vector3i(1, 0, 0)])
|
||||
_expect(outer.is_success(), "outer transaction commits when Path callback only attempts reentry")
|
||||
_expect(path.reentry_result != null and not path.reentry_result.is_success(), "Path callback reentry is rejected")
|
||||
_expect(not source.has_entity(&"nested_path_route"), "reentrant Path callback cannot create a nested ROUTE")
|
||||
|
||||
var nested_source_result: Array = [null]
|
||||
source.configure_compatibility_validator(
|
||||
func(_payload: Dictionary) -> Dictionary:
|
||||
nested_source_result[0] = adapter.place_route(
|
||||
&"nested_source_request",
|
||||
&"nested_source_route",
|
||||
[Vector3i(5, 0, 0)]
|
||||
)
|
||||
return {&"ok": true}
|
||||
)
|
||||
var source_outer = adapter.place_route(&"outer_source_callback", &"second_route", [Vector3i(3, 0, 0)])
|
||||
_expect(source_outer.is_success(), "outer transaction commits when Spatial validator only attempts reentry")
|
||||
_expect(nested_source_result[0] != null and not nested_source_result[0].is_success(), "Spatial callback reentry is rejected")
|
||||
_expect(not source.has_entity(&"nested_source_route"), "reentrant Spatial callback cannot create a nested ROUTE")
|
||||
source.configure_compatibility_validator(Callable())
|
||||
|
||||
var before_adapter := adapter.create_snapshot()
|
||||
var before_source := source.create_snapshot()
|
||||
var before_path := path.create_snapshot()
|
||||
path.tamper_adapter_next_rebuild = true
|
||||
var tampered = adapter.replace_route(&"path_tamper_request", &"second_route", [Vector3i(4, 0, 0)])
|
||||
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, tampered, "Path callback adapter tamper")
|
||||
_expect(not adapter.is_quarantined(), "detected callback tamper with exact rollback stays available")
|
||||
|
||||
source.configure_compatibility_validator(
|
||||
func(_payload: Dictionary) -> Dictionary:
|
||||
return {&"ok": false, &"reason": &"WORLD_POLICY_REJECTED"}
|
||||
)
|
||||
before_adapter = adapter.create_snapshot()
|
||||
before_source = source.create_snapshot()
|
||||
before_path = path.create_snapshot()
|
||||
var rejected = adapter.place_route(&"source_rejection", &"rejected_route", [Vector3i(6, 0, 0)])
|
||||
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, rejected, "Spatial validator rejection")
|
||||
source.configure_compatibility_validator(Callable())
|
||||
path.adapter = null
|
||||
|
||||
|
||||
func _test_uncertain_rollback_quarantines_and_recovers() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := ControlledPathService.new()
|
||||
var adapter := AdapterType.new()
|
||||
path.adapter = adapter
|
||||
var start := Vector3i(0, 0, 0)
|
||||
var middle := Vector3i(1, 0, 0)
|
||||
var goal := Vector3i(2, 0, 0)
|
||||
var base := [start, goal]
|
||||
adapter.configure(source, path, base)
|
||||
var recovery_adapter := adapter.create_snapshot()
|
||||
var recovery_source := source.create_snapshot()
|
||||
var recovery_path := path.create_snapshot()
|
||||
path.fail_next_rebuild_after_mutation = true
|
||||
path.reject_restore = true
|
||||
var failed = adapter.place_route(&"uncertain_request", &"uncertain_route", [middle])
|
||||
_expect(not failed.is_success() and not failed.details[&"rollback_ok"], "uncertain rollback reports failure explicitly")
|
||||
_expect(adapter.is_quarantined() and adapter.get_quarantine_reason() == adapter.QUARANTINE_TRANSACTION_ROLLBACK, "uncertain rollback quarantines the adapter")
|
||||
_expect(adapter.is_projection_stale(), "quarantined projection reports stale")
|
||||
var blocked = adapter.place_route(&"blocked_in_quarantine", &"blocked_route", [Vector3i(3, 0, 0)])
|
||||
_expect(blocked.code == CommandResultType.Code.TRANSACTION_CLOSED, "quarantine blocks further mutations")
|
||||
|
||||
path.reject_restore = false
|
||||
_expect(source.restore_snapshot(recovery_source), "recovery restores the Spatial source snapshot")
|
||||
_restore_path_for_test(path, base, recovery_path)
|
||||
_expect(path.create_snapshot() == recovery_path, "recovery restores the exact Path snapshot")
|
||||
_expect(adapter.restore_snapshot(recovery_adapter), "exact adapter snapshot clears quarantine after dependencies recover")
|
||||
_expect(not adapter.is_quarantined() and not adapter.is_projection_stale(), "snapshot recovery returns the adapter to service")
|
||||
_expect(adapter.place_route(&"post_recovery", &"recovered_route", [middle]).is_success(), "route transaction succeeds after quarantine recovery")
|
||||
path.adapter = null
|
||||
|
||||
|
||||
func _test_strict_snapshot_round_trip_and_atomic_rejection() -> void:
|
||||
var source := SpatialType.new()
|
||||
var path := PathType.new()
|
||||
var adapter := AdapterType.new()
|
||||
var base := [Vector3i(0, 0, 0), Vector3i(3, 0, 0)]
|
||||
adapter.configure(source, path, base)
|
||||
adapter.place_route(&"z_place", &"route_z", [Vector3i(1, 0, 0)])
|
||||
adapter.place_route(&"a_place", &"route_a", [Vector3i(2, 0, 0)])
|
||||
adapter.synchronize_projection(&"b_sync_base", base)
|
||||
adapter.replace_route(&"m_noop", &"route_a", [Vector3i(2, 0, 0)])
|
||||
var snapshot := adapter.create_snapshot()
|
||||
_expect(
|
||||
snapshot[&"processed_requests"].map(func(row: Dictionary) -> String: return row[&"request_id"]) == ["a_place", "b_sync_base", "m_noop", "z_place"],
|
||||
"snapshot request history is lexical"
|
||||
)
|
||||
var restored_source := SpatialType.new()
|
||||
_expect(restored_source.restore_snapshot(source.create_snapshot()), "snapshot test restores a matching Spatial source")
|
||||
var restored_path := PathType.new()
|
||||
var restored := AdapterType.new()
|
||||
_expect(restored.configure(restored_source, restored_path, base), "snapshot test configures matching dependencies")
|
||||
_expect(restored.restore_snapshot(snapshot), "strict current adapter snapshot restores")
|
||||
_expect(restored.create_snapshot() == snapshot, "adapter snapshot round trip is exact")
|
||||
_expect(restored.validate_invariants()[&"ok"], "restored adapter snapshot crosslinks are valid")
|
||||
var replay = restored.place_route(&"z_place", &"route_z", [Vector3i(1, 0, 0)])
|
||||
_expect(replay.is_success() and replay.is_replay, "restored idempotency history replays exactly")
|
||||
var before := restored.create_snapshot()
|
||||
var source_before := restored_source.create_snapshot()
|
||||
var path_before := restored_path.create_snapshot()
|
||||
|
||||
var version_type := snapshot.duplicate(true)
|
||||
version_type[&"schema_version"] = "1"
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, version_type, before, source_before, path_before, "string schema version")
|
||||
var unknown := snapshot.duplicate(true)
|
||||
unknown[&"unknown"] = true
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, unknown, before, source_before, path_before, "unknown root field")
|
||||
var source_revision_type := snapshot.duplicate(true)
|
||||
source_revision_type[&"source_revision"] = float(source_revision_type[&"source_revision"])
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, source_revision_type, before, source_before, path_before, "floating source revision")
|
||||
var projection_bound := snapshot.duplicate(true)
|
||||
projection_bound[&"projection_revision"] = 0
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, projection_bound, before, source_before, path_before, "zero projection revision")
|
||||
var projection_history_gap := snapshot.duplicate(true)
|
||||
projection_history_gap[&"projection_revision"] = int(projection_history_gap[&"projection_revision"]) + 1
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, projection_history_gap, before, source_before, path_before, "projection revision without request evidence")
|
||||
var unordered_base := snapshot.duplicate(true)
|
||||
unordered_base[&"base_walkable_cells"].reverse()
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, unordered_base, before, source_before, path_before, "unordered base cells")
|
||||
var duplicate_route := snapshot.duplicate(true)
|
||||
duplicate_route[&"projected_route_cells"].append(duplicate_route[&"projected_route_cells"][0])
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, duplicate_route, before, source_before, path_before, "duplicate projected route cell")
|
||||
var broken_union := snapshot.duplicate(true)
|
||||
broken_union[&"projected_walkable_cells"].remove_at(1)
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, broken_union, before, source_before, path_before, "broken projected union")
|
||||
var quarantine_reason := snapshot.duplicate(true)
|
||||
quarantine_reason[&"quarantine_reason"] = "UNKNOWN_REASON"
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, quarantine_reason, before, source_before, path_before, "orphan quarantine reason")
|
||||
var request_order := snapshot.duplicate(true)
|
||||
request_order[&"processed_requests"].reverse()
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, request_order, before, source_before, path_before, "unordered request history")
|
||||
var request_extra := snapshot.duplicate(true)
|
||||
request_extra[&"processed_requests"][0][&"extra"] = true
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, request_extra, before, source_before, path_before, "unknown request field")
|
||||
var fingerprint := snapshot.duplicate(true)
|
||||
fingerprint[&"processed_requests"][0][&"fingerprint"] = "tampered"
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, fingerprint, before, source_before, path_before, "request fingerprint tamper")
|
||||
var sync_input := snapshot.duplicate(true)
|
||||
sync_input[&"processed_requests"][1][&"input_cells"] = [Vector3i(9, 0, 0)]
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, sync_input, before, source_before, path_before, "synchronization base fingerprint tamper")
|
||||
var detail_revision := snapshot.duplicate(true)
|
||||
detail_revision[&"processed_requests"][0][&"details"][&"projection_revision"] = int(snapshot[&"projection_revision"]) + 1
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, detail_revision, before, source_before, path_before, "request result revision tamper")
|
||||
var detail_cells := snapshot.duplicate(true)
|
||||
detail_cells[&"processed_requests"][0][&"details"][&"cells"] = [Vector3i(8, 0, 0)]
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, detail_cells, before, source_before, path_before, "request result cell tamper")
|
||||
var deep := snapshot.duplicate(true)
|
||||
var deep_value: Variant = []
|
||||
for _index: int in range(adapter.MAX_SNAPSHOT_DEPTH + 2):
|
||||
deep_value = [deep_value]
|
||||
deep[&"processed_requests"] = deep_value
|
||||
_atomic_snapshot_reject(restored, restored_source, restored_path, deep, before, source_before, path_before, "excessive snapshot depth")
|
||||
|
||||
restored_path.rebuild_graph(base)
|
||||
var stale_before := restored.create_snapshot()
|
||||
_expect(not restored.restore_snapshot(snapshot), "valid metadata snapshot rejects mismatched live Path topology")
|
||||
_expect(restored.create_snapshot() == stale_before, "live-crosslink rejection is adapter-atomic")
|
||||
_expect(restored.synchronize_projection(&"repair_after_restore_reject").is_success(), "synchronization repairs live mismatch after restore rejection")
|
||||
|
||||
|
||||
func _test_strict_input_and_route_only_boundary() -> void:
|
||||
var invalid_adapter := AdapterType.new()
|
||||
_expect(not invalid_adapter.configure(SpatialType.new(), PathType.new(), Vector3i.ZERO), "non-Array base cells are rejected")
|
||||
_expect(not invalid_adapter.configure(SpatialType.new(), PathType.new(), [Vector3i.ZERO, Vector3i.ZERO]), "duplicate base cells are rejected")
|
||||
_expect(not invalid_adapter.configure(SpatialType.new(), PathType.new(), [Vector3i(0, 0, 2)]), "invalid base layer is rejected")
|
||||
|
||||
var source := SpatialType.new()
|
||||
var path := PathType.new()
|
||||
var adapter := AdapterType.new()
|
||||
adapter.configure(source, path, [Vector3i.ZERO])
|
||||
_expect(adapter.is_bound_to(source, path), "adapter reports its exact authoritative service binding")
|
||||
_expect(not adapter.is_bound_to(SpatialType.new(), path), "binding check rejects another equal Spatial instance")
|
||||
_expect(not adapter.is_bound_to(source, PathType.new()), "binding check rejects another equal Path instance")
|
||||
var before := adapter.create_snapshot()
|
||||
_expect(not adapter.place_route(1, &"route", [Vector3i(1, 0, 0)]).is_success(), "numeric request ID is rejected")
|
||||
_expect(not adapter.place_route(&"bad_entity", 1, [Vector3i(1, 0, 0)]).is_success(), "numeric entity ID is rejected")
|
||||
_expect(not adapter.place_route(&"bad_cells", &"route", Vector3i(1, 0, 0)).is_success(), "non-Array route cells are rejected")
|
||||
_expect(not adapter.place_route(&"empty_cells", &"route", []).is_success(), "empty route placement is rejected")
|
||||
_expect(not adapter.place_route(&"duplicate_cells", &"route", [Vector3i(1, 0, 0), Vector3i(1, 0, 0)]).is_success(), "duplicate route cells are rejected")
|
||||
_expect(not adapter.place_route(&"invalid_layer", &"route", [Vector3i(1, 0, 2)]).is_success(), "invalid route layer is rejected")
|
||||
_expect(adapter.create_snapshot() == before, "malformed route requests preserve adapter state")
|
||||
_expect(source.place(&"decor_only", source.CATEGORY_DECORATION, [Vector3i.ZERO]).is_success(), "DECORATION entity exists beside adapter boundary")
|
||||
var source_before := source.create_snapshot()
|
||||
var path_before := path.create_snapshot()
|
||||
var wrong_category = adapter.remove_route(&"remove_decor", &"decor_only")
|
||||
_expect(not wrong_category.is_success(), "adapter refuses to remove a non-ROUTE entity")
|
||||
_expect(source.create_snapshot() == source_before and path.create_snapshot() == path_before, "route-only rejection leaves both services untouched")
|
||||
_expect(not adapter.configure(source, path, [Vector3i.ZERO]), "configured adapter rejects reconfiguration")
|
||||
|
||||
|
||||
func _assert_exact_rollback(
|
||||
adapter: RouteSubslotPathAdapter,
|
||||
source: SpatialSubslotService,
|
||||
path: PathService,
|
||||
adapter_before: Dictionary,
|
||||
source_before: Dictionary,
|
||||
path_before: Dictionary,
|
||||
result: CommandResult,
|
||||
label: String
|
||||
) -> void:
|
||||
_expect(not result.is_success() and bool(result.details.get(&"rollback_ok", false)), "%s reports exact rollback" % label)
|
||||
_expect(adapter.create_snapshot() == adapter_before, "%s restores exact adapter snapshot" % label)
|
||||
_expect(source.create_snapshot() == source_before, "%s restores exact Spatial snapshot" % label)
|
||||
_expect(path.create_snapshot() == path_before, "%s restores exact Path snapshot" % label)
|
||||
|
||||
|
||||
func _atomic_snapshot_reject(
|
||||
adapter: RouteSubslotPathAdapter,
|
||||
source: SpatialSubslotService,
|
||||
path: PathService,
|
||||
candidate: Dictionary,
|
||||
adapter_before: Dictionary,
|
||||
source_before: Dictionary,
|
||||
path_before: Dictionary,
|
||||
label: String
|
||||
) -> void:
|
||||
_expect(not adapter.restore_snapshot(candidate), "%s is rejected" % label)
|
||||
_expect(adapter.create_snapshot() == adapter_before, "%s rejection is adapter-atomic" % label)
|
||||
_expect(source.create_snapshot() == source_before and path.create_snapshot() == path_before, "%s rejection leaves dependencies untouched" % label)
|
||||
|
||||
|
||||
func _restore_path_for_test(path: PathService, cells: Array, snapshot: Dictionary) -> void:
|
||||
path.rebuild_graph(cells)
|
||||
path.restore_snapshot(snapshot)
|
||||
path.rebuild_graph(cells)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bigl6r8i1b5cl
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://b4465rs47daij
|
||||
@@ -0,0 +1,188 @@
|
||||
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("S1: runtime world and construction integration")
|
||||
_reset_runtime()
|
||||
_test_world_bootstrap()
|
||||
_test_rank_expansion()
|
||||
_test_build_to_completion()
|
||||
_test_overlap_is_atomic()
|
||||
_test_mid_build_snapshot()
|
||||
_test_cancel_and_demolish()
|
||||
_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() -> 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()
|
||||
_runtime.initialize(_clock, _state, 8080)
|
||||
_clock.set_paused(true)
|
||||
|
||||
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_world_bootstrap() -> void:
|
||||
var bounds: Dictionary = _runtime.world_service.get_bounds()
|
||||
_expect(bounds == {&"min_x": -12, &"max_x": 12, &"min_y": 0, &"max_y": 6}, "Rank 1 world uses canonical map bounds")
|
||||
_expect(_runtime.world_service.is_walkable(Vector3i(-12, 0, WorldService.LAYER_FRONT)), "starter FRONT boundary corridor is walkable")
|
||||
_expect(_runtime.world_service.is_support_anchor(Vector3i(0, 0, WorldService.LAYER_FRONT)), "starter boundary corridor is structurally anchored")
|
||||
_expect(_runtime.path_service.has_connector(&"starter.staff_door"), "starter staff door explicitly connects FRONT and BACK")
|
||||
|
||||
func _test_rank_expansion() -> void:
|
||||
var reputation = _runtime.submit_command(
|
||||
&"update_progression_metrics",
|
||||
{&"metrics": {&"reputation": 25}},
|
||||
&"player", &"", 100, -1, &"s1.rank.reputation"
|
||||
)
|
||||
var capital = _runtime.submit_command(
|
||||
&"change_resources",
|
||||
{&"deltas": {&"gold": 5_000}},
|
||||
&"player", &"", 100, -1, &"s1.rank.capital"
|
||||
)
|
||||
_expect(reputation.is_success() and capital.is_success(), "Rank 2 eligibility facts are recorded before promotion")
|
||||
var result = _runtime.submit_command(
|
||||
&"set_world_tree_rank",
|
||||
{&"rank": 2},
|
||||
&"player", &"", 100, -1, &"s1.rank.two"
|
||||
)
|
||||
_expect(result.is_success() and _state.world_tree_rank == 2, "rank command expands progression exactly once")
|
||||
_expect(_runtime.world_service.get_bounds() == {&"min_x": -18, &"max_x": 18, &"min_y": -4, &"max_y": 10}, "Rank 2 uses canonical expanded bounds")
|
||||
_expect(_runtime.world_service.is_walkable(Vector3i(18, 0, WorldService.LAYER_FRONT)), "new boundary corridor cells become walkable")
|
||||
_expect(not _runtime.world_service.is_excavated(Vector3i(0, -1, WorldService.LAYER_FRONT)), "new underground cells remain unexcavated")
|
||||
var normalize_capital = _runtime.submit_command(
|
||||
&"change_resources",
|
||||
{&"deltas": {&"gold": -5_000}},
|
||||
&"player", &"", 100, -1, &"s1.rank.capital.normalize"
|
||||
)
|
||||
_expect(normalize_capital.is_success() and _state.get_resource(&"gold") == 5_000, "Rank fixture restores the construction budget")
|
||||
var rejected = _runtime.submit_command(
|
||||
&"set_world_tree_rank", {&"rank": 1}, &"player", &"", 100, -1, &"s1.rank.shrink"
|
||||
)
|
||||
_expect(not rejected.is_success(), "world rank and bounds cannot shrink")
|
||||
|
||||
func _test_build_to_completion() -> void:
|
||||
var result = _runtime.submit_command(
|
||||
&"build_facility",
|
||||
{&"definition_id": &"reception", &"origin": Vector3i(-6, 0, WorldService.LAYER_FRONT), &"rotation": 0},
|
||||
&"player", &"", 50, -1, &"s1.build.reception"
|
||||
)
|
||||
var build_label := "valid build command creates a construction job"
|
||||
if not result.is_success():
|
||||
build_label += " (%s: %s; %s)" % [CommandResult.Code.keys()[result.code], result.message, result.details]
|
||||
_expect(result.is_success(), build_label)
|
||||
var job_id := StringName(result.details.get(&"job_id", &""))
|
||||
var job = _runtime.construction_service.get_job(job_id)
|
||||
_expect(job != null and job.state == ConstructionJob.State.PLANNED, "new facility begins as PLANNED")
|
||||
_expect(_state.get_resource(&"gold") == 4500 and _state.get_resource(&"materials") == 42, "construction cost is paid once")
|
||||
_expect(not job_id.is_empty() and _runtime.world_service.get_occupant(Vector3i(-6, 0, 0)) == job_id, "construction job reserves its footprint")
|
||||
_expect(_runtime.latest_digest.length() == 64, "build command leaves a serializable runtime snapshot")
|
||||
if job == null:
|
||||
return
|
||||
_clock.advance_minutes(119)
|
||||
_expect(job.state == ConstructionJob.State.BUILDING and job.progress < ConstructionJob.MAX_PROGRESS, "job remains BUILDING before its final minute")
|
||||
_clock.advance_one_minute()
|
||||
_expect(job.state == ConstructionJob.State.COMPLETED, "job completes at its configured build time")
|
||||
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
|
||||
_expect(facilities.size() == 1, "completion creates one facility instance")
|
||||
var instance_id := StringName(facilities[0][&"instance_id"])
|
||||
_expect(_runtime.world_service.get_occupant(Vector3i(-6, 0, 0)) == instance_id, "completion transfers world occupancy to the facility")
|
||||
var saw_completion := false
|
||||
for event in _runtime.domain_events.recent_events():
|
||||
if event.event_type == &"FACILITY_COMPLETED" and instance_id in event.entity_ids:
|
||||
saw_completion = true
|
||||
_expect(saw_completion, "facility completion is published as a domain event")
|
||||
|
||||
func _test_overlap_is_atomic() -> void:
|
||||
var before_resources: Dictionary = _state.create_snapshot()
|
||||
var before_job_count: int = _runtime.construction_service.get_jobs().size()
|
||||
var result = _runtime.submit_command(
|
||||
&"build_facility",
|
||||
{&"definition_id": &"reception", &"origin": [-6, 0, 0], &"rotation": 0},
|
||||
&"player", &"", 50, -1, &"s1.build.overlap"
|
||||
)
|
||||
_expect(not result.is_success(), "overlapping construction command is rejected")
|
||||
_expect(_state.create_snapshot() == before_resources, "rejected placement spends no resource")
|
||||
_expect(_runtime.construction_service.get_jobs().size() == before_job_count, "rejected placement creates no job")
|
||||
|
||||
func _test_mid_build_snapshot() -> void:
|
||||
var result = _runtime.submit_command(
|
||||
&"build_facility",
|
||||
{&"definition_id": &"warehouse", &"origin": Vector3i(2, 0, WorldService.LAYER_BACK), &"rotation": 0},
|
||||
&"player", &"", 40, -1, &"s1.build.warehouse"
|
||||
)
|
||||
_expect(result.is_success(), "BACK-layer warehouse construction starts")
|
||||
var job_id := StringName(result.details[&"job_id"])
|
||||
_clock.advance_minutes(90)
|
||||
var snapshot: Dictionary = _runtime.create_snapshot()
|
||||
var expected_digest := SimulationDigest.compute(snapshot)
|
||||
var expected_progress: int = _runtime.construction_service.get_job(job_id).progress
|
||||
_clock.advance_minutes(90)
|
||||
_expect(_runtime.construction_service.get_job(job_id).state == ConstructionJob.State.COMPLETED, "warehouse can complete after the snapshot")
|
||||
_expect(_runtime.restore_snapshot(snapshot), "mid-build world and construction snapshot restores")
|
||||
_expect(_runtime.latest_digest == expected_digest, "mid-build restore reproduces the exact digest")
|
||||
_expect(_runtime.construction_service.get_job(job_id).progress == expected_progress, "mid-build integer progress restores exactly")
|
||||
_clock.advance_minutes(90)
|
||||
_expect(_runtime.construction_service.get_job(job_id).state == ConstructionJob.State.COMPLETED, "restored build completes once")
|
||||
|
||||
func _test_cancel_and_demolish() -> void:
|
||||
var materials_before_cancel: int = _state.get_resource(&"materials")
|
||||
var start = _runtime.submit_command(
|
||||
&"build_facility",
|
||||
{&"definition_id": &"cafeteria", &"origin": Vector3i(7, 0, WorldService.LAYER_FRONT), &"rotation": 90},
|
||||
&"player", &"", 30, -1, &"s1.build.cancel_target"
|
||||
)
|
||||
_expect(start.is_success(), "cancellable construction starts")
|
||||
var job_id := StringName(start.details[&"job_id"])
|
||||
var cancel = _runtime.submit_command(
|
||||
&"cancel_construction",
|
||||
{&"job_id": job_id},
|
||||
&"player", &"", 30, -1, &"s1.cancel.target"
|
||||
)
|
||||
_expect(cancel.is_success(), "zero-progress construction cancels")
|
||||
_expect(_state.get_resource(&"materials") == materials_before_cancel, "zero-progress cancellation returns all materials")
|
||||
_expect(_runtime.world_service.get_entity_cells(job_id).is_empty(), "cancellation releases reserved world cells")
|
||||
|
||||
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
|
||||
var reception_id := StringName(facilities[0][&"instance_id"])
|
||||
var materials_before_demolition: int = _state.get_resource(&"materials")
|
||||
var demolition = _runtime.submit_command(
|
||||
&"demolish_facility",
|
||||
{&"instance_id": reception_id},
|
||||
&"player", &"", 30, -1, &"s1.demolish.reception"
|
||||
)
|
||||
_expect(demolition.is_success(), "completed facility can be demolished through a command")
|
||||
_expect(_state.get_resource(&"materials") == materials_before_demolition + 2, "demolition returns 25 percent of reception materials")
|
||||
_expect(_runtime.world_service.get_entity_cells(reception_id).is_empty(), "demolition releases facility occupancy")
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bonrrpg25e8s
|
||||
@@ -0,0 +1,312 @@
|
||||
extends SceneTree
|
||||
|
||||
const SpatialSubslotServiceType = preload("res://scripts/world/spatial_subslot_service.gd")
|
||||
const WorldServiceType = preload("res://scripts/world/world_service.gd")
|
||||
const DigestType = preload("res://scripts/core/simulation_digest.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S1: Spatial subslots")
|
||||
_test_three_independent_slots_and_world_coexistence()
|
||||
_test_revision_dirty_and_replacement()
|
||||
_test_strict_input_limits()
|
||||
_test_compatibility_callback_atomicity_and_reentry()
|
||||
_test_snapshot_round_trip_and_corruption_rejection()
|
||||
_test_deterministic_insertion_order()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_three_independent_slots_and_world_coexistence() -> void:
|
||||
var world := WorldServiceType.new()
|
||||
world.reset({&"min_x": -3, &"max_x": 3, &"min_y": -3, &"max_y": 3})
|
||||
var shared := Vector3i(0, 0, 0)
|
||||
_expect(world.occupy(&"major_facility", [shared], &"facility_large"), "major World occupant registers first")
|
||||
var service := SpatialSubslotServiceType.new()
|
||||
var route_cells := [Vector3i(1, 0, 0), shared, Vector3i(0, -1, 1)]
|
||||
_expect(service.place(&"route_main", service.CATEGORY_ROUTE, route_cells).is_success(), "multi-cell ROUTE shares a primary World cell")
|
||||
_expect(world.get_occupant(shared) == &"major_facility", "subslot placement never mutates primary World occupancy")
|
||||
_expect(service.place(&"decor_banner", service.CATEGORY_DECORATION, [shared]).is_success(), "DECORATION shares the ROUTE cell")
|
||||
_expect(service.place(&"utility_pipe", service.CATEGORY_UTILITY, [shared]).is_success(), "UTILITY shares ROUTE and DECORATION")
|
||||
_expect(
|
||||
service.get_cell_slots(shared) == {
|
||||
&"ROUTE": &"route_main",
|
||||
&"DECORATION": &"decor_banner",
|
||||
&"UTILITY": &"utility_pipe",
|
||||
},
|
||||
"one layer cell exposes exactly three independent category slots"
|
||||
)
|
||||
_expect(
|
||||
service.get_entity_cells(&"route_main") == [
|
||||
Vector3i(0, -1, 1), shared, Vector3i(1, 0, 0),
|
||||
],
|
||||
"one entity owns multiple cells in canonical y/x/layer order"
|
||||
)
|
||||
_expect(
|
||||
service.get_route_cells() == [Vector3i(0, -1, 1), shared, Vector3i(1, 0, 0)],
|
||||
"route query is ready for deterministic Path graph input"
|
||||
)
|
||||
var before_overlap := service.create_snapshot()
|
||||
_expect(not service.place(&"route_overlap", service.CATEGORY_ROUTE, [shared]).is_success(), "second ROUTE in one cell is rejected")
|
||||
_expect(service.create_snapshot() == before_overlap, "same-category overlap rejection is atomic")
|
||||
_expect(not service.place(&"route_main", service.CATEGORY_UTILITY, [Vector3i(2, 0, 0)]).is_success(), "one entity ID cannot own a second category")
|
||||
|
||||
var returned_cells := service.get_entity_cells(&"route_main")
|
||||
returned_cells.clear()
|
||||
var returned_entities := service.get_entities()
|
||||
returned_entities[0][&"cells"].clear()
|
||||
var returned_slots := service.get_cell_slots(shared)
|
||||
returned_slots.clear()
|
||||
_expect(service.get_entity_cells(&"route_main").size() == 3, "entity cell query is defensive")
|
||||
_expect(service.get_entities().size() == 3, "entity records are defensive")
|
||||
_expect(service.get_cell_slots(shared).size() == 3, "cell slot query is defensive")
|
||||
_expect(service.validate_invariants()[&"ok"], "three-way coexistence preserves all crosslinks")
|
||||
|
||||
|
||||
func _test_revision_dirty_and_replacement() -> void:
|
||||
var service := SpatialSubslotServiceType.new()
|
||||
var a := Vector3i(1, 1, 0)
|
||||
var b := Vector3i(-1, 0, 1)
|
||||
var c := Vector3i(2, -1, 0)
|
||||
_expect(service.place(&"route", service.CATEGORY_ROUTE, [a, b]).is_success(), "first mutation succeeds")
|
||||
_expect(service.revision == 1 and service.get_revision_for_category(service.CATEGORY_ROUTE) == 1, "ROUTE mutation advances global and category revisions once")
|
||||
_expect(service.get_revision_for_category(service.CATEGORY_DECORATION) == 0, "unmodified category revision remains zero")
|
||||
_expect(service.place(&"decor", service.CATEGORY_DECORATION, [a]).is_success(), "second category mutation succeeds")
|
||||
_expect(service.revision == 2 and service.get_revision_for_category(service.CATEGORY_DECORATION) == 2, "DECORATION records its exact last revision")
|
||||
_expect(service.get_dirty_cells() == [b, a], "dirty cells are canonical and deduplicated")
|
||||
var consumed := service.consume_dirty_cells()
|
||||
consumed.clear()
|
||||
_expect(service.get_dirty_cells().is_empty() and service.revision == 2, "dirty consumption is defensive and does not advance revision")
|
||||
|
||||
var replacement := service.replace_entity_cells(&"route", [c, a])
|
||||
_expect(replacement.is_success() and replacement.details[&"changed"], "entity cell set can be atomically replaced")
|
||||
_expect(service.revision == 3 and service.get_revision_for_category(service.CATEGORY_ROUTE) == 3, "replacement advances only its category revision")
|
||||
_expect(service.get_dirty_cells() == [c, b, a], "replacement dirties removed, retained and added cells once")
|
||||
var no_op_revision := service.revision
|
||||
_expect(service.replace_entity_cells(&"route", [a, c]).is_success(), "canonical-equivalent replacement is a successful no-op")
|
||||
_expect(service.revision == no_op_revision, "replacement no-op does not change revision")
|
||||
_expect(service.remove(&"decor").is_success(), "entity removal succeeds")
|
||||
_expect(service.revision == 4 and service.get_revision_for_category(service.CATEGORY_DECORATION) == 4, "removal advances exact category revision")
|
||||
_expect(service.get_slot_entity(a, service.CATEGORY_ROUTE) == &"route", "removal preserves another category in the same cell")
|
||||
_expect(service.get_slot_entity(a, service.CATEGORY_DECORATION).is_empty(), "removal clears only the owned category slot")
|
||||
_expect(not service.remove(&"decor").is_success(), "double removal is rejected")
|
||||
_expect(service.validate_invariants()[&"ok"], "replacement and removal preserve strict crosslinks")
|
||||
|
||||
|
||||
func _test_strict_input_limits() -> void:
|
||||
var service := SpatialSubslotServiceType.new()
|
||||
var valid_snapshot := service.create_snapshot()
|
||||
_expect(not service.place(12, service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "integer entity ID is rejected")
|
||||
_expect(not service.place(&"", service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "empty entity ID is rejected")
|
||||
_expect(not service.place(StringName("x".repeat(service.MAX_ENTITY_ID_LENGTH + 1)), service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "oversized entity ID is rejected")
|
||||
_expect(not service.place(&"bad id", service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "unstable entity ID is rejected")
|
||||
_expect(not service.place(&"lower_category", &"route", [Vector3i.ZERO]).is_success(), "non-canonical category is rejected")
|
||||
_expect(not service.place(&"numeric_category", 0, [Vector3i.ZERO]).is_success(), "numeric category is rejected rather than coerced")
|
||||
_expect(not service.place(&"not_array", service.CATEGORY_ROUTE, Vector3i.ZERO).is_success(), "non-Array cells are rejected")
|
||||
_expect(not service.place(&"empty_cells", service.CATEGORY_ROUTE, []).is_success(), "empty cell set is rejected")
|
||||
_expect(not service.place(&"duplicate_cells", service.CATEGORY_ROUTE, [Vector3i.ZERO, Vector3i.ZERO]).is_success(), "duplicate cells are rejected")
|
||||
_expect(not service.place(&"wrong_cell_type", service.CATEGORY_ROUTE, [Vector2i.ZERO]).is_success(), "non-Vector3i cell is rejected")
|
||||
_expect(not service.place(&"wrong_layer", service.CATEGORY_ROUTE, [Vector3i(0, 0, 2)]).is_success(), "unknown layer is rejected")
|
||||
_expect(not service.place(&"deep_coordinate", service.CATEGORY_ROUTE, [Vector3i(0, service.MAX_COORDINATE_ABS + 1, 0)]).is_success(), "coordinate depth limit is enforced")
|
||||
var too_many_cells: Array[Vector3i] = []
|
||||
for x: int in range(service.MAX_CELLS_PER_ENTITY + 1):
|
||||
too_many_cells.append(Vector3i(x, 0, 0))
|
||||
_expect(not service.place(&"too_many_cells", service.CATEGORY_ROUTE, too_many_cells).is_success(), "per-entity cell capacity is enforced")
|
||||
_expect(service.create_snapshot() == valid_snapshot and service.revision == 0, "all malformed input failures preserve the empty state")
|
||||
_expect(service.get_entity_ids(&"route").is_empty(), "invalid category query fails closed")
|
||||
_expect(service.get_revision_for_category(&"route") == -1, "invalid category revision query fails closed")
|
||||
|
||||
|
||||
func _test_compatibility_callback_atomicity_and_reentry() -> void:
|
||||
var service := SpatialSubslotServiceType.new()
|
||||
var callback_payloads: Array[Dictionary] = []
|
||||
service.configure_compatibility_validator(
|
||||
func(payload: Dictionary) -> Dictionary:
|
||||
callback_payloads.append(payload.duplicate(true))
|
||||
payload[&"after_cells"].clear()
|
||||
return {&"ok": true, &"details": {&"pass_through": true}}
|
||||
)
|
||||
_expect(service.place(&"pass_route", service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "pass-through validator can approve placement")
|
||||
_expect(callback_payloads[0][&"operation"] == &"PLACE", "validator receives explicit operation context")
|
||||
_expect(callback_payloads[0][&"after_cells"] == [Vector3i.ZERO], "validator payload captured by caller retains exact candidate")
|
||||
_expect(service.get_entity_cells(&"pass_route") == [Vector3i.ZERO], "callback payload mutation cannot alter committed cells")
|
||||
var callback_report := service.get_last_callback_report()
|
||||
callback_report[&"details"].clear()
|
||||
_expect(service.get_last_callback_report()[&"details"] == {&"pass_through": true}, "callback report is a defensive deep copy")
|
||||
|
||||
service.configure_compatibility_validator(
|
||||
func(_payload: Dictionary) -> Dictionary:
|
||||
return {&"ok": false, &"reason": &"PRIMARY_BLOCKS_CATEGORY"}
|
||||
)
|
||||
var before_rejection := service.create_snapshot()
|
||||
var rejected = service.replace_entity_cells(&"pass_route", [Vector3i(1, 0, 0)])
|
||||
_expect(not rejected.is_success() and rejected.details[&"rollback_ok"], "callback rejection reports successful pre-commit rollback")
|
||||
_expect(service.create_snapshot() == before_rejection, "callback failure preserves snapshot and revision exactly")
|
||||
|
||||
service.configure_compatibility_validator(
|
||||
func(_payload: Dictionary) -> Variant:
|
||||
return true
|
||||
)
|
||||
_expect(not service.place(&"invalid_response", service.CATEGORY_UTILITY, [Vector3i(2, 0, 0)]).is_success(), "non-Dictionary callback response is rejected")
|
||||
_expect(service.create_snapshot() == before_rejection, "invalid callback response is atomic")
|
||||
|
||||
var nested_result: Array = [null]
|
||||
var configure_during_callback: Array = [true]
|
||||
service.configure_compatibility_validator(
|
||||
func(_payload: Dictionary) -> Dictionary:
|
||||
nested_result[0] = service.place(&"nested", service.CATEGORY_DECORATION, [Vector3i(3, 0, 0)])
|
||||
configure_during_callback[0] = service.configure_compatibility_validator(Callable())
|
||||
return {&"ok": true}
|
||||
)
|
||||
_expect(service.place(&"outer", service.CATEGORY_DECORATION, [Vector3i(2, 0, 0)]).is_success(), "outer callback mutation commits")
|
||||
_expect(nested_result[0] != null and not nested_result[0].is_success(), "callback reentry is rejected")
|
||||
_expect(not configure_during_callback[0], "callback cannot replace its own boundary during mutation")
|
||||
_expect(service.has_entity(&"outer") and not service.has_entity(&"nested"), "reentry rejection commits only the outer entity")
|
||||
|
||||
service.configure_compatibility_validator(
|
||||
func(_payload: Dictionary) -> Dictionary:
|
||||
service._entity_cells[&"tampered"] = [Vector3i(9, 9, 0)]
|
||||
return {&"ok": true}
|
||||
)
|
||||
var before_tamper := service.create_snapshot()
|
||||
var tamper_result = service.place(&"tamper_probe", service.CATEGORY_UTILITY, [Vector3i(4, 0, 0)])
|
||||
_expect(not tamper_result.is_success() and tamper_result.details[&"rollback_ok"], "direct callback tamper is detected and rolled back")
|
||||
_expect(service.create_snapshot() == before_tamper and not service.has_entity(&"tampered"), "callback tamper rollback restores every internal crosslink")
|
||||
_expect(service.get_last_callback_report()[&"tampered"], "callback report records tamper detection")
|
||||
|
||||
service.configure_compatibility_validator(
|
||||
func(_payload: Dictionary) -> Dictionary:
|
||||
service._compatibility_validator = Callable()
|
||||
return {&"ok": true}
|
||||
)
|
||||
var before_boundary_tamper := service.create_snapshot()
|
||||
var boundary_tamper_result = service.place(&"boundary_tamper", service.CATEGORY_UTILITY, [Vector3i(5, 0, 0)])
|
||||
_expect(not boundary_tamper_result.is_success() and boundary_tamper_result.details[&"rollback_ok"], "callback cannot replace its validator boundary directly")
|
||||
_expect(service.create_snapshot() == before_boundary_tamper, "validator-boundary tamper rollback is atomic")
|
||||
service.configure_compatibility_validator(Callable())
|
||||
|
||||
|
||||
func _test_snapshot_round_trip_and_corruption_rejection() -> void:
|
||||
var source := SpatialSubslotServiceType.new()
|
||||
source.place(&"z_route", source.CATEGORY_ROUTE, [Vector3i(2, 0, 0), Vector3i(1, -1, 1)])
|
||||
source.place(&"a_decor", source.CATEGORY_DECORATION, [Vector3i(2, 0, 0)])
|
||||
source.place(&"m_utility", source.CATEGORY_UTILITY, [Vector3i(-1, 2, 1)])
|
||||
source.consume_dirty_cells()
|
||||
source.replace_entity_cells(&"z_route", [Vector3i(3, 0, 0), Vector3i(1, -1, 1)])
|
||||
var snapshot := source.create_snapshot()
|
||||
_expect(snapshot[&"entities"].map(func(row: Dictionary) -> String: return row[&"entity_id"]) == ["a_decor", "m_utility", "z_route"], "snapshot entities are lexical")
|
||||
var restored := SpatialSubslotServiceType.new()
|
||||
_expect(restored.restore_snapshot(snapshot), "strict current snapshot restores")
|
||||
_expect(restored.create_snapshot() == snapshot, "snapshot round trip is exact")
|
||||
_expect(DigestType.compute(restored.create_snapshot()) == DigestType.compute(snapshot), "snapshot digest round trip is deterministic")
|
||||
_expect(restored.validate_invariants()[&"ok"], "restored entity/category/cell graph is valid")
|
||||
var before := restored.create_snapshot()
|
||||
|
||||
var version_type := snapshot.duplicate(true)
|
||||
version_type[&"schema_version"] = "1"
|
||||
_atomic_reject(restored, version_type, before, "string schema version")
|
||||
var unknown_field := snapshot.duplicate(true)
|
||||
unknown_field[&"unknown"] = true
|
||||
_atomic_reject(restored, unknown_field, before, "unknown top-level field")
|
||||
var revision_type := snapshot.duplicate(true)
|
||||
revision_type[&"revision"] = 4.0
|
||||
_atomic_reject(restored, revision_type, before, "floating revision")
|
||||
var revision_bound := snapshot.duplicate(true)
|
||||
revision_bound[&"revision"] = source.MAX_REVISION + 1
|
||||
_atomic_reject(restored, revision_bound, before, "oversized revision")
|
||||
var category_keys := snapshot.duplicate(true)
|
||||
category_keys[&"category_revisions"].erase(&"UTILITY")
|
||||
_atomic_reject(restored, category_keys, before, "missing category revision")
|
||||
var category_revision := snapshot.duplicate(true)
|
||||
category_revision[&"category_revisions"][&"ROUTE"] = int(category_revision[&"revision"]) + 1
|
||||
_atomic_reject(restored, category_revision, before, "category revision above global revision")
|
||||
var missing_latest_category := snapshot.duplicate(true)
|
||||
missing_latest_category[&"category_revisions"][&"ROUTE"] = int(missing_latest_category[&"revision"]) - 1
|
||||
_atomic_reject(restored, missing_latest_category, before, "no category at global revision")
|
||||
var entity_order := snapshot.duplicate(true)
|
||||
entity_order[&"entities"].reverse()
|
||||
_atomic_reject(restored, entity_order, before, "unordered entities")
|
||||
var entity_extra := snapshot.duplicate(true)
|
||||
entity_extra[&"entities"][0][&"extra"] = 1
|
||||
_atomic_reject(restored, entity_extra, before, "unknown entity field")
|
||||
var entity_id_type := snapshot.duplicate(true)
|
||||
entity_id_type[&"entities"][0][&"entity_id"] = 1
|
||||
_atomic_reject(restored, entity_id_type, before, "non-string entity ID")
|
||||
var invalid_category := snapshot.duplicate(true)
|
||||
invalid_category[&"entities"][0][&"category"] = "DECOR"
|
||||
_atomic_reject(restored, invalid_category, before, "unknown category")
|
||||
var unsorted_cells := snapshot.duplicate(true)
|
||||
unsorted_cells[&"entities"][2][&"cells"].reverse()
|
||||
_atomic_reject(restored, unsorted_cells, before, "unordered entity cells")
|
||||
var duplicate_category_slot := snapshot.duplicate(true)
|
||||
duplicate_category_slot[&"entities"].insert(2, {
|
||||
&"entity_id": "n_route",
|
||||
&"category": "ROUTE",
|
||||
&"cells": [Vector3i(3, 0, 0)],
|
||||
})
|
||||
_atomic_reject(restored, duplicate_category_slot, before, "duplicate category slot")
|
||||
var invalid_cell := snapshot.duplicate(true)
|
||||
invalid_cell[&"entities"][0][&"cells"] = [Vector3i(0, 0, 2)]
|
||||
_atomic_reject(restored, invalid_cell, before, "invalid entity layer")
|
||||
var dirty_order := snapshot.duplicate(true)
|
||||
dirty_order[&"dirty_cells"].reverse()
|
||||
_atomic_reject(restored, dirty_order, before, "unordered dirty cells")
|
||||
var dirty_duplicate := snapshot.duplicate(true)
|
||||
dirty_duplicate[&"dirty_cells"].append(dirty_duplicate[&"dirty_cells"][0])
|
||||
_atomic_reject(restored, dirty_duplicate, before, "duplicate dirty cell")
|
||||
var depth := snapshot.duplicate(true)
|
||||
var deep_value: Variant = []
|
||||
for _index: int in range(source.MAX_SNAPSHOT_DEPTH + 2):
|
||||
deep_value = [deep_value]
|
||||
depth[&"entities"] = deep_value
|
||||
_atomic_reject(restored, depth, before, "excessive snapshot depth")
|
||||
var entity_capacity := snapshot.duplicate(true)
|
||||
var oversized_entities: Array = []
|
||||
oversized_entities.resize(source.MAX_ENTITIES + 1)
|
||||
entity_capacity[&"entities"] = oversized_entities
|
||||
_atomic_reject(restored, entity_capacity, before, "entity collection capacity")
|
||||
|
||||
var internal_before := restored.create_snapshot()
|
||||
restored._slots[Vector3i(99, 99, 0)] = {restored.CATEGORY_ROUTE: &"ghost"}
|
||||
_expect(not restored.validate_invariants()[&"ok"], "live invariant audit detects an orphan cell-side slot")
|
||||
_expect(restored.restore_snapshot(internal_before), "valid snapshot repairs audited private corruption")
|
||||
|
||||
|
||||
func _test_deterministic_insertion_order() -> void:
|
||||
var first := SpatialSubslotServiceType.new()
|
||||
var second := SpatialSubslotServiceType.new()
|
||||
first.place(&"z", first.CATEGORY_ROUTE, [Vector3i(2, 0, 1), Vector3i(-2, 0, 0)])
|
||||
first.place(&"a", first.CATEGORY_ROUTE, [Vector3i(0, 1, 0)])
|
||||
second.place(&"a", second.CATEGORY_ROUTE, [Vector3i(0, 1, 0)])
|
||||
second.place(&"z", second.CATEGORY_ROUTE, [Vector3i(-2, 0, 0), Vector3i(2, 0, 1)])
|
||||
_expect(first.create_snapshot() == second.create_snapshot(), "snapshot ignores Dictionary and input-cell insertion order")
|
||||
_expect(first.get_route_cells() == second.get_route_cells(), "route projection order is deterministic")
|
||||
|
||||
|
||||
func _atomic_reject(
|
||||
service: SpatialSubslotService,
|
||||
candidate: Dictionary,
|
||||
before: Dictionary,
|
||||
label: String
|
||||
) -> void:
|
||||
_expect(not service.restore_snapshot(candidate), "%s is rejected" % label)
|
||||
_expect(service.create_snapshot() == before, "%s rejection is atomic" % label)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnk1clco7jj33
|
||||
@@ -0,0 +1,199 @@
|
||||
extends SceneTree
|
||||
|
||||
const WorldServiceType = preload("res://scripts/world/world_service.gd")
|
||||
const DigestType = preload("res://scripts/core/simulation_digest.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S1: World Service")
|
||||
_test_bounds_zones_and_footprints()
|
||||
_test_occupancy_validation_and_release()
|
||||
_test_walkability_excavation_and_revisions()
|
||||
_test_connected_support_validation()
|
||||
_test_snapshot_round_trip_and_atomic_restore()
|
||||
_test_snapshot_order_is_deterministic()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_bounds_zones_and_footprints() -> void:
|
||||
var world = WorldServiceType.new()
|
||||
_expect(world.reset(_bounds(-2, 2, -2, 2)), "dictionary bounds are accepted")
|
||||
_expect(world.is_in_bounds(Vector3i(-2, -2, 0)), "FRONT minimum corner is in bounds")
|
||||
_expect(world.is_in_bounds(Vector3i(2, 2, 1)), "BACK maximum corner is in bounds")
|
||||
_expect(not world.is_in_bounds(Vector3i(0, 0, 2)), "unknown layer is out of bounds")
|
||||
_expect(not world.is_in_bounds(Vector3i(3, 0, 0)), "x outside bounds is rejected")
|
||||
_expect(world.get_zone(Vector3i(0, 1, 0)) == world.ZONE_ABOVE_GROUND, "positive y is above ground")
|
||||
_expect(world.get_zone(0) == world.ZONE_BOUNDARY, "zero y is boundary")
|
||||
_expect(world.get_zone(Vector2i(0, -1)) == world.ZONE_UNDERGROUND, "negative y is underground")
|
||||
|
||||
var footprint := world.cells_for_footprint(Vector3i(-1, -1, 1), Vector2i(2, 2))
|
||||
_expect(
|
||||
footprint == [
|
||||
Vector3i(-1, -1, 1), Vector3i(0, -1, 1),
|
||||
Vector3i(-1, 0, 1), Vector3i(0, 0, 1),
|
||||
],
|
||||
"rectangular footprint has stable row-major ordering"
|
||||
)
|
||||
_expect(world.cells_for_footprint(Vector3i.ZERO, Vector2i.ZERO).is_empty(), "empty footprint is rejected")
|
||||
var before := world.get_bounds()
|
||||
_expect(not world.reset(Rect2i(0, 0, 0, 2)), "invalid reset is rejected")
|
||||
_expect(world.get_bounds() == before, "failed reset preserves the current world")
|
||||
|
||||
|
||||
func _test_occupancy_validation_and_release() -> void:
|
||||
var world = WorldServiceType.new()
|
||||
world.reset(_bounds(-3, 3, -2, 2))
|
||||
var room_cells := world.cells_for_footprint(Vector3i(0, 0, 0), Vector2i(2, 1))
|
||||
_expect(world.occupy(&"room-b", room_cells, &"facility_small"), "valid entity occupies its footprint")
|
||||
_expect(world.get_occupant(Vector3i(0, 0, 0)) == &"room-b", "cell points back to occupant")
|
||||
_expect(world.get_occupant_kind(Vector3i(1, 0, 0)) == &"facility_small", "occupant kind is retained")
|
||||
_expect(world.get_entity_cells(&"room-b") == room_cells, "entity points to canonical cells")
|
||||
|
||||
var returned_cells := world.get_entity_cells(&"room-b")
|
||||
returned_cells.clear()
|
||||
_expect(world.get_entity_cells(&"room-b") == room_cells, "returned cell arrays cannot mutate world state")
|
||||
|
||||
var overlap := world.validate_occupancy([Vector3i(1, 0, 0)], &"room-a")
|
||||
_expect(not overlap[&"ok"] and overlap[&"errors"].has(world.ERROR_OCCUPIED), "overlapping occupation reports OCCUPIED")
|
||||
var revision_before_failure: int = world.revision
|
||||
_expect(not world.occupy(&"room-a", [Vector3i(1, 0, 0)], &"facility"), "overlapping occupy is atomic")
|
||||
_expect(world.revision == revision_before_failure, "failed occupy does not change revision")
|
||||
_expect(not world.occupy(&"room-b", [Vector3i(2, 0, 0)], &"facility"), "same entity cannot occupy twice")
|
||||
|
||||
var malformed := world.validate_occupancy([
|
||||
Vector3i(0, 1, 0),
|
||||
Vector3i(0, 1, 0),
|
||||
Vector3i(0, 1, 1),
|
||||
Vector3i(9, 1, 0),
|
||||
Vector2i.ZERO,
|
||||
])
|
||||
_expect(malformed[&"errors"].has(world.ERROR_DUPLICATE_CELL), "duplicate cell is reported")
|
||||
_expect(malformed[&"errors"].has(world.ERROR_LAYER_MISMATCH), "layer crossing is reported")
|
||||
_expect(malformed[&"errors"].has(world.ERROR_OUT_OF_BOUNDS), "out-of-bounds cell is reported")
|
||||
_expect(malformed[&"errors"].has(world.ERROR_INVALID_CELL_TYPE), "non-Vector3i cell is reported")
|
||||
|
||||
_expect(world.release(&"room-b"), "release succeeds for existing entity")
|
||||
_expect(world.get_occupant(Vector3i(0, 0, 0)).is_empty(), "release clears cell-side occupation")
|
||||
_expect(world.get_entity_cells(&"room-b").is_empty(), "release clears entity-side occupation")
|
||||
_expect(not world.release(&"room-b"), "double release is rejected")
|
||||
|
||||
|
||||
func _test_walkability_excavation_and_revisions() -> void:
|
||||
var world = WorldServiceType.new()
|
||||
world.reset(_bounds(-1, 1, -2, 2))
|
||||
var path_cell := Vector3i(0, 1, 0)
|
||||
var cave_front := Vector3i(0, -1, 0)
|
||||
var cave_back := Vector3i(0, -1, 1)
|
||||
_expect(not world.is_walkable(path_cell), "walkability defaults to false")
|
||||
_expect(world.set_walkable(path_cell, true), "walkability can be enabled")
|
||||
_expect(world.is_walkable(path_cell), "enabled walkability is readable")
|
||||
_expect(world.dirty_revision == world.revision, "dirty revision exposes the current world revision")
|
||||
var walk_revision: int = world.revision
|
||||
_expect(world.set_walkable(path_cell, true), "same walkability is a successful no-op")
|
||||
_expect(world.revision == walk_revision, "walkability no-op does not dirty world")
|
||||
|
||||
_expect(world.is_excavated(Vector3i(0, 0, 0)), "boundary is usable without excavation")
|
||||
_expect(not world.is_excavated(cave_front), "underground defaults to unexcavated")
|
||||
_expect(world.set_excavated(cave_front, true), "underground spatial cell can be excavated")
|
||||
_expect(world.is_excavated(cave_front) and world.is_excavated(cave_back), "excavation is shared by FRONT and BACK")
|
||||
_expect(world.excavation_revision == world.revision, "excavation tracks its dirty revision")
|
||||
_expect(world.get_dirty_cells().has(cave_front) and world.get_dirty_cells().has(cave_back), "both layers become dirty")
|
||||
var dirty := world.consume_dirty_cells()
|
||||
_expect(not dirty.is_empty() and world.get_dirty_cells().is_empty(), "dirty cells can be consumed deterministically")
|
||||
_expect(not world.set_walkable(Vector3i(99, 0, 0), true), "out-of-bounds walkability mutation is rejected")
|
||||
|
||||
|
||||
func _test_connected_support_validation() -> void:
|
||||
var world = WorldServiceType.new()
|
||||
world.reset(_bounds(-5, 5, 0, 2))
|
||||
_expect(world.set_support(Vector3i(0, 0, 0), 3), "explicit support capacity is stored")
|
||||
_expect(world.occupy(&"existing-room", [Vector3i(1, 0, 0)], &"facility_small"), "existing structural load is stored")
|
||||
var supported := world.validate_support([Vector3i(2, 0, 0), Vector3i(3, 0, 0)], 1)
|
||||
_expect(supported[&"ok"], "component capacity covers existing and proposed load")
|
||||
_expect(supported[&"required_weight"] == 3, "support report includes existing structural load")
|
||||
_expect(supported[&"available_capacity"] == 3, "support report exposes finite capacity")
|
||||
|
||||
var overloaded := world.validate_support([
|
||||
Vector3i(2, 0, 0), Vector3i(3, 0, 0), Vector3i(4, 0, 0),
|
||||
], 1)
|
||||
_expect(not overloaded[&"ok"] and overloaded[&"errors"].has(world.ERROR_UNSUPPORTED), "overloaded component is rejected")
|
||||
_expect(overloaded[&"unsupported_cells"].size() == 3, "all proposed cells in failed component are identified")
|
||||
|
||||
_expect(world.set_support(Vector3i(-2, 0, 1), 0, true), "infinite structural anchor is stored")
|
||||
var anchored := world.validate_support([Vector3i(-1, 0, 1), Vector3i(0, 0, 1)], 99)
|
||||
_expect(anchored[&"ok"] and anchored[&"available_capacity"] == -1, "connected anchor supports any finite weight")
|
||||
_expect(not world.validate_support([Vector3i(0, 0, 0)], 0)[&"ok"], "non-positive proposed weight is rejected")
|
||||
|
||||
|
||||
func _test_snapshot_round_trip_and_atomic_restore() -> void:
|
||||
var source = WorldServiceType.new()
|
||||
source.reset(_bounds(-3, 3, -3, 3))
|
||||
source.occupy(&"warehouse", [Vector3i(1, 0, 1), Vector3i(0, 0, 1)], &"facility_medium")
|
||||
source.set_walkable(Vector3i(-1, 0, 0), true)
|
||||
source.set_excavated(Vector3i(2, -2, 0), true)
|
||||
source.set_excavated(Vector3i(-2, 1, 0), false)
|
||||
source.set_support(Vector3i(0, 0, 0), 5, false)
|
||||
source.set_support(Vector3i(-1, 0, 1), 0, true)
|
||||
var snapshot := source.create_snapshot()
|
||||
|
||||
var restored = WorldServiceType.new()
|
||||
restored.reset(Rect2i(0, 0, 1, 1))
|
||||
_expect(restored.restore_snapshot(snapshot), "valid world snapshot restores")
|
||||
_expect(restored.create_snapshot() == snapshot, "world snapshot round trip is exact")
|
||||
_expect(DigestType.compute(restored.create_snapshot()) == DigestType.compute(snapshot), "round trip preserves deterministic digest")
|
||||
|
||||
var corrupt: Dictionary = snapshot.duplicate(true)
|
||||
corrupt[&"occupancies"].append({
|
||||
&"entity_id": &"overlap",
|
||||
&"kind": &"facility",
|
||||
&"cells": [Vector3i(0, 0, 1)],
|
||||
})
|
||||
var before_failed_restore := restored.create_snapshot()
|
||||
_expect(not restored.restore_snapshot(corrupt), "overlapping snapshot is rejected")
|
||||
_expect(restored.create_snapshot() == before_failed_restore, "failed restore is fully atomic")
|
||||
|
||||
|
||||
func _test_snapshot_order_is_deterministic() -> void:
|
||||
var first = WorldServiceType.new()
|
||||
var second = WorldServiceType.new()
|
||||
first.reset(_bounds(-2, 2, -1, 1))
|
||||
second.reset(_bounds(-2, 2, -1, 1))
|
||||
first.occupy(&"z-entity", [Vector3i(1, 0, 0)], &"decoration")
|
||||
first.occupy(&"a-entity", [Vector3i(-1, 0, 0)], &"decoration")
|
||||
second.occupy(&"a-entity", [Vector3i(-1, 0, 0)], &"decoration")
|
||||
second.occupy(&"z-entity", [Vector3i(1, 0, 0)], &"decoration")
|
||||
first.set_walkable(Vector3i(1, 1, 1), true)
|
||||
first.set_walkable(Vector3i(-1, -1, 0), true)
|
||||
second.set_walkable(Vector3i(-1, -1, 0), true)
|
||||
second.set_walkable(Vector3i(1, 1, 1), true)
|
||||
_expect(first.create_snapshot() == second.create_snapshot(), "snapshot ignores Dictionary insertion order")
|
||||
_expect(DigestType.compute(first.create_snapshot()) == DigestType.compute(second.create_snapshot()), "equivalent worlds have identical digest")
|
||||
|
||||
|
||||
func _bounds(min_x: int, max_x: int, min_y: int, max_y: int) -> Dictionary:
|
||||
return {
|
||||
&"min_x": min_x,
|
||||
&"max_x": max_x,
|
||||
&"min_y": min_y,
|
||||
&"max_y": max_y,
|
||||
}
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
return
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://nj6v4aby5clu
|
||||
Reference in New Issue
Block a user