1236 lines
46 KiB
GDScript
1236 lines
46 KiB
GDScript
extends SceneTree
|
|
|
|
const RUNTIME_SNAPSHOT_VERSION := 9
|
|
const INFRASTRUCTURE_SNAPSHOT_KEYS: Array[StringName] = [
|
|
&"excavation",
|
|
&"spatial_subslots",
|
|
&"route_projection",
|
|
&"facility_infrastructure",
|
|
]
|
|
const INFRASTRUCTURE_MEMBER_NAMES: Array[StringName] = [
|
|
&"excavation_service",
|
|
&"spatial_subslot_service",
|
|
&"route_subslot_path_adapter",
|
|
&"facility_infrastructure_projector",
|
|
]
|
|
|
|
const PathType = preload("res://scripts/world/path_service.gd")
|
|
|
|
|
|
class DenyingOperationsCoordinator:
|
|
extends OperationsCoordinator
|
|
|
|
func can_start_construction_or_investment() -> Dictionary:
|
|
return {
|
|
&"allowed": false,
|
|
&"reason": &"ARREARS_REMAIN",
|
|
&"arrears_total": 1,
|
|
&"consecutive_insolvency_count": 1,
|
|
}
|
|
|
|
|
|
class SnapshotCountingDomainEventStream:
|
|
extends DomainEventStream
|
|
|
|
var snapshot_calls := 0
|
|
|
|
func create_snapshot() -> Dictionary:
|
|
snapshot_calls += 1
|
|
return super.create_snapshot()
|
|
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S1: Runtime infrastructure integration")
|
|
var probe := _new_fixture(81_001)
|
|
var surface_ready := _test_runtime_surface(probe)
|
|
_free_fixture(probe)
|
|
if surface_ready:
|
|
_test_completion_capture_fast_path()
|
|
_test_rank_and_operations_gates()
|
|
_test_excavation_tick_pause_resume_and_completion()
|
|
_test_completed_proxy_projection()
|
|
_test_proxy_completion_raw_signal_commit_order()
|
|
_test_proxy_completion_projection_failure_is_atomic()
|
|
_test_completed_elevator_topology()
|
|
_test_demolition_projection_atomicity()
|
|
_test_current_snapshot_strictness()
|
|
_test_v5_proxy_migration_determinism()
|
|
_test_rank_expansion_preserves_route_union()
|
|
_finish()
|
|
|
|
|
|
func _finish() -> void:
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _new_fixture(seed: int) -> Dictionary:
|
|
var clock: Variant = load("res://autoload/game_clock.gd").new()
|
|
var state: Variant = load("res://autoload/game_state.gd").new()
|
|
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
|
|
clock.reset(1, 6, 0)
|
|
state.reset()
|
|
var initialized: Dictionary = runtime.initialize(clock, state, seed)
|
|
clock.set_paused(true)
|
|
_expect(bool(initialized.get(&"valid", false)), "fixture Runtime initializes")
|
|
return {&"clock": clock, &"state": state, &"runtime": runtime}
|
|
|
|
|
|
func _free_fixture(fixture: Dictionary) -> void:
|
|
var runtime: Variant = fixture.get(&"runtime")
|
|
var clock: Variant = fixture.get(&"clock")
|
|
var state: Variant = fixture.get(&"state")
|
|
if runtime != null:
|
|
runtime.free()
|
|
if clock != null:
|
|
clock.free()
|
|
if state != null:
|
|
state.free()
|
|
|
|
|
|
func _test_runtime_surface(fixture: Dictionary) -> bool:
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var ready := true
|
|
for member_name: StringName in INFRASTRUCTURE_MEMBER_NAMES:
|
|
var present := _object_has_property(runtime, member_name)
|
|
_expect(present, "Runtime publishes %s" % member_name)
|
|
ready = ready and present
|
|
if present:
|
|
var service: Variant = runtime.get(member_name)
|
|
_expect(service != null, "%s is initialized" % member_name)
|
|
ready = ready and service != null
|
|
var snapshot: Dictionary = runtime.create_snapshot()
|
|
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
|
|
var current_version := int(runtime_data.get(&"runtime_schema_version", -1)) == RUNTIME_SNAPSHOT_VERSION
|
|
_expect(current_version, "Runtime emits schema v9 with infrastructure state")
|
|
ready = ready and current_version
|
|
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
|
|
var valid_dictionary := runtime_data.get(key, null) is Dictionary
|
|
_expect(valid_dictionary, "current Runtime stores strict %s state" % key)
|
|
ready = ready and valid_dictionary
|
|
if not ready:
|
|
print(" PENDING: Runtime v9 infrastructure surface is not connected yet; behavioral cases skipped.")
|
|
return ready
|
|
|
|
|
|
func _test_completion_capture_fast_path() -> void:
|
|
var fixture := _new_fixture(81_005)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var clock: Variant = fixture[&"clock"]
|
|
var counting_events := SnapshotCountingDomainEventStream.new()
|
|
_expect(
|
|
counting_events.restore_snapshot(runtime.domain_events.create_snapshot()),
|
|
"fast-path probe preserves the authoritative DomainEvent stream"
|
|
)
|
|
runtime.domain_events = counting_events
|
|
counting_events.snapshot_calls = 0
|
|
clock.advance_minutes(1)
|
|
_expect(
|
|
counting_events.snapshot_calls == 1,
|
|
"idle Construction tick performs only the normal publish snapshot capture"
|
|
)
|
|
_expect(
|
|
runtime.construction_service.current_minute == clock.absolute_minute
|
|
and runtime.excavation_service.current_minute == clock.absolute_minute,
|
|
"idle fast path keeps Construction and Excavation on the exact Runtime minute"
|
|
)
|
|
|
|
var started = _submit(
|
|
runtime,
|
|
&"build_facility",
|
|
{
|
|
&"definition_id": &"guest_corridor",
|
|
&"origin": Vector3i(0, 1, PathType.FRONT),
|
|
&"rotation": 0,
|
|
},
|
|
&"infra.fast_path.progress"
|
|
)
|
|
_expect(started.is_success(), "fast-path probe starts a non-completing Construction job")
|
|
if not started.is_success():
|
|
_free_fixture(fixture)
|
|
return
|
|
var job_id := StringName(started.details.get(&"job_id", &""))
|
|
var raw_job_changed_count := [0]
|
|
runtime.construction_service.job_changed.connect(
|
|
func(changed_job: ConstructionJob) -> void:
|
|
if changed_job != null and changed_job.job_id == job_id:
|
|
raw_job_changed_count[0] += 1
|
|
)
|
|
counting_events.snapshot_calls = 0
|
|
clock.advance_minutes(1)
|
|
var job: ConstructionJob = runtime.construction_service.get_job(job_id)
|
|
_expect(
|
|
job != null and job.state == ConstructionJob.State.BUILDING
|
|
and job.progress > 0 and job.progress < ConstructionJob.MAX_PROGRESS,
|
|
"ordinary Construction progress remains on the non-completion fast path"
|
|
)
|
|
_expect(
|
|
counting_events.snapshot_calls == 1,
|
|
"non-completing Construction progress avoids the full completion token"
|
|
)
|
|
_expect(
|
|
raw_job_changed_count[0] == 1,
|
|
"non-completion fast path preserves the raw job_changed notification"
|
|
)
|
|
_expect(
|
|
runtime.construction_service.current_minute == clock.absolute_minute
|
|
and runtime.excavation_service.current_minute == clock.absolute_minute,
|
|
"progress fast path keeps both minute-driven infrastructure services synchronized"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_rank_and_operations_gates() -> void:
|
|
var fixture := _new_fixture(81_010)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var target := Vector3i(0, -1, PathType.FRONT)
|
|
var before_rank_gate := _infrastructure_state(fixture)
|
|
var rank_rejected = _submit(
|
|
runtime, &"start_excavation", {&"cell": target}, &"infra.rank.reject"
|
|
)
|
|
_expect(not rank_rejected.is_success(), "Rank 1 cannot start excavation")
|
|
_expect(
|
|
_infrastructure_state(fixture) == before_rank_gate,
|
|
"Rank rejection changes no infrastructure or resource state"
|
|
)
|
|
_expect(_promote_rank_two(fixture, &"infra.rank"), "fixture reaches Rank 2 through public commands")
|
|
_free_fixture(fixture)
|
|
|
|
fixture = _new_fixture(81_011)
|
|
runtime = fixture[&"runtime"]
|
|
_expect(_promote_rank_two(fixture, &"infra.operations.rank"), "Operations-gate fixture reaches Rank 2")
|
|
var enabled: Dictionary = runtime.enable_production_operations()
|
|
_expect(bool(enabled.get(&"ok", false)), "Operations enable before gate probe")
|
|
# A deterministic coordinator test double exercises the public gate result
|
|
# without advancing a full payroll day or mutating Runtime private helpers.
|
|
runtime.operations_coordinator = DenyingOperationsCoordinator.new()
|
|
var before_operations_gate := _infrastructure_state(fixture)
|
|
var gate_rejected = _submit(
|
|
runtime, &"start_excavation", {&"cell": target}, &"infra.operations.reject"
|
|
)
|
|
_expect(
|
|
not gate_rejected.is_success()
|
|
and gate_rejected.details.get(&"reason", &"") == &"ARREARS_REMAIN",
|
|
"excavation uses the Operations construction/investment gate"
|
|
)
|
|
_expect(
|
|
_infrastructure_state(fixture) == before_operations_gate,
|
|
"Operations gate rejection is infrastructure-atomic"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_excavation_tick_pause_resume_and_completion() -> void:
|
|
var fixture := _new_fixture(81_020)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var clock: Variant = fixture[&"clock"]
|
|
_expect(_promote_rank_two(fixture, &"infra.excavation.rank"), "excavation fixture reaches Rank 2")
|
|
var target := Vector3i(0, -1, PathType.FRONT)
|
|
var started = _submit(
|
|
runtime, &"start_excavation", {&"cell": target}, &"infra.excavation.start"
|
|
)
|
|
_expect(started.is_success(), "start_excavation accepts a cell payload")
|
|
if not started.is_success():
|
|
_free_fixture(fixture)
|
|
return
|
|
var excavation_job_id := StringName(started.details.get(&"job_id", &""))
|
|
var excavation_events_before := _domain_event_count(runtime, &"EXCAVATION_COMPLETED")
|
|
var build = _submit(
|
|
runtime,
|
|
&"build_facility",
|
|
{
|
|
&"definition_id": &"guest_corridor",
|
|
&"origin": Vector3i(-4, 1, PathType.FRONT),
|
|
&"rotation": 0,
|
|
},
|
|
&"infra.excavation.tick.build"
|
|
)
|
|
_expect(build.is_success(), "construction comparison job starts in the same Runtime minute")
|
|
var construction_job_id := StringName(build.details.get(&"job_id", &""))
|
|
clock.advance_minutes(5)
|
|
var excavation_job: Dictionary = runtime.excavation_service.get_job(excavation_job_id)
|
|
var construction_job := _construction_job_snapshot(runtime, construction_job_id)
|
|
_expect(
|
|
int(excavation_job.get(&"progress", 0)) > 0
|
|
and int(construction_job.get(&"progress", 0)) > 0,
|
|
"excavation and construction both progress through Runtime ticks"
|
|
)
|
|
_expect(
|
|
int(excavation_job.get(&"last_progress_at", -1)) == clock.absolute_minute
|
|
and int(construction_job.get(&"last_progress_at", -2)) == clock.absolute_minute
|
|
and runtime.excavation_service.current_minute == runtime.construction_service.current_minute,
|
|
"excavation advances in the exact Construction phase minute"
|
|
)
|
|
|
|
var pause = _submit(
|
|
runtime,
|
|
&"pause_excavation",
|
|
{&"job_id": excavation_job_id},
|
|
&"infra.excavation.pause"
|
|
)
|
|
var paused_progress := int(runtime.excavation_service.get_job(excavation_job_id)[&"progress"])
|
|
_expect(pause.is_success(), "pause_excavation accepts a job_id payload")
|
|
clock.advance_minutes(4)
|
|
_expect(
|
|
int(runtime.excavation_service.get_job(excavation_job_id)[&"progress"]) == paused_progress,
|
|
"paused Runtime ticks retain excavation progress exactly"
|
|
)
|
|
var paused_snapshot: Dictionary = runtime.create_snapshot()
|
|
_expect(
|
|
runtime.restore_snapshot(paused_snapshot)
|
|
and runtime.create_snapshot() == paused_snapshot,
|
|
"paused excavation survives an exact current Runtime save/restore"
|
|
)
|
|
_expect(
|
|
runtime.excavation_service.get_job(excavation_job_id).get(&"state", &"")
|
|
== runtime.excavation_service.STATE_PAUSED
|
|
and int(runtime.excavation_service.get_job(excavation_job_id).get(&"progress", -1))
|
|
== paused_progress,
|
|
"restored excavation retains its paused state and accumulated work"
|
|
)
|
|
var resume = _submit(
|
|
runtime,
|
|
&"resume_excavation",
|
|
{&"job_id": excavation_job_id},
|
|
&"infra.excavation.resume"
|
|
)
|
|
_expect(resume.is_success(), "resume_excavation accepts the same job_id")
|
|
clock.advance_minutes(24)
|
|
excavation_job = runtime.excavation_service.get_job(excavation_job_id)
|
|
_expect(
|
|
excavation_job.get(&"state", &"") == runtime.excavation_service.STATE_ACTIVE
|
|
and int(excavation_job.get(&"work_units_done", -1))
|
|
== 29 * runtime.excavation_service.NORMAL_WORK_RATE
|
|
and int(excavation_job.get(&"progress", -1)) < runtime.excavation_service.MAX_PROGRESS
|
|
and not runtime.world_service.is_excavated(target)
|
|
and _domain_event_count(runtime, &"EXCAVATION_COMPLETED") == excavation_events_before,
|
|
"hardness-0 excavation remains incomplete at the exact 29-minute boundary"
|
|
)
|
|
clock.advance_minutes(1)
|
|
excavation_job = runtime.excavation_service.get_job(excavation_job_id)
|
|
_expect(
|
|
excavation_job.get(&"state", &"") == runtime.excavation_service.STATE_COMPLETED
|
|
and runtime.world_service.is_excavated(target)
|
|
and runtime.world_service.is_excavated(Vector3i(target.x, target.y, PathType.BACK)),
|
|
"the 30th minute completes FRONT and BACK excavation together"
|
|
)
|
|
_expect(
|
|
_domain_event_count(runtime, &"EXCAVATION_COMPLETED") == excavation_events_before + 1
|
|
and _saw_domain_event(runtime, &"EXCAVATION_COMPLETED", excavation_job_id),
|
|
"the 30-minute boundary publishes exactly one excavation completion event"
|
|
)
|
|
clock.advance_minutes(3)
|
|
_expect(
|
|
_domain_event_count(runtime, &"EXCAVATION_COMPLETED") == excavation_events_before + 1,
|
|
"later Runtime ticks cannot duplicate excavation completion"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_completed_proxy_projection() -> void:
|
|
var fixture := _new_fixture(81_030)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
_expect(_promote_rank_two(fixture, &"infra.proxy.rank"), "proxy fixture reaches Rank 2")
|
|
var front_a := Vector3i(0, -1, PathType.FRONT)
|
|
var front_b := Vector3i(1, -1, PathType.FRONT)
|
|
_expect(not _complete_excavation(fixture, front_a, &"infra.proxy.dig.a").is_empty(), "first proxy cell is excavated")
|
|
_expect(not _complete_excavation(fixture, front_b, &"infra.proxy.dig.b").is_empty(), "adjacent proxy cell is excavated")
|
|
runtime.world_service.set_support(front_a, 10, true)
|
|
runtime.world_service.set_support(front_b, 10, true)
|
|
runtime.world_service.set_support(Vector3i(0, -1, PathType.BACK), 10, true)
|
|
runtime.world_service.set_support(Vector3i(1, -1, PathType.BACK), 10, true)
|
|
|
|
var guest := _complete_proxy(
|
|
fixture, &"guest_corridor", front_a, &"infra.proxy.guest"
|
|
)
|
|
var adjacent_guest := _complete_proxy(
|
|
fixture, &"guest_corridor", front_b, &"infra.proxy.guest.adjacent"
|
|
)
|
|
_expect(adjacent_guest[&"ok"], "same-floor guest corridor completes beside the proxy")
|
|
var service := _complete_proxy(
|
|
fixture,
|
|
&"service_corridor",
|
|
Vector3i(1, -1, PathType.BACK),
|
|
&"infra.proxy.service"
|
|
)
|
|
var door := _complete_proxy(
|
|
fixture,
|
|
&"staff_door",
|
|
Vector3i(0, -1, PathType.BACK),
|
|
&"infra.proxy.door"
|
|
)
|
|
_expect(guest[&"ok"] and service[&"ok"] and door[&"ok"], "all three supported proxy kinds complete")
|
|
if not guest[&"ok"] or not service[&"ok"] or not door[&"ok"]:
|
|
_free_fixture(fixture)
|
|
return
|
|
var guest_id: StringName = guest[&"instance_id"]
|
|
var service_id: StringName = service[&"instance_id"]
|
|
var door_id: StringName = door[&"instance_id"]
|
|
_expect(
|
|
runtime.facility_infrastructure_projector.has_projection(guest_id)
|
|
and runtime.facility_infrastructure_projector.has_projection(service_id)
|
|
and runtime.facility_infrastructure_projector.has_projection(door_id),
|
|
"completed proxy facilities project immediately"
|
|
)
|
|
_expect(
|
|
not runtime.facility_service.has_instance(guest_id)
|
|
and not runtime.facility_service.has_instance(service_id)
|
|
and not runtime.facility_service.has_instance(door_id),
|
|
"schema proxy facilities never enter operational FacilityService"
|
|
)
|
|
var guest_projection: Dictionary = runtime.facility_infrastructure_projector.get_projection(guest_id)
|
|
var service_projection: Dictionary = runtime.facility_infrastructure_projector.get_projection(service_id)
|
|
var door_projection: Dictionary = runtime.facility_infrastructure_projector.get_projection(door_id)
|
|
_expect(
|
|
guest_projection[&"route_entity_id"] == StringName("infra.route:%s" % guest_id)
|
|
and service_projection[&"route_entity_id"] == StringName("infra.route:%s" % service_id)
|
|
and door_projection[&"connector_id"] == StringName("infra.connector:%s" % door_id),
|
|
"Runtime projection retains stable managed IDs"
|
|
)
|
|
var entrance := Vector3i(0, 0, PathType.FRONT)
|
|
var door_back := Vector3i(0, -1, PathType.BACK)
|
|
var service_back := Vector3i(1, -1, PathType.BACK)
|
|
_expect(
|
|
runtime.path_service.find_path(front_b, front_a) == [front_b, front_a],
|
|
"guest corridors form a real same-floor FRONT Path edge"
|
|
)
|
|
_expect(
|
|
runtime.path_service.find_path(entrance, front_a).is_empty(),
|
|
"corridor projection does not create an implicit staircase from ground level"
|
|
)
|
|
_expect(
|
|
runtime.path_service.find_path(
|
|
front_a,
|
|
service_back,
|
|
[PathType.FRONT, PathType.BACK],
|
|
[&"staff"]
|
|
) == [front_a, door_back, service_back],
|
|
"staff door and service corridor form a real staff-only Path"
|
|
)
|
|
_expect(
|
|
runtime.path_service.find_path(
|
|
front_a,
|
|
service_back,
|
|
[PathType.FRONT, PathType.BACK],
|
|
[]
|
|
).is_empty(),
|
|
"guest access cannot traverse the staff-only projection"
|
|
)
|
|
_expect(
|
|
runtime.world_service.get_occupant(front_a) == guest_id
|
|
and runtime.world_service.get_occupant(service_back) == service_id
|
|
and runtime.world_service.get_occupant(door_back) == door_id,
|
|
"proxy ROUTEs coexist with authoritative primary World occupancy"
|
|
)
|
|
_expect(
|
|
runtime.spatial_subslot_service.validate_invariants()[&"ok"]
|
|
and runtime.route_subslot_path_adapter.validate_invariants()[&"ok"]
|
|
and runtime.facility_infrastructure_projector.validate_invariants()[&"ok"],
|
|
"completed proxy projection satisfies every infrastructure invariant"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_proxy_completion_raw_signal_commit_order() -> void:
|
|
var fixture := _new_fixture(81_034)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var clock: Variant = fixture[&"clock"]
|
|
var started = _submit(
|
|
runtime,
|
|
&"build_facility",
|
|
{
|
|
&"definition_id": &"guest_corridor",
|
|
&"origin": Vector3i(0, 1, PathType.FRONT),
|
|
&"rotation": 0,
|
|
},
|
|
&"infra.proxy.signals.start"
|
|
)
|
|
_expect(started.is_success(), "signal-order fixture starts a proxy job")
|
|
if not started.is_success():
|
|
_free_fixture(fixture)
|
|
return
|
|
var job_id := StringName(started.details.get(&"job_id", &""))
|
|
var job: ConstructionJob = runtime.construction_service.get_job(job_id)
|
|
if job == null:
|
|
_expect(false, "signal-order fixture exposes its Construction job")
|
|
_free_fixture(fixture)
|
|
return
|
|
clock.advance_minutes(maxi(job.build_minutes - 1, 0))
|
|
var raw_signal_order: Array[StringName] = []
|
|
runtime.construction_service.job_changed.connect(
|
|
func(changed_job: ConstructionJob) -> void:
|
|
if changed_job != null and changed_job.job_id == job_id:
|
|
raw_signal_order.append(&"job_changed")
|
|
)
|
|
runtime.construction_service.facility_completed.connect(
|
|
func(instance: Dictionary) -> void:
|
|
if StringName(instance.get(&"source_job_id", &"")) == job_id:
|
|
raw_signal_order.append(&"facility_completed")
|
|
)
|
|
var completed_events_before := _domain_event_count(runtime, &"FACILITY_COMPLETED")
|
|
var projected_events_before := _domain_event_count(runtime, &"FACILITY_INFRASTRUCTURE_PROJECTED")
|
|
clock.advance_minutes(1)
|
|
job = runtime.construction_service.get_job(job_id)
|
|
_expect(
|
|
job != null
|
|
and job.state == ConstructionJob.State.COMPLETED
|
|
and runtime.facility_infrastructure_projector.has_projection(job.facility_instance_id),
|
|
"successful proxy completion commits Construction and projection together"
|
|
)
|
|
_expect(
|
|
raw_signal_order == [&"job_changed", &"facility_completed"],
|
|
"successful completion publishes raw job_changed then facility_completed exactly once each"
|
|
)
|
|
_expect(
|
|
_domain_event_count(runtime, &"FACILITY_COMPLETED") == completed_events_before + 1
|
|
and _domain_event_count(runtime, &"FACILITY_INFRASTRUCTURE_PROJECTED")
|
|
== projected_events_before + 1,
|
|
"successful proxy completion publishes each committed DomainEvent exactly once"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_proxy_completion_projection_failure_is_atomic() -> void:
|
|
var fixture := _new_fixture(81_035)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var clock: Variant = fixture[&"clock"]
|
|
var started = _submit(
|
|
runtime,
|
|
&"build_facility",
|
|
{
|
|
&"definition_id": &"guest_corridor",
|
|
&"origin": Vector3i(0, 1, PathType.FRONT),
|
|
&"rotation": 0,
|
|
},
|
|
&"infra.proxy.failure.start"
|
|
)
|
|
_expect(started.is_success(), "projection-failure fixture starts a proxy job")
|
|
if not started.is_success():
|
|
_free_fixture(fixture)
|
|
return
|
|
var job_id := StringName(started.details.get(&"job_id", &""))
|
|
var job: Variant = runtime.construction_service.get_job(job_id)
|
|
if job == null:
|
|
_expect(false, "projection-failure fixture exposes its Construction job")
|
|
_free_fixture(fixture)
|
|
return
|
|
clock.advance_minutes(maxi(int(job.build_minutes) - 1, 0))
|
|
job = runtime.construction_service.get_job(job_id)
|
|
_expect(
|
|
job != null
|
|
and job.state != ConstructionJob.State.COMPLETED
|
|
and job.progress > 0
|
|
and job.progress < ConstructionJob.MAX_PROGRESS,
|
|
"proxy job reaches the exact minute before completion"
|
|
)
|
|
var external_route_id := &"external_route_before_proxy_completion"
|
|
var made_stale = runtime.spatial_subslot_service.place(
|
|
external_route_id,
|
|
runtime.spatial_subslot_service.CATEGORY_ROUTE,
|
|
[Vector3i(5, 2, PathType.FRONT)]
|
|
)
|
|
_expect(
|
|
made_stale.is_success()
|
|
and runtime.route_subslot_path_adapter.is_projection_stale(),
|
|
"public Spatial mutation makes the completion projector dependency stale"
|
|
)
|
|
var before_failure := _completion_boundary_state(fixture)
|
|
var completed_events_before := _domain_event_count(runtime, &"FACILITY_COMPLETED")
|
|
var projected_events_before := _domain_event_count(runtime, &"FACILITY_INFRASTRUCTURE_PROJECTED")
|
|
var raw_completion_signals: Array[StringName] = []
|
|
runtime.construction_service.job_changed.connect(
|
|
func(changed_job: ConstructionJob) -> void:
|
|
if changed_job != null and changed_job.job_id == job_id:
|
|
raw_completion_signals.append(&"job_changed")
|
|
)
|
|
runtime.construction_service.facility_completed.connect(
|
|
func(instance: Dictionary) -> void:
|
|
if StringName(instance.get(&"source_job_id", &"")) == job_id:
|
|
raw_completion_signals.append(&"facility_completed")
|
|
)
|
|
# A newly raised runtime failure must stop an initially-running batch. The
|
|
# test otherwise uses paused manual stepping so no wall-clock frame can race it.
|
|
clock.set_paused(false)
|
|
clock.advance_minutes(1)
|
|
var after_job: Variant = runtime.construction_service.get_job(job_id)
|
|
_expect(
|
|
_completion_boundary_state(fixture) == before_failure,
|
|
"failed completion restores Construction, World, Path, infrastructure, and resources exactly"
|
|
)
|
|
_expect(
|
|
after_job != null
|
|
and after_job.state != ConstructionJob.State.COMPLETED
|
|
and after_job.progress < ConstructionJob.MAX_PROGRESS
|
|
and runtime.construction_service.get_facilities().is_empty()
|
|
and runtime.facility_infrastructure_projector.get_projection_ids().is_empty(),
|
|
"projection rejection leaves no completed facility or one-sided projection"
|
|
)
|
|
_expect(
|
|
runtime.spatial_subslot_service.get_entity_ids(
|
|
runtime.spatial_subslot_service.CATEGORY_ROUTE
|
|
) == [external_route_id]
|
|
and runtime.route_subslot_path_adapter.get_projected_route_cells().is_empty(),
|
|
"projection rejection retains only the pre-existing stale mutation and no managed ROUTE"
|
|
)
|
|
_expect(
|
|
_domain_event_count(runtime, &"FACILITY_COMPLETED") == completed_events_before
|
|
and _domain_event_count(runtime, &"FACILITY_INFRASTRUCTURE_PROJECTED") == projected_events_before,
|
|
"projection rejection publishes no ghost completion or projection DomainEvent"
|
|
)
|
|
_expect(
|
|
raw_completion_signals.is_empty(),
|
|
"projection rejection exposes no raw job_changed or facility_completed signal"
|
|
)
|
|
var diagnostic := _runtime_failure_diagnostic(runtime)
|
|
_expect(
|
|
clock.paused
|
|
and bool(diagnostic.get(&"latched", false))
|
|
and not String(diagnostic.get(&"reason", "")).is_empty(),
|
|
"projection rejection pauses Runtime with a public reason-bearing failure latch"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_completed_elevator_topology() -> void:
|
|
var fixture := _new_fixture(81_040)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
_expect(_promote_rank_two(fixture, &"infra.vertical.rank"), "vertical fixture reaches Rank 2")
|
|
var cash_before: int = runtime.economy_service.get_balance(&"PLAYER_CASH")
|
|
for request: Array in [[&"guest_elevator", Vector3i(-6, 0, PathType.FRONT)], [&"service_elevator", Vector3i(5, 0, PathType.BACK)]]:
|
|
var result = _submit(runtime, &"build_facility", {&"definition_id": request[0], &"origin": request[1], &"rotation": 0}, StringName("infra.vertical." + String(request[0])))
|
|
_expect(result.is_success(), "%s is accepted through paid build-command preflight" % request[0])
|
|
_expect(runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 3700, "accepted lift construction spends actual definition costs")
|
|
_expect(runtime.facility_infrastructure_projector.get_projection_ids().is_empty(), "unfinished lift construction does not create usable shaft topology")
|
|
fixture[&"clock"].advance_minutes(360)
|
|
_expect(runtime.facility_infrastructure_projector.get_projection_ids().size() == 2 and runtime.elevators.get_report()[&"shafts"].size() == 2, "completed guest and service modules project separate cabins")
|
|
for shaft: Dictionary in runtime.elevators.get_report()[&"shafts"]:
|
|
var result = _submit(runtime, &"set_elevator_operation", {&"shaft_id": shaft[&"shaft_id"], &"operator_open": false}, StringName("infra.vertical.stop." + String(shaft[&"shaft_id"])))
|
|
_expect(result.is_success(), "completed lift can be stopped through Runtime command")
|
|
_expect(runtime.path_service.find_path(shaft[&"stops"][0], shaft[&"stops"][1]).is_empty(), "stopped lift shaft never permits an ordinary vertical walk bypass")
|
|
_expect(runtime.facility_infrastructure_projector.validate_invariants()[&"ok"], "completed elevator ROUTE and owned shaft restrictions crosslink exactly")
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_demolition_projection_atomicity() -> void:
|
|
var fixture := _new_fixture(81_050)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
_expect(_promote_rank_two(fixture, &"infra.demolish.rank"), "demolition fixture reaches Rank 2")
|
|
var built := _complete_proxy(
|
|
fixture,
|
|
&"guest_corridor",
|
|
Vector3i(0, 1, PathType.FRONT),
|
|
&"infra.demolish.proxy"
|
|
)
|
|
_expect(built[&"ok"], "demolition target proxy completes")
|
|
if not built[&"ok"]:
|
|
_free_fixture(fixture)
|
|
return
|
|
var instance_id: StringName = built[&"instance_id"]
|
|
var projection: Dictionary = runtime.facility_infrastructure_projector.get_projection(instance_id)
|
|
_expect(
|
|
projection.get(&"route_entity_id", null) is StringName,
|
|
"demolition fixture exposes the completed proxy projection"
|
|
)
|
|
if not projection.get(&"route_entity_id", null) is StringName:
|
|
_free_fixture(fixture)
|
|
return
|
|
var managed_route: StringName = projection[&"route_entity_id"]
|
|
_expect(
|
|
runtime.spatial_subslot_service.place(
|
|
&"external_route",
|
|
runtime.spatial_subslot_service.CATEGORY_ROUTE,
|
|
[Vector3i(5, 2, PathType.FRONT)]
|
|
).is_success(),
|
|
"public Spatial mutation creates a deterministic stale-dependency failure"
|
|
)
|
|
var before_failure := _infrastructure_state(fixture)
|
|
var refunds_before := _ledger_category_count(runtime, &"CONSTRUCTION_REFUND")
|
|
var raw_demolished_ids: Array[StringName] = []
|
|
runtime.construction_service.facility_demolished.connect(
|
|
func(demolished_id: StringName, _refund: Dictionary) -> void:
|
|
if demolished_id == instance_id:
|
|
raw_demolished_ids.append(demolished_id)
|
|
)
|
|
var rejected = _submit(
|
|
runtime,
|
|
&"demolish_facility",
|
|
{&"instance_id": instance_id},
|
|
&"infra.demolish.stale"
|
|
)
|
|
_expect(
|
|
not rejected.is_success()
|
|
and not String(rejected.details.get(&"reason", "")).is_empty(),
|
|
"demolition rejects stale projection with a stable public reason"
|
|
)
|
|
_expect(
|
|
_infrastructure_state(fixture) == before_failure,
|
|
"failed unprojection preserves projection, construction, World, and resources exactly"
|
|
)
|
|
_expect(
|
|
raw_demolished_ids.is_empty()
|
|
and _ledger_category_count(runtime, &"CONSTRUCTION_REFUND") == refunds_before
|
|
and runtime.spatial_subslot_service.get_entity_cells(&"external_route")
|
|
== [Vector3i(5, 2, PathType.FRONT)],
|
|
"failed demolition emits no raw signal or refund and retains the external ROUTE"
|
|
)
|
|
_expect(
|
|
runtime.route_subslot_path_adapter.synchronize_projection(&"infra.demolish.sync").is_success(),
|
|
"fixture repairs the stale route through the public adapter"
|
|
)
|
|
var removed = _submit(
|
|
runtime,
|
|
&"demolish_facility",
|
|
{&"instance_id": instance_id},
|
|
&"infra.demolish.commit"
|
|
)
|
|
_expect(removed.is_success(), "proxy demolition commits after dependencies are fresh")
|
|
_expect(
|
|
not runtime.facility_infrastructure_projector.has_projection(instance_id)
|
|
and not runtime.spatial_subslot_service.has_entity(managed_route),
|
|
"demolition removes projector and Spatial projection together"
|
|
)
|
|
_expect(
|
|
runtime.spatial_subslot_service.get_entity_cells(&"external_route")
|
|
== [Vector3i(5, 2, PathType.FRONT)]
|
|
and runtime.path_service.has_cell(Vector3i(5, 2, PathType.FRONT)),
|
|
"committed demolition preserves the unrelated external ROUTE projection"
|
|
)
|
|
_expect(
|
|
runtime.construction_service.get_facility(instance_id).is_empty()
|
|
and runtime.world_service.get_entity_cells(instance_id).is_empty(),
|
|
"demolition removes Construction and primary World state together"
|
|
)
|
|
_expect(
|
|
_saw_domain_event(runtime, &"FACILITY_DEMOLISHED", instance_id),
|
|
"committed proxy demolition publishes its domain event"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_current_snapshot_strictness() -> void:
|
|
var fixture := _new_fixture(81_060)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
_expect(_promote_rank_two(fixture, &"infra.snapshot.rank"), "snapshot fixture reaches Rank 2")
|
|
var target := Vector3i(0, -1, PathType.FRONT)
|
|
var excavation_job_id := _complete_excavation(fixture, target, &"infra.snapshot.dig")
|
|
_expect(not excavation_job_id.is_empty(), "snapshot fixture stores completed excavation evidence")
|
|
if excavation_job_id.is_empty():
|
|
_free_fixture(fixture)
|
|
return
|
|
var built := _complete_proxy(
|
|
fixture,
|
|
&"guest_corridor",
|
|
Vector3i(0, 1, PathType.FRONT),
|
|
&"infra.snapshot.proxy"
|
|
)
|
|
_expect(built[&"ok"], "snapshot fixture stores one completed projection")
|
|
var snapshot: Dictionary = runtime.create_snapshot()
|
|
var expected_digest := SimulationDigest.compute(snapshot)
|
|
_expect(runtime.restore_snapshot(snapshot), "valid Runtime v9 snapshot restores")
|
|
_expect(
|
|
runtime.create_snapshot() == snapshot and runtime.latest_digest == expected_digest,
|
|
"Runtime v9 round trip is exact"
|
|
)
|
|
|
|
_assert_runtime_restore_rejected(
|
|
runtime,
|
|
snapshot,
|
|
func(candidate: Dictionary) -> void:
|
|
candidate[&"systems"][&"runtime"][&"unknown_infrastructure"] = {},
|
|
"unknown v9 Runtime field"
|
|
)
|
|
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
|
|
_assert_runtime_restore_rejected(
|
|
runtime,
|
|
snapshot,
|
|
func(candidate: Dictionary) -> void:
|
|
candidate[&"systems"][&"runtime"].erase(key),
|
|
"missing %s field" % key
|
|
)
|
|
_assert_runtime_restore_rejected(
|
|
runtime,
|
|
snapshot,
|
|
func(candidate: Dictionary) -> void:
|
|
candidate[&"systems"][&"runtime"][key] = [],
|
|
"non-Dictionary %s field" % key
|
|
)
|
|
_assert_runtime_restore_rejected(
|
|
runtime,
|
|
snapshot,
|
|
func(candidate: Dictionary) -> void:
|
|
candidate[&"systems"][&"runtime"][key][&"unknown"] = true,
|
|
"unknown nested %s field" % key
|
|
)
|
|
_assert_runtime_restore_rejected(
|
|
runtime,
|
|
snapshot,
|
|
func(candidate: Dictionary) -> void:
|
|
candidate[&"systems"][&"runtime"][&"route_projection"][&"projected_route_cells"].clear(),
|
|
"Spatial-to-adapter route crosslink tamper"
|
|
)
|
|
_assert_runtime_restore_rejected(
|
|
runtime,
|
|
snapshot,
|
|
func(candidate: Dictionary) -> void:
|
|
candidate[&"systems"][&"runtime"][&"facility_infrastructure"][&"projections"].clear(),
|
|
"projector-to-managed-route crosslink tamper"
|
|
)
|
|
_assert_runtime_restore_rejected(
|
|
runtime,
|
|
snapshot,
|
|
func(candidate: Dictionary) -> void:
|
|
candidate[&"systems"][&"runtime"][&"excavation"][&"jobs"][0][&"position"] = [2, -2],
|
|
"excavation-to-World completion crosslink tamper"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _test_v5_proxy_migration_determinism() -> void:
|
|
var source := _new_fixture(81_070)
|
|
var runtime: Variant = source[&"runtime"]
|
|
var built := _complete_proxy(
|
|
source,
|
|
&"guest_corridor",
|
|
Vector3i(0, 1, PathType.FRONT),
|
|
&"infra.legacy.proxy"
|
|
)
|
|
_expect(built[&"ok"], "legacy fixture creates a completed proxy")
|
|
if not built[&"ok"]:
|
|
_free_fixture(source)
|
|
return
|
|
var instance_id: StringName = built[&"instance_id"]
|
|
var construction_instance: Dictionary = runtime.construction_service.get_facility(instance_id)
|
|
var legacy_registered := _register_legacy_operational_proxy(runtime, construction_instance)
|
|
_expect(
|
|
legacy_registered,
|
|
"legacy fixture reproduces the v5 operational-proxy shape"
|
|
)
|
|
if not legacy_registered:
|
|
_free_fixture(source)
|
|
return
|
|
var baseline := _new_fixture(81_070)
|
|
var legacy: Dictionary = runtime.create_snapshot()
|
|
var legacy_runtime: Dictionary = legacy[&"systems"][&"runtime"]
|
|
var stripped_v6_events := _strip_v6_infrastructure_domain_history(legacy_runtime)
|
|
legacy_runtime[&"runtime_schema_version"] = 5
|
|
legacy_runtime.erase(&"kitchen_production")
|
|
legacy_runtime.erase(&"elevators")
|
|
legacy_runtime.erase(&"legacy_stairs")
|
|
legacy_runtime.erase(&"staff_commute")
|
|
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
|
|
legacy_runtime.erase(key)
|
|
legacy_runtime[&"path"] = baseline[&"runtime"].path_service.create_snapshot()
|
|
legacy_runtime[&"path"][&"schema_version"] = 3
|
|
legacy_runtime[&"path"].erase(&"vertical_walk_blockers")
|
|
legacy_runtime[&"path"].erase(&"vertical_walk_requires_connector")
|
|
legacy_runtime[&"path"].erase(&"transit_actor_ids")
|
|
_free_fixture(baseline)
|
|
_expect(
|
|
stripped_v6_events > 0 and _legacy_v5_infrastructure_shape_is_clean(legacy_runtime),
|
|
"v5 fixture removes v6-only aggregate and DomainEvent history instead of relabelling v6"
|
|
)
|
|
if not _legacy_v5_infrastructure_shape_is_clean(legacy_runtime):
|
|
_free_fixture(source)
|
|
return
|
|
|
|
var first_target := _new_fixture(81_071)
|
|
var first_runtime: Variant = first_target[&"runtime"]
|
|
_expect(first_runtime.restore_snapshot(legacy), "v5 proxy snapshot migrates through Runtime restore")
|
|
var first_migrated: Dictionary = first_runtime.create_snapshot()
|
|
_expect(
|
|
int(first_migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 9,
|
|
"v5 migration is immediately re-emitted as v9"
|
|
)
|
|
_expect(
|
|
first_runtime.facility_infrastructure_projector.has_projection(instance_id)
|
|
and first_runtime.path_service.has_cell(Vector3i(0, 1, PathType.FRONT)),
|
|
"v5 construction proxy is deterministically reprojected into real Path"
|
|
)
|
|
_expect(
|
|
not first_runtime.facility_service.has_instance(instance_id),
|
|
"v5 operational proxy is removed from FacilityService during migration"
|
|
)
|
|
|
|
var second_target := _new_fixture(81_072)
|
|
var second_runtime: Variant = second_target[&"runtime"]
|
|
_expect(second_runtime.restore_snapshot(legacy), "the same v5 proxy snapshot migrates a second time")
|
|
_expect(
|
|
second_runtime.create_snapshot() == first_migrated,
|
|
"v5 proxy reprojection is deterministic across Runtime instances"
|
|
)
|
|
_free_fixture(second_target)
|
|
_free_fixture(first_target)
|
|
_free_fixture(source)
|
|
|
|
|
|
func _test_rank_expansion_preserves_route_union() -> void:
|
|
var fixture := _new_fixture(81_080)
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var route_cell := Vector3i(0, 1, PathType.FRONT)
|
|
var built := _complete_proxy(
|
|
fixture,
|
|
&"guest_corridor",
|
|
route_cell,
|
|
&"infra.expansion.proxy"
|
|
)
|
|
_expect(built[&"ok"] and runtime.path_service.has_cell(route_cell), "Rank 1 fixture has a projected route outside base walkability")
|
|
if not built[&"ok"] or not runtime.path_service.has_cell(route_cell):
|
|
_free_fixture(fixture)
|
|
return
|
|
var adjacent_cell := Vector3i(1, 1, PathType.FRONT)
|
|
var adjacent := _complete_proxy(fixture, &"guest_corridor", adjacent_cell, &"infra.expansion.adjacent")
|
|
_expect(adjacent[&"ok"], "rank expansion fixture has a same-floor corridor connection")
|
|
_expect(_promote_rank_two(fixture, &"infra.expansion.rank"), "rank expansion commits with an existing route")
|
|
_expect(
|
|
runtime.route_subslot_path_adapter.get_projected_route_cells().has(route_cell)
|
|
and runtime.path_service.has_cell(route_cell),
|
|
"rank expansion preserves projected ROUTE cells"
|
|
)
|
|
_expect(
|
|
_same_cells(
|
|
runtime.route_subslot_path_adapter.get_base_walkable_cells(),
|
|
runtime.world_service.get_walkable_cells()
|
|
),
|
|
"rank expansion replaces the adapter base with all expanded World walkability"
|
|
)
|
|
_expect(
|
|
runtime.path_service.find_path(adjacent_cell, route_cell) == [adjacent_cell, route_cell],
|
|
"expanded base preserves the pre-existing same-floor route"
|
|
)
|
|
_expect(
|
|
runtime.path_service.find_path(Vector3i(0, 0, PathType.FRONT), route_cell).is_empty(),
|
|
"rank expansion does not add implicit vertical walking"
|
|
)
|
|
_free_fixture(fixture)
|
|
|
|
|
|
func _promote_rank_two(fixture: Dictionary, prefix: StringName) -> bool:
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var reputation = _submit(
|
|
runtime,
|
|
&"update_progression_metrics",
|
|
{&"metrics": {&"reputation": 25}},
|
|
StringName("%s.reputation" % prefix)
|
|
)
|
|
var capital = _submit(
|
|
runtime,
|
|
&"change_resources",
|
|
{
|
|
&"deltas": {
|
|
&"gold": maxi(
|
|
10_000 - runtime.economy_service.get_balance(&"PLAYER_CASH"),
|
|
0
|
|
),
|
|
},
|
|
},
|
|
StringName("%s.capital" % prefix)
|
|
)
|
|
var promoted = _submit(
|
|
runtime,
|
|
&"set_world_tree_rank",
|
|
{&"rank": 2},
|
|
StringName("%s.promote" % prefix)
|
|
)
|
|
return reputation.is_success() and capital.is_success() and promoted.is_success() \
|
|
and fixture[&"state"].world_tree_rank == 2
|
|
|
|
|
|
func _complete_excavation(
|
|
fixture: Dictionary,
|
|
cell: Vector3i,
|
|
key: StringName
|
|
) -> StringName:
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var result = _submit(runtime, &"start_excavation", {&"cell": cell}, key)
|
|
if not result.is_success():
|
|
return &""
|
|
var job_id := StringName(result.details.get(&"job_id", &""))
|
|
var job: Dictionary = runtime.excavation_service.get_job(job_id)
|
|
fixture[&"clock"].advance_minutes(int(job.get(&"build_minutes", 0)))
|
|
return job_id if runtime.excavation_service.get_job(job_id).get(&"state", &"") \
|
|
== runtime.excavation_service.STATE_COMPLETED else &""
|
|
|
|
|
|
func _complete_proxy(
|
|
fixture: Dictionary,
|
|
definition_id: StringName,
|
|
origin: Vector3i,
|
|
key: StringName
|
|
) -> Dictionary:
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var result = _submit(
|
|
runtime,
|
|
&"build_facility",
|
|
{&"definition_id": definition_id, &"origin": origin, &"rotation": 0},
|
|
key
|
|
)
|
|
if not result.is_success():
|
|
return {&"ok": false, &"result": result}
|
|
var job_id := StringName(result.details.get(&"job_id", &""))
|
|
var job: Variant = runtime.construction_service.get_job(job_id)
|
|
if job == null:
|
|
return {&"ok": false, &"result": result}
|
|
fixture[&"clock"].advance_minutes(int(job.build_minutes))
|
|
return {
|
|
&"ok": job.state == ConstructionJob.State.COMPLETED,
|
|
&"job_id": job_id,
|
|
&"instance_id": job.facility_instance_id,
|
|
&"result": result,
|
|
}
|
|
|
|
|
|
func _register_legacy_operational_proxy(runtime: Variant, instance: Dictionary) -> bool:
|
|
var definition: Variant = runtime.definition_registry.get_facility(
|
|
StringName(instance.get(&"definition_id", &""))
|
|
)
|
|
if definition == null:
|
|
return false
|
|
var instance_id := StringName(instance.get(&"instance_id", &""))
|
|
# While this acceptance test straddles the v5/v6 boundary, an older Runtime
|
|
# may already have registered the proxy through its completion callback. A
|
|
# completed v6 Runtime deliberately will not, so construct the same legacy
|
|
# aggregate only when it is absent.
|
|
if runtime.facility_service.has_instance(instance_id):
|
|
return true
|
|
runtime.economy_service.register_account(StringName("FACILITY_REVENUE:%s" % instance_id))
|
|
var result = runtime.facility_service.create_instance({
|
|
&"instance_id": instance_id,
|
|
&"definition_id": definition.id,
|
|
&"origin": instance.get(&"origin", Vector3i.ZERO),
|
|
&"rotation": instance.get(&"rotation", 0),
|
|
&"occupied_cells": instance.get(&"occupied_cells", []),
|
|
&"local_inventory_id": StringName("facility.inventory:%s" % instance_id),
|
|
&"capacity": maxi(definition.capacity, 1),
|
|
&"queue_capacity": definition.queue_capacity,
|
|
&"base_service_minutes": definition.base_service_minutes,
|
|
&"base_quality": definition.base_quality,
|
|
&"base_price": definition.base_price,
|
|
&"staff_min": definition.staff_min,
|
|
&"staff_max": definition.staff_max,
|
|
&"created_at": int(instance.get(&"completed_at", 0)),
|
|
})
|
|
return result.is_success()
|
|
|
|
|
|
func _strip_v6_infrastructure_domain_history(runtime_data: Dictionary) -> int:
|
|
var stream_value: Variant = runtime_data.get(&"domain_events", null)
|
|
if not stream_value is Dictionary:
|
|
return 0
|
|
var stream: Dictionary = stream_value
|
|
var recent_value: Variant = stream.get("recent_events", null)
|
|
var published_value: Variant = stream.get("published_ids", null)
|
|
if not recent_value is Array or not published_value is Array:
|
|
return 0
|
|
var removed_ids: Dictionary = {}
|
|
var retained_recent: Array[Dictionary] = []
|
|
for raw_event: Variant in recent_value:
|
|
if not raw_event is Dictionary:
|
|
return 0
|
|
var event: Dictionary = raw_event
|
|
if StringName(event.get("event_type", "")) in [
|
|
&"FACILITY_INFRASTRUCTURE_PROJECTED",
|
|
&"EXCAVATION_COMPLETED",
|
|
]:
|
|
removed_ids[StringName(event.get("event_id", ""))] = true
|
|
else:
|
|
retained_recent.append(event.duplicate(true))
|
|
var retained_published: Array[String] = []
|
|
for raw_event_id: Variant in published_value:
|
|
var event_id := StringName(raw_event_id)
|
|
if not removed_ids.has(event_id):
|
|
retained_published.append(String(event_id))
|
|
stream["recent_events"] = retained_recent
|
|
stream["published_ids"] = retained_published
|
|
return removed_ids.size()
|
|
|
|
|
|
func _legacy_v5_infrastructure_shape_is_clean(runtime_data: Dictionary) -> bool:
|
|
if runtime_data.has(&"kitchen_production") or runtime_data.has(&"elevators") or runtime_data.has(&"legacy_stairs") or runtime_data.has(&"staff_commute"):
|
|
return false
|
|
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
|
|
if runtime_data.has(key) or runtime_data.has(String(key)):
|
|
return false
|
|
var stream_value: Variant = runtime_data.get(&"domain_events", null)
|
|
if not stream_value is Dictionary:
|
|
return false
|
|
for raw_event: Variant in stream_value.get("recent_events", []):
|
|
if not raw_event is Dictionary:
|
|
return false
|
|
if StringName(raw_event.get("event_type", "")) in [
|
|
&"FACILITY_INFRASTRUCTURE_PROJECTED",
|
|
&"EXCAVATION_COMPLETED",
|
|
]:
|
|
return false
|
|
var queue_value: Variant = runtime_data.get(&"command_queue", null)
|
|
if not queue_value is Dictionary:
|
|
return false
|
|
for queue_key: String in ["pending", "in_flight"]:
|
|
for raw_command: Variant in queue_value.get(queue_key, []):
|
|
if not raw_command is Dictionary:
|
|
return false
|
|
if StringName(raw_command.get("command_type", "")) in [
|
|
&"start_excavation", &"pause_excavation", &"resume_excavation",
|
|
]:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _construction_job_snapshot(runtime: Variant, job_id: StringName) -> Dictionary:
|
|
for row: Variant in runtime.construction_service.create_snapshot().get(&"jobs", []):
|
|
if row is Dictionary and StringName(row.get(&"job_id", &"")) == job_id:
|
|
return row.duplicate(true)
|
|
return {}
|
|
|
|
|
|
func _infrastructure_state(fixture: Dictionary) -> Dictionary:
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
return {
|
|
&"game_state": fixture[&"state"].create_snapshot(),
|
|
&"economy": runtime.economy_service.create_snapshot(),
|
|
&"world": runtime.world_service.create_snapshot(),
|
|
&"path": runtime.path_service.create_snapshot(),
|
|
&"construction": runtime.construction_service.create_snapshot(),
|
|
&"excavation": runtime.excavation_service.create_snapshot(),
|
|
&"spatial_subslots": runtime.spatial_subslot_service.create_snapshot(),
|
|
&"route_projection": runtime.route_subslot_path_adapter.create_snapshot(),
|
|
&"facility_infrastructure": runtime.facility_infrastructure_projector.create_snapshot(),
|
|
&"facilities": runtime.facility_service.create_snapshot(),
|
|
&"mana": runtime.mana_network_service.create_snapshot(),
|
|
}
|
|
|
|
|
|
func _completion_boundary_state(fixture: Dictionary) -> Dictionary:
|
|
# MANA is an earlier Clock phase and has legitimately committed the new
|
|
# minute before Construction discovers a projection failure. The atomic
|
|
# completion boundary starts at Construction and owns the state below.
|
|
var state := _infrastructure_state(fixture)
|
|
state.erase(&"mana")
|
|
return state
|
|
|
|
|
|
func _assert_runtime_restore_rejected(
|
|
runtime: Variant,
|
|
source: Dictionary,
|
|
mutator: Callable,
|
|
label: String
|
|
) -> void:
|
|
var before: Dictionary = runtime.create_snapshot()
|
|
var candidate: Dictionary = source.duplicate(true)
|
|
mutator.call(candidate)
|
|
_expect(not runtime.restore_snapshot(candidate), "%s is rejected" % label)
|
|
_expect(runtime.create_snapshot() == before, "%s rejection is Runtime-atomic" % label)
|
|
|
|
|
|
func _saw_domain_event(runtime: Variant, event_type: StringName, entity_id: StringName) -> bool:
|
|
for event: SimulationDomainEvent in runtime.domain_events.recent_events():
|
|
if event.event_type == event_type and entity_id in event.entity_ids:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _domain_event_count(runtime: Variant, event_type: StringName) -> int:
|
|
var count := 0
|
|
for event: SimulationDomainEvent in runtime.domain_events.recent_events():
|
|
if event.event_type == event_type:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _ledger_category_count(runtime: Variant, category: StringName) -> int:
|
|
var count := 0
|
|
for entry: LedgerEntry in runtime.economy_service.get_entries():
|
|
if entry.category == category:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _runtime_failure_diagnostic(runtime: Variant) -> Dictionary:
|
|
var report_methods: Array[StringName] = [
|
|
&"get_infrastructure_report",
|
|
&"get_operations_report",
|
|
]
|
|
for method_name: StringName in report_methods:
|
|
if not runtime.has_method(method_name):
|
|
continue
|
|
var raw_report: Variant = runtime.call(method_name)
|
|
if not raw_report is Dictionary:
|
|
continue
|
|
var report: Dictionary = raw_report
|
|
var latched_value: Variant = report.get(
|
|
&"failure_latched",
|
|
report.get(&"runtime_failure_latched", null)
|
|
)
|
|
var raw_failure: Variant = report.get(
|
|
&"failure_report",
|
|
report.get(&"runtime_failure_report", null)
|
|
)
|
|
if typeof(latched_value) != TYPE_BOOL or not raw_failure is Dictionary:
|
|
continue
|
|
var failure: Dictionary = raw_failure
|
|
var reason := String(failure.get(
|
|
&"reason",
|
|
failure.get(&"code", failure.get(&"message", ""))
|
|
))
|
|
return {
|
|
&"latched": bool(latched_value),
|
|
&"reason": reason,
|
|
&"failure": failure.duplicate(true),
|
|
}
|
|
return {}
|
|
|
|
|
|
func _submit(
|
|
runtime: Variant,
|
|
command_type: StringName,
|
|
payload: Dictionary,
|
|
idempotency_key: StringName
|
|
) -> Variant:
|
|
return runtime.submit_command(
|
|
command_type,
|
|
payload,
|
|
&"player",
|
|
&"",
|
|
100,
|
|
-1,
|
|
idempotency_key
|
|
)
|
|
|
|
|
|
func _object_has_property(value: Object, property_name: StringName) -> bool:
|
|
for property: Dictionary in value.get_property_list():
|
|
if StringName(property.get(&"name", property.get("name", ""))) == property_name:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _same_cells(first: Array, second: Array) -> bool:
|
|
if first.size() != second.size():
|
|
return false
|
|
var first_set: Dictionary = {}
|
|
for cell: Variant in first:
|
|
first_set[cell] = true
|
|
for cell: Variant in second:
|
|
if not first_set.has(cell):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|