Files
TheTree_mock/tests/s1/test_construction_service.gd

753 lines
34 KiB
GDScript

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)