257 lines
9.4 KiB
GDScript
257 lines
9.4 KiB
GDScript
extends SceneTree
|
|
|
|
const CapabilityScript := preload("res://world/targets/WorldTargetCapability.gd")
|
|
const DescriptorScript := preload("res://world/targets/WorldTargetDescriptor.gd")
|
|
const RegistryScript := preload("res://world/targets/WorldTargetRegistry.gd")
|
|
const ActionCommandScript := preload("res://simulation/commands/ActionCommand.gd")
|
|
const ActionResultScript := preload("res://simulation/commands/ActionResult.gd")
|
|
const ActionOfferScript := preload("res://simulation/commands/ActionOffer.gd")
|
|
const InteractionServiceScript := preload("res://simulation/commands/InteractionService.gd")
|
|
const ActionCommandServiceScript := preload("res://simulation/commands/ActionCommandService.gd")
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
_test_canonical_lookup_and_cross_kind_uniqueness()
|
|
_test_deterministic_iteration_and_capability_updates()
|
|
_test_transient_handle_lifecycle()
|
|
_test_two_world_isolation()
|
|
_test_descriptors_and_commands_are_non_authoritative_values()
|
|
_finish()
|
|
|
|
|
|
func _test_canonical_lookup_and_cross_kind_uniqueness() -> void:
|
|
var registry := RegistryScript.new(&"jajce")
|
|
var descriptor: WorldTargetDescriptor = _descriptor(
|
|
&"shared_target", &"resource", [&"gather_food"], Vector3(2.0, 0.0, 3.0)
|
|
)
|
|
var handle = registry.register_target(descriptor)
|
|
_check(handle != null, "A valid target should register")
|
|
_check(registry.size() == 1, "The registry should contain one canonical target")
|
|
_check(
|
|
registry.get_descriptor(&"shared_target").get_target_kind() == &"resource",
|
|
"Canonical lookup should return the registered descriptor"
|
|
)
|
|
var conflicting: WorldTargetDescriptor = _descriptor(
|
|
&"shared_target", &"storage", [&"deposit_food"]
|
|
)
|
|
_check(
|
|
registry.register_target(conflicting) == null,
|
|
"One target ID must not register under a second kind in the same context"
|
|
)
|
|
_check(
|
|
registry.last_error.contains("already registered as kind 'resource'"),
|
|
"A cross-kind collision should expose a useful non-diagnostic error"
|
|
)
|
|
_check(registry.size() == 1, "A rejected collision must not replace the canonical entry")
|
|
|
|
|
|
func _test_deterministic_iteration_and_capability_updates() -> void:
|
|
var registry := RegistryScript.new(&"ordered_world")
|
|
registry.register_target(_descriptor(&"target_z", &"resource", [&"gather_food"]))
|
|
var middle_handle = registry.register_target(
|
|
_descriptor(&"target_m", &"activity", [&"rest", &"socialize"])
|
|
)
|
|
registry.register_target(_descriptor(&"target_a", &"resource", [&"gather_food"]))
|
|
|
|
_check(
|
|
registry.get_target_ids() == [&"target_a", &"target_m", &"target_z"],
|
|
"Iteration should be stable-ID ordered regardless of registration order"
|
|
)
|
|
_check(
|
|
registry.get_target_ids(&"resource") == [&"target_a", &"target_z"],
|
|
"Kind-filtered iteration should remain deterministic"
|
|
)
|
|
_check(
|
|
registry.get_target_ids(&"", &"gather_food") == [&"target_a", &"target_z"],
|
|
"Capability-filtered iteration should remain deterministic"
|
|
)
|
|
var updated: WorldTargetDescriptor = _descriptor(
|
|
&"target_m", &"activity", [&"trade"], Vector3(8.0, 0.0, -2.0)
|
|
)
|
|
_check(registry.update_target(middle_handle, updated), "A live handle should update facts")
|
|
_check(
|
|
registry.get_target_ids(&"", &"socialize").is_empty(),
|
|
"Updating capabilities should remove old capability-index membership"
|
|
)
|
|
_check(
|
|
registry.get_target_ids(&"", &"trade") == [&"target_m"],
|
|
"Updating capabilities should add new capability-index membership"
|
|
)
|
|
_check(
|
|
registry.resolve_handle(middle_handle).get_local_position() == Vector3(8.0, 0.0, -2.0),
|
|
"A target update should preserve handle identity while replacing transient facts"
|
|
)
|
|
|
|
|
|
func _test_transient_handle_lifecycle() -> void:
|
|
var registry := RegistryScript.new(&"lifecycle_world")
|
|
var first_provider := Node.new()
|
|
var first_handle = registry.register_target(
|
|
_descriptor(&"workbench", &"activity", [&"craft"]), first_provider
|
|
)
|
|
_check(
|
|
registry.is_handle_valid(first_handle, true),
|
|
"A newly registered provider handle should resolve"
|
|
)
|
|
_check(
|
|
registry.get_provider(first_handle) == first_provider,
|
|
"A handle should resolve its provider"
|
|
)
|
|
_check(
|
|
registry.unregister_target(first_handle), "The current handle should unregister its target"
|
|
)
|
|
_check(
|
|
not registry.is_handle_valid(first_handle), "Unregistration should invalidate the handle"
|
|
)
|
|
|
|
var second_provider := Node.new()
|
|
var second_handle = registry.register_target(
|
|
_descriptor(&"workbench", &"activity", [&"craft"]), second_provider
|
|
)
|
|
_check(
|
|
second_handle.get_generation() > first_handle.get_generation(),
|
|
"Re-registration should advance the transient generation"
|
|
)
|
|
_check(
|
|
not registry.unregister_target(first_handle),
|
|
"A stale provider must not unregister a newer canonical target"
|
|
)
|
|
_check(
|
|
registry.is_handle_valid(second_handle, true),
|
|
"Rejecting a stale handle should leave the newer target intact"
|
|
)
|
|
second_provider.free()
|
|
_check(
|
|
registry.prune_invalid_providers() == [&"workbench"],
|
|
"Dead weak providers should be pruned deterministically"
|
|
)
|
|
_check(not registry.is_handle_valid(second_handle), "Pruning should invalidate its handle")
|
|
first_provider.free()
|
|
|
|
|
|
func _test_two_world_isolation() -> void:
|
|
var first_world := RegistryScript.new(&"world_one")
|
|
var second_world := RegistryScript.new(&"world_two")
|
|
var first_handle = first_world.register_target(
|
|
_descriptor(&"village_pantry", &"storage", [&"withdraw_food"])
|
|
)
|
|
var second_handle = second_world.register_target(
|
|
_descriptor(&"village_pantry", &"landmark", [&"inspect"])
|
|
)
|
|
|
|
_check(
|
|
first_handle != null and second_handle != null, "Two contexts may reuse a local target ID"
|
|
)
|
|
_check(
|
|
first_world.resolve_handle(second_handle) == null,
|
|
"A handle from another registry must never resolve, even with the same target ID"
|
|
)
|
|
_check(
|
|
second_world.resolve_handle(first_handle) == null,
|
|
"Registry ownership should isolate handles in both directions"
|
|
)
|
|
_check(
|
|
first_world.unregister_target(first_handle),
|
|
"The first world should unregister independently"
|
|
)
|
|
_check(
|
|
second_world.is_handle_valid(second_handle),
|
|
"Unloading one context must not affect a second context's canonical target"
|
|
)
|
|
_check(
|
|
second_world.get_descriptor(&"village_pantry").get_target_kind() == &"landmark",
|
|
"Each context should retain its own target facts"
|
|
)
|
|
|
|
|
|
func _test_descriptors_and_commands_are_non_authoritative_values() -> void:
|
|
var attributes := {"amounts": [1, 2]}
|
|
var metadata := {"tags": ["village"]}
|
|
var capability := CapabilityScript.new(&"inspect", attributes)
|
|
var descriptor := DescriptorScript.new(
|
|
&"notice_board", &"activity", [capability], Vector3.ZERO, "Notice board", metadata
|
|
)
|
|
attributes["amounts"][0] = 99
|
|
metadata["tags"].append("mutated")
|
|
var registry := RegistryScript.new(&"command_world")
|
|
var handle = registry.register_target(descriptor)
|
|
var stored := registry.get_descriptor(&"notice_board")
|
|
_check(
|
|
stored.get_capability(&"inspect").get_attribute(&"amounts") == [1, 2],
|
|
"Capability attributes should be isolated from caller mutation"
|
|
)
|
|
_check(
|
|
stored.get_metadata_value(&"tags") == ["village"],
|
|
"Target metadata should be isolated from caller mutation"
|
|
)
|
|
|
|
var offer_parameters := {"page": 1}
|
|
var offer := ActionOfferScript.new(
|
|
&"inspect_board", &"inspect", handle, "Read notices", true, "", offer_parameters, 2.0
|
|
)
|
|
offer_parameters["page"] = 9
|
|
_check(offer.is_valid(), "A well-formed advisory action offer should be valid")
|
|
_check(offer.get_parameters()["page"] == 1, "An offer should copy its parameter value")
|
|
var command_parameters := {"page": 1}
|
|
var command := ActionCommandScript.new(
|
|
&"command_1", &"person_7", offer.get_action_id(), offer.get_target(), command_parameters
|
|
)
|
|
command_parameters["page"] = 5
|
|
_check(command.is_valid(), "A command should carry a well-formed transient target intent")
|
|
_check(command.get_parameters()["page"] == 1, "A command should copy its parameter value")
|
|
_check(registry.unregister_target(handle), "The offered target should be removable")
|
|
_check(
|
|
command.is_valid() and not registry.is_handle_valid(command.get_target()),
|
|
"Command shape validity must not make a stale target authoritative"
|
|
)
|
|
|
|
var accepted := ActionResultScript.accepted(&"command_1", {"queued": true})
|
|
var rejected := ActionResultScript.rejected(&"command_2", &"stale_target", "Try again")
|
|
_check(accepted.is_valid() and accepted.is_accepted(), "Accepted results should be explicit")
|
|
_check(
|
|
rejected.is_valid() and rejected.is_terminal() and not rejected.is_accepted(),
|
|
"Rejected results should carry a terminal, non-success status"
|
|
)
|
|
_check(
|
|
(
|
|
InteractionServiceScript
|
|
. new()
|
|
. get_action_offers(&"person_7", command.get_target())
|
|
. is_empty()
|
|
),
|
|
"The manager-independent interaction interface should have no implicit offers"
|
|
)
|
|
_check(
|
|
ActionCommandServiceScript.new().submit_command(command) == null,
|
|
"The manager-independent command interface should have no implicit authority"
|
|
)
|
|
|
|
|
|
func _descriptor(
|
|
target_id: StringName,
|
|
target_kind: StringName,
|
|
capability_ids: Array[StringName],
|
|
position: Vector3 = Vector3.ZERO
|
|
) -> WorldTargetDescriptor:
|
|
var capabilities: Array = []
|
|
for capability_id in capability_ids:
|
|
capabilities.append(CapabilityScript.new(capability_id))
|
|
return DescriptorScript.new(target_id, target_kind, capabilities, position)
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("[TEST] World target registry passed: scoped canonical IDs, handles, and commands")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|