313 lines
18 KiB
GDScript
313 lines
18 KiB
GDScript
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)
|