Preserve approved pixel tower prototype as development baseline

This commit is contained in:
Codex
2026-09-09 07:51:16 +09:00
commit 423fd6dd2e
915 changed files with 95193 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
[gd_resource type="Resource" script_class="FacilityDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/data/facility_definition.gd" id="1"]
[resource]
script = ExtResource("1")
definition_schema_version = 2
id = &"invalid_service_profile"
display_name = "不正サービス試験施設"
description = "詳細サービス値の拒否を確認する試験用定義。"
category = &"test_facility"
service_types = Array[StringName]([&"test_service"])
default_service_type = &"test_service"
service_profiles = {&"test_service": {&"duration_minutes": -1, &"base_quality": 50, &"base_price": 0, &"capacity_units": 1, &"need_effects": {&"sleep": 0}}}
footprint = Vector2i(1, 1)
guest_access_offsets = Array[Vector2i]([Vector2i(0, 0)])
max_level = 1
upgrade_costs = Array[Dictionary]([])
capacity = 1
queue_capacity = 0
base_service_minutes = 1
base_quality = 50
base_price = 0
opening_schedule = {&"morning": true, &"day": true, &"night": true}
statistics_keys = Array[StringName]([&"services_completed"])
tags = Array[StringName]([&"test_facility"])
+342
View File
@@ -0,0 +1,342 @@
extends SceneTree
const CatalogType := preload("res://scripts/data/mvp_content_catalog.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_test_baseline_catalog()
_test_guest_combinations()
_test_six_need_schema()
_test_staff_hire_boundary()
_test_unique_pair_and_defensive_copies()
_test_invalid_content_is_fail_closed()
_test_type_coercion_is_rejected()
_test_fixed_ids_namespaces_and_resource_paths()
_test_unique_character_safety_contract()
_test_numeric_and_container_limits()
_test_json_key_collisions()
_test_error_order_is_deterministic()
if _failures == 0:
print("PASS: MvpContentCatalog (%d checks)" % _checks)
else:
push_error("FAIL: MvpContentCatalog (%d of %d checks failed)" % [_failures, _checks])
quit(_failures)
func _test_baseline_catalog() -> void:
var catalog := CatalogType.new()
var report: Dictionary = catalog.rebuild()
_expect(bool(report[&"valid"]), "MVP character catalog passes strict schema validation")
_expect(int(report[&"error_count"]) == 0, "baseline character data has no validation errors")
_expect(int(report[&"counts"]["guest_classes"]) == 8, "all eight guest classes are indexed")
_expect(int(report[&"counts"]["species"]) == 4, "all four independent species are indexed")
_expect(int(report[&"counts"]["staff_candidates"]) == 20, "all twenty free-hire candidates are indexed")
_expect(int(report[&"counts"]["unique_characters"]) == 2, "one heroine and one rival are indexed")
_expect(catalog.get_guest_class_ids() == [
&"cleric", &"diplomat", &"mage", &"merchant", &"noble", &"outsider", &"rogue", &"warrior",
], "guest classes use stable lexical order independent of JSON order")
_expect(catalog.get_species_ids() == [
&"beastfolk", &"demonfolk", &"elf", &"human",
], "species use stable lexical order")
_expect(catalog.get_staff_candidate_ids().front() == &"staff_akane", "candidate IDs are stable and namespaced")
_expect(catalog.get_staff_candidate_ids().back() == &"staff_tessa", "all candidate IDs participate in deterministic order")
func _test_guest_combinations() -> void:
var catalog := CatalogType.new()
catalog.rebuild()
var definitions := catalog.build_all_guest_definitions()
_expect(definitions.size() == 32, "eight classes and four species produce 32 independent archetypes")
var ids: Array[StringName] = []
for definition in definitions:
ids.append(definition.guest_type_id)
_expect(definition.is_valid(), "generated archetype %s satisfies GuestDefinition invariants" % definition.guest_type_id)
_expect(definition.adult_verified, "generated archetype %s is adult-content gated" % definition.guest_type_id)
var unique_ids: Dictionary = {}
for definition_id: StringName in ids:
unique_ids[definition_id] = true
_expect(unique_ids.size() == 32 and not unique_ids.has(&""), "all generated archetype IDs are non-empty and unique")
var warrior_elf = catalog.build_guest_definition(&"warrior", &"elf")
_expect(warrior_elf != null and warrior_elf.guest_type_id == &"warrior_elf", "class and species IDs form a reproducible archetype ID")
_expect(warrior_elf.wealth_min == 330 and warrior_elf.wealth_max == 990, "species wealth multiplier uses integer basis points")
_expect(warrior_elf.base_patience == 70 and warrior_elf.risk_tolerance == 70, "species modifiers compose with class values once")
_expect(&"combatant" in warrior_elf.tags and &"elf" in warrior_elf.tags, "class and species tags remain separate and both survive composition")
_expect(warrior_elf.preference_weights[&"quiet"] == 8, "species preferences extend class preferences")
_expect(warrior_elf.need_rates[&"sleep"] == 5, "species Need modifiers compose on the canonical Sleep axis")
_expect(catalog.build_guest_definition(&"missing", &"human") == null, "unknown class fails closed")
_expect(catalog.build_guest_definition(&"warrior", &"missing") == null, "unknown species fails closed")
func _test_six_need_schema() -> void:
var catalog := CatalogType.new()
catalog.rebuild()
var definition = catalog.build_guest_definition(&"diplomat", &"demonfolk")
var need_ids: Array = definition.need_rates.keys()
need_ids.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b))
_expect(need_ids == [&"hunger", &"hygiene", &"intimacy", &"leisure", &"medical", &"sleep"], "generated definitions expose exactly the canonical six Need IDs")
_expect(not definition.need_rates.has(&"safety") and not definition.need_rates.has(&"social"), "Safety and social context are not persisted as Needs")
_expect(definition.need_rates[&"leisure"] == 5, "species Leisure modifier composes without being misread as Medical")
var documents := _load_documents()
documents[&"guest_classes"]["entries"][0]["need_rates"].erase("hygiene")
documents[&"guest_classes"]["entries"][0]["need_rates"]["safety"] = 1
var report := catalog.rebuild_from_documents(documents)
_expect(&"unknown_map_key" in _issue_codes(report[&"errors"]), "legacy Need names are rejected by the current content schema")
documents = _load_documents()
documents[&"guest_classes"]["entries"][0]["need_rates"].erase("hygiene")
report = catalog.rebuild_from_documents(documents)
_expect(&"missing_map_key" in _issue_codes(report[&"errors"]), "all six class Need rates are required")
documents = _load_documents()
documents[&"species"]["entries"][0]["need_rate_modifiers"]["rest"] = -1
report = catalog.rebuild_from_documents(documents)
_expect(&"unknown_map_key" in _issue_codes(report[&"errors"]), "species modifiers cannot reintroduce a migration-only alias")
func _test_staff_hire_boundary() -> void:
var catalog := CatalogType.new()
catalog.rebuild()
for candidate_id: StringName in catalog.get_staff_candidate_ids():
var candidate := catalog.get_entry(&"staff_candidates", candidate_id)
_expect(bool(candidate[&"adult_verified"]) and int(candidate[&"age"]) >= 18, "%s carries explicit adult verification" % candidate_id)
_expect(int(candidate[&"daily_wage"]) >= 0 and int(candidate[&"signing_cost"]) >= 0, "%s has non-negative employment costs" % candidate_id)
_expect((candidate[&"skills"] as Dictionary).size() == 4, "%s defines all four dispatch skills" % candidate_id)
var payload := catalog.staff_hire_payload(&"staff_klaus")
_expect(payload[&"staff_id"] == &"staff_klaus" and payload[&"role_id"] == &"guard", "hire payload preserves candidate and role IDs")
_expect(payload[&"adult_verified"] and int(payload[&"age"]) == 36, "hire payload carries the adult gate across the runtime boundary")
_expect(payload[&"preferred_shift"] == &"day", "hire payload exposes the validated preferred shift to the scheduler adapter")
_expect(payload[&"options"][&"daily_wage"] == 155 and payload[&"options"][&"employment_kind"] == &"FREE_HIRE", "runtime options preserve wage liability and ordinary employment kind")
_expect(payload[&"options"][&"skills"][&"combat"] == 82, "hire payload maps static skills into StaffRuntime options")
_expect(&"security" in payload[&"options"][&"permitted_task_tags"], "hire payload preserves permitted task tags")
var runtime := StaffRuntime.new().configure(payload[&"staff_id"], payload[&"role_id"], payload[&"options"])
_expect(runtime.is_valid() and runtime.daily_wage == 155 and runtime.get_employment_kind_name() == &"FREE_HIRE", "hire payload constructs an exact payroll-bearing StaffRuntime")
_expect(catalog.staff_hire_payload(&"missing").is_empty(), "unknown candidate cannot produce a hire payload")
func _test_unique_pair_and_defensive_copies() -> void:
var catalog := CatalogType.new()
catalog.rebuild()
var heroine := catalog.get_entry(&"unique_characters", &"heroine_anri")
var rival := catalog.get_entry(&"unique_characters", &"rival_rai")
_expect(heroine[&"character_kind"] == &"heroine" and rival[&"character_kind"] == &"rival", "unique characters have distinct mechanical kinds")
_expect(heroine[&"counterpart_id"] == &"rival_rai" and rival[&"counterpart_id"] == &"heroine_anri", "heroine and rival references are reciprocal")
_expect(heroine[&"adult_verified"] and rival[&"adult_verified"], "both relationship participants are explicitly adult-verified")
_expect(heroine[&"mechanical_effects"][&"assigned_floor_patience_loss_multiplier_bp"] == 9000, "heroine baseline effect is represented in integer basis points")
heroine[&"display_name"] = "tampered"
heroine[&"mechanical_effects"][&"assigned_floor_patience_loss_multiplier_bp"] = 1
var pristine := catalog.get_entry(&"unique_characters", &"heroine_anri")
_expect(pristine[&"display_name"] == "アンリ", "returned definitions cannot mutate catalog identity")
_expect(pristine[&"mechanical_effects"][&"assigned_floor_patience_loss_multiplier_bp"] == 9000, "returned nested data is also a defensive copy")
func _test_invalid_content_is_fail_closed() -> void:
var documents := _load_documents()
var staff_entries: Array = documents[&"staff_candidates"]["entries"]
staff_entries[0]["adult_verified"] = false
staff_entries[1]["age"] = 17
staff_entries[2]["traits"].append("unknown_trait")
staff_entries[3]["typo_field"] = 1
var guest_entries: Array = documents[&"guest_classes"]["entries"]
guest_entries[1]["id"] = guest_entries[0]["id"]
var catalog := CatalogType.new()
var report: Dictionary = catalog.rebuild_from_documents(documents)
_expect(not bool(report[&"valid"]), "invalid character data rejects the whole catalog")
_expect(catalog.get_staff_candidate_ids().is_empty(), "fail-closed rebuild exposes no partially valid candidates")
_expect(catalog.build_all_guest_definitions().is_empty(), "fail-closed rebuild cannot generate partial archetypes")
var codes := _issue_codes(report[&"errors"])
_expect(&"adult_verification_required" in codes, "missing adult verification has a stable error code")
_expect(&"adult_age_required" in codes, "underage data has a stable error code")
_expect(&"unknown_tag" in codes, "unknown trait is rejected instead of silently ignored")
_expect(&"unknown_field" in codes, "misspelled data fields are rejected")
_expect(&"duplicate_id" in codes, "duplicate stable IDs are rejected")
func _test_error_order_is_deterministic() -> void:
var documents := _load_documents()
documents[&"species"]["schema_version"] = 99
documents[&"unique_characters"]["entries"][0]["counterpart_id"] = "missing_actor"
var catalog := CatalogType.new()
var first: Dictionary = catalog.rebuild_from_documents(documents)
var second: Dictionary = catalog.rebuild_from_documents(documents)
_expect(first[&"errors"] == second[&"errors"], "content error order is deterministic across rebuilds")
_expect(not bool(second[&"valid"]), "unsupported schema and orphan references remain fatal")
_expect(&"unsupported_schema" in _issue_codes(second[&"errors"]), "schema rejection has a stable code")
_expect(&"missing_counterpart" in _issue_codes(second[&"errors"]), "orphan counterpart has a stable code")
func _test_type_coercion_is_rejected() -> void:
var documents := _load_documents()
documents[&"species"]["schema_version"] = "1"
documents[&"guest_classes"]["entries"][0]["wealth_min"] = "300"
documents[&"staff_candidates"]["entries"][0]["adult_verified"] = "true"
documents[&"staff_candidates"]["entries"][1]["age"] = "38"
documents[&"staff_candidates"]["entries"][2]["daily_wage"] = "145"
documents[&"unique_characters"]["entries"][0]["character_kind"] = 1
var catalog := CatalogType.new()
var report: Dictionary = catalog.rebuild_from_documents(documents)
var codes := _issue_codes(report[&"errors"])
_expect(not bool(report[&"valid"]), "stringified booleans and numbers do not pass strict content validation")
_expect(&"unsupported_schema" in codes, "string schema version is not coerced")
_expect(&"invalid_wealth_range" in codes, "string wealth is not coerced")
_expect(&"adult_verification_required" in codes, "string adult flag is not truthy-coerced")
_expect(&"adult_age_required" in codes, "string age is not coerced")
_expect(&"negative_value" in codes, "string employment cost is not coerced")
_expect(&"unknown_character_kind" in codes, "numeric character kind is not coerced")
func _test_fixed_ids_namespaces_and_resource_paths() -> void:
var documents := _load_documents()
documents[&"guest_classes"]["entries"][0]["id"] = "replacement_class"
var catalog := CatalogType.new()
var report := catalog.rebuild_from_documents(documents)
_expect(&"mvp_id_set_mismatch" in _issue_codes(report[&"errors"]), "same-count replacement cannot silently change the fixed MVP ID set")
_expect(not bool(report[&"valid"]), "fixed-ID mismatch fails closed")
documents = _load_documents()
documents[&"staff_candidates"]["entries"][0]["id"] = "akane"
report = catalog.rebuild_from_documents(documents)
_expect(&"invalid_namespace" in _issue_codes(report[&"errors"]), "free-hire actor IDs must retain the staff namespace")
documents = _load_documents()
documents[&"species"]["entries"][0]["tags"] = ["elf"]
report = catalog.rebuild_from_documents(documents)
_expect(&"species_identity_mismatch" in _issue_codes(report[&"errors"]), "species identity cannot be changed by swapping its tag")
var paths := CatalogType.DEFAULT_PATHS.duplicate(true)
paths[&"species"] = "res://data/characters/../characters/mvp_species.json"
report = catalog.rebuild(paths)
_expect(&"invalid_resource_path" in _issue_codes(report[&"errors"]), "resource traversal segments are rejected before file parsing")
_expect(catalog.get_species_ids().is_empty(), "invalid resource paths expose no stale prior catalog")
paths = CatalogType.DEFAULT_PATHS.duplicate(true)
paths.erase(&"species")
report = catalog.rebuild(paths)
_expect(&"missing_content_type" in _issue_codes(report[&"errors"]), "custom path sets must name every required document")
func _test_unique_character_safety_contract() -> void:
var documents := _load_documents()
var heroine: Dictionary = documents[&"unique_characters"]["entries"][0]
heroine["traits"].erase("unsellable")
var catalog := CatalogType.new()
var report := catalog.rebuild_from_documents(documents)
_expect(&"unique_safety_traits_required" in _issue_codes(report[&"errors"]), "unique actors cannot lose the unsellable safety marker")
documents = _load_documents()
documents[&"staff_candidates"]["entries"][0]["traits"].append("unique")
report = catalog.rebuild_from_documents(documents)
_expect(&"reserved_trait" in _issue_codes(report[&"errors"]), "free-hire candidates cannot impersonate unique actors")
documents = _load_documents()
heroine = documents[&"unique_characters"]["entries"][0]
heroine["mechanical_effects"]["arbitrary_runtime_hook"] = 1
report = catalog.rebuild_from_documents(documents)
var codes := _issue_codes(report[&"errors"])
_expect(&"invalid_mechanical_effect" in codes, "unknown mechanical effect keys are rejected")
_expect(&"effect_kind_mismatch" in codes, "effects cannot cross the heroine/rival capability boundary")
documents = _load_documents()
documents[&"unique_characters"]["entries"][0]["mechanical_effects"]["assigned_floor_patience_loss_multiplier_bp"] = 2_000
report = catalog.rebuild_from_documents(documents)
_expect(&"invalid_mechanical_effect" in _issue_codes(report[&"errors"]), "basis-point effects below the global multiplier floor are rejected")
documents = _load_documents()
documents[&"unique_characters"]["entries"][1]["mechanical_effects"]["base_shield_attack"] = 101
report = catalog.rebuild_from_documents(documents)
_expect(&"invalid_mechanical_effect" in _issue_codes(report[&"errors"]), "relationship attack data cannot exceed its bounded runtime scale")
func _test_numeric_and_container_limits() -> void:
var documents := _load_documents()
documents[&"staff_candidates"]["entries"][0]["age"] = CatalogType.MAX_AGE + 1
var catalog := CatalogType.new()
var report := catalog.rebuild_from_documents(documents)
_expect(&"adult_age_required" in _issue_codes(report[&"errors"]), "implausibly large ages do not enter persistent actor data")
documents = _load_documents()
documents[&"guest_classes"]["entries"][0]["wealth_max"] = CatalogType.MAX_GOLD_VALUE + 1
report = catalog.rebuild_from_documents(documents)
_expect(&"invalid_wealth_range" in _issue_codes(report[&"errors"]), "money values are bounded before basis-point multiplication")
documents = _load_documents()
documents[&"staff_candidates"]["entries"][0]["daily_wage"] = INF
report = catalog.rebuild_from_documents(documents)
_expect(&"negative_value" in _issue_codes(report[&"errors"]), "non-finite numeric input is never accepted as an integer")
documents = _load_documents()
documents[&"staff_candidates"]["entries"][0]["display_name"] = "unsafe\nname"
report = catalog.rebuild_from_documents(documents)
_expect(&"missing_display_name" in _issue_codes(report[&"errors"]), "control characters are rejected from player-visible names")
documents = _load_documents()
for raw_document: Variant in documents.values():
for raw_entry: Variant in raw_document["entries"]:
for index in 40:
raw_entry["unexpected_%02d" % index] = index
report = catalog.rebuild_from_documents(documents)
_expect(int(report[&"error_count"]) == CatalogType.MAX_ERRORS, "adversarial documents cannot amplify validation reports beyond the error cap")
_expect(bool(report[&"errors_truncated"]), "capped validation reports explicitly disclose truncation")
_expect(not bool(report[&"valid"]), "error-report bounding never weakens fail-closed validation")
func _test_json_key_collisions() -> void:
var catalog := CatalogType.new()
_expect(
catalog._validate_json_key_structure("{\"id\":1,\"id\":2}") == &"duplicate_json_key",
"duplicate JSON object keys are rejected before the parser can overwrite one value"
)
_expect(
catalog._validate_json_key_structure("{\"id\":1,\"\\u0069d\":2}") == &"duplicate_json_key",
"escaped and literal forms of the same JSON key collide canonically"
)
_expect(
catalog._validate_json_key_structure("{\"outer\":{\"age\":1,\"age\":2}}") == &"duplicate_json_key",
"duplicate-key detection covers nested content maps"
)
_expect(
catalog._validate_json_key_structure("[".repeat(CatalogType.MAX_JSON_DEPTH + 1) + "0" + "]".repeat(CatalogType.MAX_JSON_DEPTH + 1)) == &"json_structure_limit_exceeded",
"deeply nested JSON is rejected before recursive validation can exhaust the stack"
)
for path: String in CatalogType.DEFAULT_PATHS.values():
var file := FileAccess.open(path, FileAccess.READ)
_expect(catalog._validate_json_key_structure(file.get_as_text()).is_empty(), "%s has a unique bounded JSON key structure" % path.get_file())
var documents := _load_documents()
documents[&"species"]["entries"][0][42] = "not_a_field_name"
var report := catalog.rebuild_from_documents(documents)
_expect(&"invalid_key_type" in _issue_codes(report[&"errors"]), "non-string object keys are rejected without coercion")
func _load_documents() -> Dictionary:
var result: Dictionary = {}
for content_type: StringName in [&"guest_classes", &"species", &"staff_candidates", &"unique_characters"]:
var path := String(CatalogType.DEFAULT_PATHS[content_type])
var file := FileAccess.open(path, FileAccess.READ)
result[content_type] = JSON.parse_string(file.get_as_text())
return result
func _issue_codes(raw_issues: Variant) -> Array[StringName]:
var result: Array[StringName] = []
if raw_issues is Array:
for raw_issue: Variant in raw_issues:
if raw_issue is Dictionary:
var code := StringName(raw_issue.get(&"code", raw_issue.get("code", "")))
if not result.has(code):
result.append(code)
return result
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://ce2yaa87mm67b
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://cbf77pwt7oqjy
+415
View File
@@ -0,0 +1,415 @@
extends SceneTree
const ProgressionType := preload("res://scripts/progression/progression_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_test_rank_bounds_and_evidence()
_test_metrics_are_atomic_and_lifetime_monotonic()
_test_rank_one_through_seven()
_test_external_transaction_and_idempotency()
_test_failed_rollback_quarantines_progression()
_test_callback_reentry_and_internal_tamper()
_test_snapshot_round_trip_and_atomic_rejection()
_test_promotion_evidence_and_legacy_migration()
_test_input_and_history_bounds()
if _failures == 0:
print("PASS: ProgressionService (%d checks)" % _checks)
else:
push_error("FAIL: ProgressionService (%d of %d checks failed)" % [_failures, _checks])
quit(_failures)
func _test_rank_bounds_and_evidence() -> void:
var service := ProgressionType.new()
_expect(service.configure(), "progression starts at Rank 1")
_expect(service.current_rank == 1, "configured rank is authoritative")
_expect(service.get_rank_bounds(1) == {&"min_x":-12, &"max_x":12, &"min_y":0, &"max_y":6}, "Rank 1 bounds match the system specification")
_expect(service.get_rank_bounds(3) == {&"min_x":-24, &"max_x":24, &"min_y":-8, &"max_y":16}, "Rank 3 MVP bounds include eight underground levels")
_expect(service.get_rank_bounds(7) == {&"min_x":-60, &"max_x":60, &"min_y":-40, &"max_y":56}, "Rank 7 bounds match the final expansion")
_expect(service.get_rank_bounds(8).is_empty(), "unknown rank has no implicit clamped bounds")
var report := service.evaluate_rank(2)
_expect(not report[&"eligible"] and report[&"reason"] == &"REQUIREMENTS_NOT_MET", "Rank 2 reports unmet requirements")
_expect(_requirement_ids(report) == [&"reputation", &"asset_gold"], "Rank 2 evidence order is stable and player-readable")
_expect(report[&"requirements"][0][&"current"] == 0 and report[&"requirements"][0][&"required"] == 25, "eligibility evidence includes current and required values")
var skip := service.evaluate_rank(3)
_expect(not skip[&"eligible"] and skip[&"reason"] == &"TARGET_MUST_BE_NEXT_RANK", "rank skipping is rejected before metric evaluation")
func _test_metrics_are_atomic_and_lifetime_monotonic() -> void:
var service := ProgressionType.new()
service.configure()
_expect(service.update_metrics({&"reputation": 25, &"asset_gold": 10_000, &"faith_lifetime": 100}), "valid metric patch commits atomically")
var before := service.metrics.duplicate(true)
_expect(not service.update_metrics({&"reputation": 101, &"asset_gold": 20_000}), "out-of-range percentage rejects the whole patch")
_expect(service.metrics == before, "failed metric patch preserves every prior metric")
_expect(not service.update_metrics({&"faith_lifetime": 99}), "lifetime Faith cannot decrease after spending current Faith")
_expect(not service.update_metrics({&"guest_lifetime": -1}), "lifetime guest count cannot become negative")
_expect(not service.update_metrics({&"unknown": 1}), "unknown progression metric is rejected")
_expect(not service.update_metrics({&"asset_gold": "10000"}), "string numbers are not coerced into progression facts")
_expect(service.metrics == before, "all rejected patches leave progression facts unchanged")
_expect(service.complete_quest(&"important_rank_5"), "valid stable quest ID records once")
_expect(service.complete_quest(&"important_rank_5"), "completing the same quest is idempotent")
_expect(service.completed_quest_ids.size() == 1, "idempotent quest completion creates no duplicate")
_expect(not service.complete_quest(&"Bad Quest"), "invalid quest ID cannot enter persistent progression")
func _test_rank_one_through_seven() -> void:
var service := ProgressionType.new()
service.configure()
service.update_metrics({&"reputation":25, &"asset_gold":10_000})
_expect(service.request_rank_up(&"rank_up_2", 2).is_success(), "Rank 2 unlocks at Reputation 25 and assets 10,000G")
service.update_metrics({&"faith_lifetime":100, &"guest_lifetime":300})
_expect(service.request_rank_up(&"rank_up_3", 3).is_success(), "Rank 3 unlocks at lifetime Faith 100 and 300 guests")
service.update_metrics({
&"reputation":45,
&"asset_gold":50_000,
&"active_above_ground_facilities":1,
&"active_underground_facilities":1,
})
_expect(service.request_rank_up(&"rank_up_4", 4).is_success(), "Rank 4 additionally requires active facilities above and below ground")
service.update_metrics({&"faith_lifetime":500})
var rank_five_missing := service.evaluate_rank(5)
_expect(not rank_five_missing[&"eligible"], "Rank 5 does not unlock from numbers alone")
service.complete_quest(&"important_rank_5")
_expect(service.request_rank_up(&"rank_up_5", 5).is_success(), "Rank 5 requires its explicit important quest")
service.update_metrics({&"underground_influence":65})
var rank_six := service.evaluate_rank(6)
_expect(rank_six[&"eligible"], "Rank 6 accepts underground influence as the specified Reputation alternative")
_expect(service.request_rank_up(&"rank_up_6", 6).is_success(), "Rank 6 commits through the alternative branch")
service.update_metrics({&"faith_lifetime":1500, &"reputation":60})
var rank_seven_missing := service.evaluate_rank(7)
_expect(not rank_seven_missing[&"eligible"], "Rank 7 still requires the main chapter flag")
service.complete_quest(&"main_chapter_complete")
_expect(service.request_rank_up(&"rank_up_7", 7).is_success(), "Rank 7 requires Faith, both progress axes, dual operation and chapter completion")
_expect(service.current_rank == 7, "rank progression is monotonic through the final rank")
_expect(not service.request_rank_up(&"rank_up_8", 8).is_success(), "Rank 8 is rejected")
func _test_external_transaction_and_idempotency() -> void:
var service := _eligible_rank_two_service()
var external := {&"rank":1, &"bounds":service.get_rank_bounds(1), &"apply_calls":0, &"rollback_calls":0}
var capture := func(_current: int, _target: int) -> Dictionary:
return {&"ok":true, &"snapshot":{&"rank":external[&"rank"], &"bounds":external[&"bounds"].duplicate(true)}}
var apply := func(target: int, bounds: Dictionary) -> Dictionary:
external[&"apply_calls"] += 1
external[&"rank"] = target
external[&"bounds"] = bounds.duplicate(true)
return {&"ok":true}
var rollback := func(snapshot: Dictionary) -> Dictionary:
external[&"rollback_calls"] += 1
external[&"rank"] = snapshot[&"rank"]
external[&"bounds"] = snapshot[&"bounds"].duplicate(true)
return {&"ok":true}
var result := service.request_rank_up(&"atomic_rank_2", 2, capture, apply, rollback)
_expect(result.is_success(), "eligible rank-up commits through capture/apply boundary")
_expect(service.current_rank == 2 and external[&"rank"] == 2, "internal rank and external world commit together")
_expect(external[&"bounds"] == service.get_rank_bounds(2), "external world receives exact target bounds")
var replay := service.request_rank_up(&"atomic_rank_2", 2)
_expect(replay.is_success() and replay.is_replay, "same request ID and target returns its recorded success")
_expect(external[&"apply_calls"] == 1, "idempotent replay does not apply external expansion twice")
var conflict := service.request_rank_up(&"atomic_rank_2", 3)
_expect(not conflict.is_success() and conflict.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, "same request ID with another target conflicts")
var partial := service.request_rank_up(&"partial_callbacks", 3, capture, Callable(), rollback)
_expect(not partial.is_success() and partial.code == CommandResult.Code.INVALID_PAYLOAD, "partial callback set is rejected before mutation")
var strict_callbacks := _eligible_rank_two_service()
var bad_capture := func(_current: int, _target: int) -> Dictionary:
return {&"ok":"true", &"snapshot":{}}
var bad_capture_result := strict_callbacks.request_rank_up(&"bad_capture_type", 2, bad_capture, apply, rollback)
_expect(not bad_capture_result.is_success() and external[&"apply_calls"] == 1, "truthy string capture status is rejected before external apply")
var string_apply := func(_target: int, _bounds: Dictionary) -> Dictionary:
return {&"ok":"true"}
var strict_rollback := func(_snapshot: Dictionary) -> Dictionary:
return {&"ok":true}
var bad_apply_result := strict_callbacks.request_rank_up(&"bad_apply_type", 2, capture, string_apply, strict_rollback)
_expect(not bad_apply_result.is_success() and strict_callbacks.current_rank == 1, "truthy string apply status is treated as failure and rolled back")
func _test_failed_rollback_quarantines_progression() -> void:
var rolled_back := _eligible_rank_two_service()
var external_rank := 1
var capture := func(_current: int, _target: int) -> Dictionary:
return {&"ok":true, &"snapshot":external_rank}
var failed_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
external_rank = target
return {&"ok":false}
var successful_rollback := func(snapshot: int) -> Dictionary:
external_rank = snapshot
return {&"ok":true}
var rolled_back_result := rolled_back.request_rank_up(
&"failed_but_rolled_back", 2, capture, failed_apply, successful_rollback
)
_expect(not rolled_back_result.is_success(), "failed external expansion rejects rank-up")
_expect(rolled_back.current_rank == 1 and external_rank == 1, "successful rollback restores both sides to Rank 1")
_expect(not rolled_back.quarantined, "verified rollback keeps progression available for a later request")
var quarantined := _eligible_rank_two_service()
external_rank = 1
var failed_rollback := func(_snapshot: int) -> Dictionary:
return {&"ok":false}
var quarantined_result := quarantined.request_rank_up(
&"rollback_failure", 2, capture, failed_apply, failed_rollback
)
_expect(not quarantined_result.is_success() and quarantined_result.code == CommandResult.Code.TRANSACTION_CLOSED, "rollback failure closes the progression boundary")
_expect(quarantined.quarantined and quarantined.quarantine_reason == &"RANK_ROLLBACK_FAILED", "rollback failure records a durable quarantine reason")
_expect(not quarantined.update_metrics({&"reputation":30}), "quarantine blocks later metric mutation")
_expect(not quarantined.request_rank_up(&"retry_after_quarantine", 2).is_success(), "quarantine blocks retry and prevents double expansion")
func _test_callback_reentry_and_internal_tamper() -> void:
var service := _eligible_rank_two_service()
var external_rank := 1
var observations := {&"metric_update":true, &"nested":null}
var capture := func(_current: int, _target: int) -> Dictionary:
return {&"ok":true, &"snapshot":external_rank}
var reentrant_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
observations[&"metric_update"] = service.update_metrics({&"reputation":99})
observations[&"nested"] = service.request_rank_up(&"nested_rank_request", 2)
external_rank = target
return {&"ok":true}
var rollback := func(snapshot: int) -> Dictionary:
external_rank = snapshot
return {&"ok":true}
var result := service.request_rank_up(&"guarded_rank_request", 2, capture, reentrant_apply, rollback)
_expect(result.is_success(), "blocked reentry cannot prevent an otherwise valid outer promotion")
_expect(not bool(observations[&"metric_update"]), "rank callback cannot mutate eligibility metrics reentrantly")
var nested := observations[&"nested"] as CommandResult
_expect(nested != null and not nested.is_success() and nested.code == CommandResult.Code.TRANSACTION_CLOSED, "nested promotion is rejected while the outer transaction is active")
_expect(service.metrics[&"reputation"] == 25 and service.current_rank == 2, "blocked callback reentry leaves one exact promotion and original evidence")
_expect(service.create_snapshot()[&"processed_requests"].size() == 1, "reentry guard creates no hidden second idempotency record")
var tampered := _eligible_rank_two_service()
external_rank = 1
var tampering_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
external_rank = target
tampered.metrics[&"reputation"] = 99
return {&"ok":true}
result = tampered.request_rank_up(&"tampered_apply", 2, capture, tampering_apply, rollback)
_expect(not result.is_success() and result.code == CommandResult.Code.TRANSACTION_APPLY_FAILED, "direct internal mutation by an apply callback invalidates the transaction")
_expect(tampered.current_rank == 1 and tampered.metrics[&"reputation"] == 25 and external_rank == 1, "tampered apply rolls both internal and external state back exactly")
_expect(not tampered.quarantined, "verified rollback after callback tamper keeps later promotion available")
var rollback_tampered := _eligible_rank_two_service()
external_rank = 1
var failed_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
external_rank = target
return {&"ok":false}
var mutating_rollback := func(snapshot: int) -> Dictionary:
external_rank = snapshot
rollback_tampered.current_rank = 7
return {&"ok":true}
result = rollback_tampered.request_rank_up(&"tampered_rollback", 2, capture, failed_apply, mutating_rollback)
_expect(not result.is_success() and rollback_tampered.current_rank == 1, "internal state is restored even when a successful rollback callback tampers with it")
_expect(external_rank == 1 and not rollback_tampered.quarantined, "successful external rollback remains authoritative after internal tamper repair")
var valid_capture_tampered := _eligible_rank_two_service()
var callback_counts := {&"apply":0, &"rollback":0}
var mutating_capture := func(_current: int, _target: int) -> Dictionary:
valid_capture_tampered.metrics[&"reputation"] = 99
return {&"ok":true, &"snapshot":1}
var counted_apply := func(_target: int, _bounds: Dictionary) -> Dictionary:
callback_counts[&"apply"] += 1
return {&"ok":true}
var counted_rollback := func(_snapshot: int) -> Dictionary:
callback_counts[&"rollback"] += 1
return {&"ok":true}
result = valid_capture_tampered.request_rank_up(&"valid_capture_tamper", 2, mutating_capture, counted_apply, counted_rollback)
_expect(not result.is_success() and callback_counts[&"apply"] == 0, "valid-looking capture tamper is detected before external apply")
_expect(callback_counts[&"rollback"] == 1 and valid_capture_tampered.metrics[&"reputation"] == 25, "capture tamper invokes one rollback and restores internal eligibility")
var capture_tampered := _eligible_rank_two_service()
var bad_capture := func(_current: int, _target: int) -> Dictionary:
capture_tampered.current_rank = 7
return {&"ok":false}
result = capture_tampered.request_rank_up(&"tampered_capture", 2, bad_capture, reentrant_apply, rollback)
_expect(not result.is_success() and result.code == CommandResult.Code.TRANSACTION_CLOSED, "invalid capture that mutates progression enters quarantine")
_expect(capture_tampered.current_rank == 1 and capture_tampered.quarantine_reason == &"RANK_CALLBACK_TAMPERED", "capture tamper restores the last valid rank and records the uncertainty reason")
func _test_snapshot_round_trip_and_atomic_rejection() -> void:
var source := _eligible_rank_two_service()
source.complete_quest(&"important_rank_5")
source.request_rank_up(&"snapshot_rank_2", 2)
var snapshot := source.create_snapshot()
var restored := ProgressionType.new()
_expect(restored.restore_snapshot(snapshot), "valid progression snapshot restores")
_expect(restored.create_snapshot() == snapshot, "progression snapshot round trip is exact")
var json_snapshot: Variant = JSON.parse_string(JSON.stringify(snapshot))
var json_restored := ProgressionType.new()
var json_ok := json_snapshot is Dictionary and json_restored.restore_snapshot(json_snapshot)
_expect(json_ok, "JSON-decoded string-key snapshot restores through the same strict boundary (%s)" % json_restored.last_error)
_expect(json_restored.create_snapshot() == snapshot, "JSON transport preserves all promotion evidence exactly")
var before := restored.create_snapshot()
var missing_metric := snapshot.duplicate(true)
missing_metric[&"metrics"].erase("asset_gold")
_expect(not restored.restore_snapshot(missing_metric), "snapshot missing one progression metric is rejected")
_expect(restored.create_snapshot() == before, "missing-metric rejection is atomic")
var bad_fingerprint := snapshot.duplicate(true)
bad_fingerprint[&"processed_requests"][0][&"fingerprint"] = "rank:7"
_expect(not restored.restore_snapshot(bad_fingerprint), "request fingerprint inconsistent with recorded rank is rejected")
_expect(restored.create_snapshot() == before, "fingerprint rejection preserves live progression")
var bad_bounds := snapshot.duplicate(true)
bad_bounds[&"processed_requests"][0][&"details"][&"bounds"][&"max_x"] = 999
_expect(not restored.restore_snapshot(bad_bounds), "derived rank bounds cannot be tampered in a snapshot")
_expect(restored.create_snapshot() == before, "derived-bounds rejection is atomic")
var bad_quarantine := snapshot.duplicate(true)
bad_quarantine[&"quarantined"] = true
bad_quarantine[&"quarantine_reason"] = ""
_expect(not restored.restore_snapshot(bad_quarantine), "quarantine boolean requires a non-empty reason")
_expect(restored.create_snapshot() == before, "quarantine mismatch rejection is atomic")
var string_rank := snapshot.duplicate(true)
string_rank[&"current_rank"] = "2"
_expect(not restored.restore_snapshot(string_rank), "string rank is not coerced during restore")
_expect(restored.create_snapshot() == before, "type rejection preserves live progression")
var string_version := snapshot.duplicate(true)
string_version[&"version"] = "1"
_expect(not restored.restore_snapshot(string_version), "string schema version is not coerced during restore")
_expect(restored.create_snapshot() == before, "schema type rejection is atomic")
var unknown_root := snapshot.duplicate(true)
unknown_root[&"unexpected"] = true
_expect(not restored.restore_snapshot(unknown_root), "unknown root snapshot field is rejected")
_expect(restored.create_snapshot() == before, "unknown-field rejection preserves live progression")
var duplicate_rank_history := snapshot.duplicate(true)
var duplicate_record: Dictionary = duplicate_rank_history[&"processed_requests"][0].duplicate(true)
duplicate_record[&"request_id"] = "snapshot_rank_2_b"
duplicate_record[&"details"][&"request_id"] = &"snapshot_rank_2_b"
duplicate_rank_history[&"processed_requests"].append(duplicate_record)
_expect(not restored.restore_snapshot(duplicate_rank_history), "two successful request IDs cannot claim the same one-time rank transition")
_expect(restored.create_snapshot() == before, "duplicate-transition rejection is atomic")
func _test_promotion_evidence_and_legacy_migration() -> void:
var source := _eligible_rank_two_service()
source.request_rank_up(&"evidence_rank_2", 2)
var snapshot := source.create_snapshot()
var evidence: Dictionary = snapshot[&"processed_requests"][0][&"details"][&"eligibility"]
_expect(snapshot[&"version"] == 2, "promotion evidence is stored in Progression Snapshot 2")
_expect(evidence[&"eligible"] and evidence[&"current_rank"] == 1 and evidence[&"target_rank"] == 2, "saved eligibility header binds the exact one-step transition")
_expect(_requirement_ids(evidence) == [&"reputation", &"asset_gold"], "saved eligibility retains deterministic condition order")
var restored := ProgressionType.new()
_expect(restored.configure(), "evidence rejection target starts from a known valid state")
var before := restored.create_snapshot()
var below_threshold := snapshot.duplicate(true)
below_threshold[&"processed_requests"][0][&"details"][&"eligibility"][&"requirements"][0][&"current"] = 24
_expect(not restored.restore_snapshot(below_threshold), "promotion history below its threshold is rejected")
_expect(restored.create_snapshot() == before, "invalid promotion evidence rejection is atomic")
var changed_requirement := snapshot.duplicate(true)
changed_requirement[&"processed_requests"][0][&"details"][&"eligibility"][&"requirements"][0][&"required"] = 24
_expect(not restored.restore_snapshot(changed_requirement), "saved threshold definitions cannot be weakened")
_expect(restored.create_snapshot() == before, "threshold tamper preserves the live progression")
var missing_history := snapshot.duplicate(true)
missing_history[&"processed_requests"] = []
_expect(not restored.restore_snapshot(missing_history), "Rank 2 cannot be restored without its Rank 2 promotion record")
_expect(restored.create_snapshot() == before, "incomplete-history rejection is atomic")
var rank_three := _rank_three_service()
var lifetime_tamper := rank_three.create_snapshot()
lifetime_tamper[&"metrics"]["faith_lifetime"] = 99
_expect(not restored.restore_snapshot(lifetime_tamper), "current lifetime Faith cannot fall below its saved Rank 3 evidence")
_expect(restored.create_snapshot() == before, "lifetime-history mismatch preserves the live progression")
var rank_five := _rank_five_service()
var quest_tamper := rank_five.create_snapshot()
quest_tamper[&"completed_quest_ids"].erase("important_rank_5")
_expect(not restored.restore_snapshot(quest_tamper), "a monotonic quest used for Rank 5 cannot disappear from the snapshot")
_expect(restored.create_snapshot() == before, "quest-history mismatch is rejected atomically")
var legacy_source := ProgressionType.new()
legacy_source.configure(1, {&"reputation":10}, [&"tutorial_complete"])
var legacy_rank_one := legacy_source.create_snapshot()
legacy_rank_one[&"version"] = 1
_expect(restored.restore_snapshot(legacy_rank_one), "unambiguous legacy Rank 1 snapshot migrates safely")
_expect(restored.create_snapshot()[&"version"] == 2 and restored.current_rank == 1, "legacy restore is immediately re-emitted as Snapshot 2")
var legacy_progressed := snapshot.duplicate(true)
legacy_progressed[&"version"] = 1
legacy_progressed[&"processed_requests"][0][&"details"].erase(&"eligibility")
var migrated_before := restored.create_snapshot()
_expect(not restored.restore_snapshot(legacy_progressed), "legacy progressed snapshot without condition evidence is rejected as ambiguous")
_expect(restored.create_snapshot() == migrated_before, "ambiguous legacy rejection preserves the migrated Rank 1 state")
func _test_input_and_history_bounds() -> void:
var service := ProgressionType.new()
_expect(not service.configure(2), "configuration cannot bypass the one-rank-at-a-time promotion boundary")
_expect(service.configure(), "valid Rank 1 configuration remains available after rejection")
_expect(not service.update_metrics({&"asset_gold":ProgressionType.MAX_SAFE_INTEGER + 1}), "metrics above the JSON-safe integer range are rejected")
_expect(not service.update_metrics({&"asset_gold":INF}), "non-finite metrics are rejected")
var oversized_quests: Array = []
for index in ProgressionType.MAX_COMPLETED_QUESTS + 1:
oversized_quests.append("quest_%03d" % index)
_expect(not service.configure(1, {}, oversized_quests), "oversized initial quest input is rejected before per-entry work")
service = _eligible_rank_two_service()
var invalid_id := service.request_rank_up(&"rank request with spaces", 2)
_expect(not invalid_id.is_success() and invalid_id.code == CommandResult.Code.INVALID_PAYLOAD, "idempotency keys must use stable lowercase IDs")
_expect(service.current_rank == 1, "invalid idempotency key cannot mutate rank")
var impossible := service.create_snapshot()
impossible[&"current_rank"] = 7
var before := service.create_snapshot()
_expect(not service.restore_snapshot(impossible), "fabricated high rank without contiguous promotion history is rejected")
_expect(service.create_snapshot() == before, "fabricated-rank rejection is atomic")
func _eligible_rank_two_service() -> ProgressionService:
var service := ProgressionType.new()
service.configure(1, {&"reputation":25, &"asset_gold":10_000})
return service
func _rank_three_service() -> ProgressionService:
var service := _eligible_rank_two_service()
service.request_rank_up(&"history_rank_2", 2)
service.update_metrics({&"faith_lifetime":100, &"guest_lifetime":300})
service.request_rank_up(&"history_rank_3", 3)
return service
func _rank_five_service() -> ProgressionService:
var service := _rank_three_service()
service.update_metrics({
&"reputation":45,
&"asset_gold":50_000,
&"active_above_ground_facilities":1,
&"active_underground_facilities":1,
})
service.request_rank_up(&"history_rank_4", 4)
service.update_metrics({&"faith_lifetime":500})
service.complete_quest(&"important_rank_5")
service.request_rank_up(&"history_rank_5", 5)
return service
func _requirement_ids(report: Dictionary) -> Array[StringName]:
var result: Array[StringName] = []
for requirement: Dictionary in report[&"requirements"]:
result.append(StringName(requirement[&"id"]))
return result
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://dt1eshc6c6rrf
@@ -0,0 +1,500 @@
extends SceneTree
class DenyingOperationsCoordinator:
extends OperationsCoordinator
func can_start_construction_or_investment() -> Dictionary:
return {
&"allowed": false,
&"reason": &"ARREARS_REMAIN",
&"arrears_total": 25,
&"consecutive_insolvency_count": 1,
}
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
var _direct_command_id := 900_000
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("MVP: Runtime content, hiring, and progression integration")
_test_catalog_bootstrap_and_queries()
_test_progression_resource_and_guest_facts()
_test_catalog_staff_hire_transaction()
_test_rank_two_three_and_atomic_failure()
_test_v3_migration_and_v4_tamper_guards()
_test_twenty_four_hour_replay()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime(seed := 82_001) -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
_clock.set_paused(true)
_expect(bool(report.get(&"valid", false)), "Runtime initializes with validated content and Progression")
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_catalog_bootstrap_and_queries() -> void:
_reset_runtime(82_001)
var report: Dictionary = _runtime.get_mvp_content_report()
_expect(bool(report[&"valid"]), "MVP catalog report is valid")
_expect(int(report[&"counts"]["guest_classes"]) == 8, "catalog exposes eight Guest classes")
_expect(int(report[&"counts"]["species"]) == 4, "catalog exposes four independent species")
_expect(int(report[&"counts"]["staff_candidates"]) == 20, "catalog exposes twenty fixed Staff candidates")
_expect(int(report[&"counts"]["unique_characters"]) == 2, "catalog exposes both unique characters")
var catalog_ids: Array[StringName] = _runtime.get_mvp_guest_definition_ids()
_expect(catalog_ids.size() == 32, "Runtime registers every class/species Guest definition")
_expect(_runtime.guest_service.get_definitions().size() == 35, "legacy three Guest definitions coexist with the 32 catalog definitions")
var sorted_ids := catalog_ids.duplicate()
sorted_ids.sort_custom(func(left: StringName, right: StringName) -> bool: return String(left) < String(right))
_expect(catalog_ids == sorted_ids, "Runtime catalog Guest IDs use stable canonical order")
var all_registered := true
for guest_type_id: StringName in catalog_ids:
if _runtime.guest_service.get_definition(guest_type_id) == null:
all_registered = false
break
_expect(all_registered, "all 32 catalog Guest definitions are queryable by stable ID")
report[&"counts"]["guest_classes"] = 0
_expect(int(_runtime.get_mvp_content_report()[&"counts"]["guest_classes"]) == 8, "content report query is a defensive copy")
var candidate: Dictionary = _runtime.get_mvp_staff_candidate_payload(&"staff_akane")
(candidate[&"options"][&"skills"] as Dictionary)[&"service"] = -1
_expect(
int(_runtime.get_mvp_staff_candidate_payload(&"staff_akane")[&"options"][&"skills"][&"service"]) == 68,
"Staff candidate payload query is a defensive copy"
)
var progression_report: Dictionary = _runtime.get_progression_report()
(progression_report[&"snapshot"][&"metrics"] as Dictionary)["asset_gold"] = 0
_expect(int(_runtime.progression_service.metrics[&"asset_gold"]) == 5_000, "Progression report query is a defensive copy")
func _test_progression_resource_and_guest_facts() -> void:
_reset_runtime(82_002)
var faith: CommandResult = _runtime.submit_command(
&"change_resources", {&"deltas": {&"faith": 40}},
&"player", &"", 50, -1, &"mvp.progression.faith"
)
_expect(faith.is_success(), "positive Faith resource change succeeds")
_expect(_state.get_resource(&"faith") == 40, "positive Faith changes live GameState")
_expect(int(_runtime.progression_service.metrics[&"faith_lifetime"]) == 40, "positive Faith increments lifetime progression atomically")
var spawned: CommandResult = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"catalog_guest_one",
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
&"gold": 50,
&"options": {&"current_cell": Vector3i.ZERO},
},
&"world", &"catalog_guest_one", 40, -1, &"mvp.progression.guest_one"
)
_expect(spawned.is_success(), "catalog Guest can spawn through Runtime")
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 1, "successful Guest spawn increments lifetime Guest count once")
var duplicate: CommandResult = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"catalog_guest_one",
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
&"options": {&"current_cell": Vector3i.ZERO},
},
&"world", &"catalog_guest_one", 40, -1, &"mvp.progression.guest_duplicate"
)
_expect(not duplicate.is_success(), "duplicate Guest spawn is rejected")
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 1, "failed Guest spawn cannot increment lifetime progression")
_expect(
_runtime.progression_service.update_metrics({&"faith_lifetime": ProgressionService.MAX_SAFE_INTEGER}),
"overflow rollback fixture reaches the maximum lifetime Faith"
)
var state_before: Dictionary = _state.create_snapshot()
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
var progression_before: Dictionary = _runtime.progression_service.create_snapshot()
var overflow: CommandResult = _runtime._execute_change_resources(_direct_command(
&"change_resources", {&"deltas": {&"faith": 1}}
))
_expect(not overflow.is_success(), "lifetime Faith overflow rejects the resource command")
_expect(_state.create_snapshot() == state_before, "failed lifetime update restores GameState exactly")
_expect(_runtime.economy_service.create_snapshot() == economy_before, "failed lifetime update restores Economy exactly")
_expect(_runtime.progression_service.create_snapshot() == progression_before, "failed lifetime update restores Progression exactly")
func _test_catalog_staff_hire_transaction() -> void:
_reset_runtime(82_003)
var bypass: CommandResult = _runtime.submit_command(
&"register_staff",
{
&"staff_id": &"staff_akane",
&"role_id": &"guard",
&"options": {&"current_cell": Vector3i.ZERO},
},
&"player", &"staff_akane", 80, -1, &"mvp.hire.bypass"
)
_expect(not bypass.is_success(), "generic Staff registration cannot bypass a reserved catalog candidate")
var cash_before: int = _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT)
var hired: CommandResult = _runtime.submit_command(
&"hire_staff_candidate",
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
&"player", &"staff_akane", 80, -1, &"mvp.hire.akane"
)
_expect(hired.is_success(), "validated Staff candidate hires atomically")
var staff: StaffRuntime = _runtime.staff_service.get_staff(&"staff_akane")
var shift: ShiftInstance = _runtime.staff_service.get_shift(StringName(hired.details[&"shift_id"]))
_expect(staff != null and staff.role_id == &"receptionist", "hire uses the catalog role as the sole source of truth")
_expect(staff != null and staff.daily_wage == 120, "FREE_HIRE retains its catalog daily wage")
_expect(staff != null and staff.get_employment_kind_name() == &"FREE_HIRE", "ordinary catalog hire records explicit FREE_HIRE employment")
_expect(staff != null and int(staff.skills[&"service"]) == 68, "hire copies catalog skills without caller inference")
_expect(staff != null and staff.permitted_task_tags == [&"reception", &"service"], "hire copies canonical permitted Task tags")
_expect(shift != null and shift.template_id == &"DAY", "hire creates the candidate's preferred Shift")
_expect(_runtime.staff_service.get_daily_payroll_total() == 120, "hired candidate participates in deterministic daily payroll")
_expect(
_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == cash_before - 180,
"signing cost is paid exactly once through the Gold ledger"
)
_expect(_state.get_resource(&"gold") == cash_before - 180, "ledger signing cost synchronizes GameState Gold")
_expect(_runtime.path_service.has_actor_projection(&"staff:staff_akane"), "hired Staff is projected into Path traffic")
_expect(_runtime.get_hired_staff_candidate_ids() == [&"staff_akane"], "Runtime records the candidate as permanently hired")
var saw_hire_event := false
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == &"STAFF_CANDIDATE_HIRED" and &"staff_akane" in event.entity_ids:
saw_hire_event = true
_expect(saw_hire_event, "successful hire publishes a structured DomainEvent")
var replay_cash: int = _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT)
var replay: CommandResult = _runtime.submit_command(
&"hire_staff_candidate",
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
&"player", &"staff_akane", 80, -1, &"mvp.hire.akane"
)
_expect(not replay.is_success() and replay.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, "resent hire command is recognized by its idempotency key")
_expect(_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == replay_cash, "resent hire never charges the signing cost twice")
var duplicate: CommandResult = _runtime.submit_command(
&"hire_staff_candidate",
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
&"player", &"staff_akane", 80, -1, &"mvp.hire.akane.other"
)
_expect(not duplicate.is_success(), "same candidate cannot be hired under a different request")
_expect(_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == replay_cash, "duplicate-candidate rejection spends no Gold")
var original_coordinator: Variant = _runtime.operations_coordinator
var original_operations_enabled: bool = bool(_runtime._operations_enabled)
var staff_before_gate: Dictionary = _runtime.staff_service.create_snapshot()
var economy_before_gate: Dictionary = _runtime.economy_service.create_snapshot()
var hired_before_gate: Array[StringName] = _runtime.get_hired_staff_candidate_ids()
_runtime.operations_coordinator = DenyingOperationsCoordinator.new()
_runtime._operations_enabled = true
var gated: CommandResult = _runtime._execute_hire_staff_candidate(_direct_command(
&"hire_staff_candidate",
{&"candidate_id": &"staff_tessa", &"current_cell": Vector3i.ZERO}
))
_runtime.operations_coordinator = original_coordinator
_runtime._operations_enabled = original_operations_enabled
_expect(
not gated.is_success()
and StringName(gated.details.get(&"reason", &"")) == &"ARREARS_REMAIN",
"Operations arrears gate rejects a new wage-bearing Staff contract"
)
_expect(
_runtime.staff_service.create_snapshot() == staff_before_gate
and _runtime.economy_service.create_snapshot() == economy_before_gate
and _runtime.get_hired_staff_candidate_ids() == hired_before_gate,
"rejected Operations-gated hire spends nothing and mutates no Staff history"
)
var staff_before: Dictionary = _runtime.staff_service.create_snapshot()
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
var game_state_before: Dictionary = _state.create_snapshot()
var path_before: Dictionary = _runtime.path_service.create_snapshot()
var credits_before: Dictionary = _runtime._movement_credits.duplicate(true)
var events_before: Dictionary = _runtime.domain_events.create_snapshot()
var ids_before: Dictionary = _runtime.id_generator.create_snapshot()
var hired_ids_before: Array[StringName] = _runtime.get_hired_staff_candidate_ids()
var callback := Callable(_runtime, "_on_staff_registered")
_runtime.staff_service.staff_registered.disconnect(callback)
var forced_failure: CommandResult = _runtime._execute_hire_staff_candidate(_direct_command(
&"hire_staff_candidate",
{&"candidate_id": &"staff_bardo", &"current_cell": Vector3i.ZERO}
))
_runtime.staff_service.staff_registered.connect(callback)
_expect(not forced_failure.is_success(), "missing Path projection forces a post-payment hire failure")
_expect(_runtime.staff_service.create_snapshot() == staff_before, "failed hire restores Staff and Shift state exactly")
_expect(_runtime.economy_service.create_snapshot() == economy_before, "failed hire restores the ledger exactly")
_expect(_state.create_snapshot() == game_state_before, "failed hire restores GameState exactly")
_expect(_runtime.path_service.create_snapshot() == path_before, "failed hire restores Path exactly")
_expect(_runtime._movement_credits == credits_before, "failed hire restores movement credits exactly")
_expect(_runtime.domain_events.create_snapshot() == events_before, "failed hire restores DomainEvents exactly")
_expect(_runtime.id_generator.create_snapshot() == ids_before, "failed hire restores stable ID counters exactly")
_expect(_runtime.get_hired_staff_candidate_ids() == hired_ids_before, "failed hire leaves candidate history unchanged")
var canonical_save: Dictionary = _runtime.create_snapshot()
var wage_tamper: Dictionary = canonical_save.duplicate(true)
for raw_staff: Dictionary in wage_tamper[&"systems"][&"runtime"][&"staff"][&"staff"]:
if String(raw_staff[&"staff_id"]) == "staff_akane":
raw_staff[&"daily_wage"] = 0
break
var live_before: Dictionary = _runtime.create_snapshot()
_expect(not _runtime.restore_snapshot(wage_tamper), "v4 restore rejects a hired candidate whose catalog wage was altered")
_expect(_runtime.create_snapshot() == live_before, "catalog Staff crosslink rejection is atomic")
func _test_rank_two_three_and_atomic_failure() -> void:
_reset_runtime(82_004)
var rank_before: Dictionary = _runtime.progression_service.create_snapshot()
var world_before: Dictionary = _runtime.world_service.create_snapshot()
var unmet: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two_unmet"}
))
_expect(not unmet.is_success(), "Rank 2 rejects unmet reputation and asset requirements")
_expect(_runtime.progression_service.create_snapshot() == rank_before, "failed eligibility check leaves Progression exact")
_expect(_runtime.world_service.create_snapshot() == world_before, "failed eligibility check leaves World exact")
_expect(_runtime._execute_update_progression_metrics(_direct_command(
&"update_progression_metrics", {&"metrics": {&"reputation": 25}}
)).is_success(), "Rank 2 reputation fact updates")
_expect(_runtime._execute_change_resources(_direct_command(
&"change_resources", {&"deltas": {&"gold": 5_000}}
)).is_success(), "Rank 2 asset threshold is funded")
var promoted_two: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two"}
))
_expect(promoted_two.is_success(), "eligible Rank 2 promotion succeeds")
_expect(_runtime.progression_service.current_rank == 2 and _state.world_tree_rank == 2, "Progression and GameState commit Rank 2 together")
_expect(_runtime.world_service.get_bounds() == ProgressionService.RANK_BOUNDS[2], "Rank 2 commits canonical World bounds")
var replay_before: Dictionary = _runtime.create_snapshot()
var replay: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two"}
))
_expect(replay.is_success() and replay.is_replay, "same rank request ID replays the original success")
_expect(_runtime.create_snapshot() == replay_before, "rank request replay is completely read-only")
var skip: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
&"set_world_tree_rank", {&"rank": 4, &"request_id": &"rank_four_skip"}
))
_expect(not skip.is_success() and _runtime.progression_service.current_rank == 2, "rank progression cannot skip Rank 3")
var rank_three_guests_ok := true
var rank_guest_definition: StringName = _runtime.get_mvp_guest_definition_ids()[0]
for index in range(300):
var guest_id := StringName("rank_three_guest_%03d" % index)
var spawned: CommandResult = _runtime._execute_spawn_guest(_direct_command(
&"spawn_guest",
{
&"guest_id": guest_id,
&"guest_type_id": rank_guest_definition,
&"gold": 0,
&"options": {
&"current_cell": Vector3i.ZERO,
&"planned_exit_at": 10_000,
},
}
))
rank_three_guests_ok = rank_three_guests_ok and spawned.is_success()
var lifetime_updated: CommandResult = _runtime._execute_update_progression_metrics(_direct_command(
&"update_progression_metrics",
{&"metrics": {&"faith_lifetime": 100}}
))
_expect(
rank_three_guests_ok and lifetime_updated.is_success() \
and int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 300,
"Rank 3 lifetime facts derive from 300 authoritative Guest records"
)
var promoted_three: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
&"set_world_tree_rank", {&"rank": 3, &"request_id": &"rank_three"}
))
_expect(promoted_three.is_success(), "eligible Rank 3 promotion succeeds")
_expect(_runtime.progression_service.current_rank == 3 and _state.world_tree_rank == 3, "Progression and GameState commit Rank 3 together")
var saw_rank_event := false
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == &"WORLD_TREE_RANK_ADVANCED" and int(event.payload.get(&"rank", 0)) == 3:
saw_rank_event = true
_expect(saw_rank_event, "successful promotion publishes WORLD_TREE_RANK_ADVANCED")
_reset_runtime(82_005)
_runtime.progression_service.update_metrics({&"reputation": 25})
_runtime._execute_change_resources(_direct_command(&"change_resources", {&"deltas": {&"gold": 5_000}}))
_expect(_runtime._expand_world_to_rank(3), "rank apply-failure fixture expands World and Path coherently")
var atomic_before := {
&"progression": _runtime.progression_service.create_snapshot(),
&"world": _runtime.world_service.create_snapshot(),
&"path": _runtime.path_service.create_snapshot(),
&"state": _state.create_snapshot(),
&"credits": _runtime._movement_credits.duplicate(true),
&"events": _runtime.domain_events.create_snapshot(),
&"ids": _runtime.id_generator.create_snapshot(),
}
var apply_failure: CommandResult = _runtime._execute_set_world_tree_rank(_direct_command(
&"set_world_tree_rank", {&"rank": 2, &"request_id": &"rank_two_apply_failure"}
))
_expect(not apply_failure.is_success(), "World/Path apply failure rejects promotion")
_expect(_runtime.progression_service.create_snapshot() == atomic_before[&"progression"], "failed rank apply restores Progression exactly")
_expect(_runtime.world_service.create_snapshot() == atomic_before[&"world"], "failed rank apply restores World exactly")
_expect(_runtime.path_service.create_snapshot() == atomic_before[&"path"], "failed rank apply restores Path exactly")
_expect(_state.create_snapshot() == atomic_before[&"state"], "failed rank apply restores GameState exactly")
_expect(_runtime._movement_credits == atomic_before[&"credits"], "failed rank apply restores movement credits exactly")
_expect(_runtime.domain_events.create_snapshot() == atomic_before[&"events"], "failed rank apply restores DomainEvents exactly")
_expect(_runtime.id_generator.create_snapshot() == atomic_before[&"ids"], "failed rank apply restores stable IDs exactly")
func _test_v3_migration_and_v4_tamper_guards() -> void:
_reset_runtime(82_006)
_runtime._execute_change_resources(_direct_command(&"change_resources", {&"deltas": {&"faith": 12}}))
for index in range(2):
var guest_id := StringName("legacy_guest_%d" % index)
var spawned: CommandResult = _runtime._execute_spawn_guest(_direct_command(
&"spawn_guest",
{
&"guest_id": guest_id,
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[index],
&"gold": 0,
&"options": {&"current_cell": Vector3i.ZERO},
}
))
_expect(spawned.is_success(), "legacy migration fixture Guest %d spawns" % index)
_runtime.guest_service.despawn_guest(&"legacy_guest_1", _clock.absolute_minute)
var v3: Dictionary = _runtime.create_snapshot()
var v3_runtime: Dictionary = v3[&"systems"][&"runtime"]
v3_runtime[&"runtime_schema_version"] = 3
v3_runtime.erase(&"operations")
v3_runtime.erase(&"catalog_manifest")
v3_runtime.erase(&"progression")
v3_runtime.erase(&"hired_staff_candidate_ids")
for infrastructure_key: StringName in [
&"excavation", &"spatial_subslots", &"route_projection",
&"facility_infrastructure",
]:
v3_runtime.erase(infrastructure_key)
_expect(_runtime.restore_snapshot(v3), "Runtime v3 Rank 1 snapshot migrates without guessed promotion evidence")
_expect(_runtime.progression_service.current_rank == 1, "v3 migration creates fresh Rank 1 Progression")
_expect(int(_runtime.progression_service.metrics[&"asset_gold"]) == _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT), "v3 migration derives asset Gold from the authoritative ledger")
_expect(int(_runtime.progression_service.metrics[&"faith_lifetime"]) == 12, "v3 migration derives lifetime Faith from current Faith")
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 2, "v3 migration derives lifetime Guests from active plus persistent records")
_expect(_runtime.get_mvp_guest_definition_ids().size() == 32, "v3 migration registers the current 32 catalog definitions")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 6, "v3 migration emits canonical Runtime schema v6")
var digest := SimulationDigest.compute(migrated)
_expect(_runtime.restore_snapshot(migrated), "canonical v5 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "canonical v5 restore is exact")
var progressed_legacy: Dictionary = v3.duplicate(true)
progressed_legacy[&"game_state"][&"world_tree_rank"] = 2
var live_before: Dictionary = _runtime.create_snapshot()
_expect(not _runtime.restore_snapshot(progressed_legacy), "v3 Rank 2 snapshot is rejected because promotion evidence is absent")
_expect(_runtime.create_snapshot() == live_before, "rejected progressed v3 migration is atomic")
var corruptions: Array[Dictionary] = []
var manifest: Dictionary = migrated.duplicate(true)
manifest[&"systems"][&"runtime"][&"catalog_manifest"][&"guest_definition_ids"][0] = "tampered_guest"
corruptions.append({&"label": "catalog manifest", &"snapshot": manifest})
var definition: Dictionary = migrated.duplicate(true)
var catalog_id := String(_runtime.get_mvp_guest_definition_ids()[0])
for raw_definition: Dictionary in definition[&"systems"][&"runtime"][&"guests"][&"definitions"]:
if String(raw_definition[&"guest_type_id"]) == catalog_id:
raw_definition[&"wealth_min"] = int(raw_definition[&"wealth_min"]) + 1
break
corruptions.append({&"label": "catalog Guest definition", &"snapshot": definition})
var progression: Dictionary = migrated.duplicate(true)
progression[&"systems"][&"runtime"][&"progression"][&"current_rank"] = 2
corruptions.append({&"label": "Progression history", &"snapshot": progression})
var rank: Dictionary = migrated.duplicate(true)
rank[&"game_state"][&"world_tree_rank"] = 2
corruptions.append({&"label": "GameState rank", &"snapshot": rank})
var bounds: Dictionary = migrated.duplicate(true)
bounds[&"systems"][&"runtime"][&"world"][&"bounds"][&"max_x"] = 13
corruptions.append({&"label": "World rank bounds", &"snapshot": bounds})
var hired_manifest: Dictionary = migrated.duplicate(true)
hired_manifest[&"systems"][&"runtime"][&"hired_staff_candidate_ids"] = ["not_a_candidate"]
corruptions.append({&"label": "hired candidate manifest", &"snapshot": hired_manifest})
for corruption: Dictionary in corruptions:
live_before = _runtime.create_snapshot()
_expect(
not _runtime.restore_snapshot(corruption[&"snapshot"])
and _runtime.create_snapshot() == live_before,
"%s tamper is rejected atomically" % corruption[&"label"]
)
func _test_twenty_four_hour_replay() -> void:
_reset_runtime(82_007)
var spawn: CommandResult = _runtime._execute_spawn_guest(_direct_command(
&"spawn_guest",
{
&"guest_id": &"replay_guest",
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
&"gold": 100,
&"options": {&"current_cell": Vector3i.ZERO, &"planned_exit_at": _clock.absolute_minute + 2_000},
}
))
var hire: CommandResult = _runtime._execute_hire_staff_candidate(_direct_command(
&"hire_staff_candidate", {&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO}
))
_expect(spawn.is_success() and hire.is_success(), "24-hour replay fixture creates catalog Guest and Staff")
var start: Dictionary = _runtime.create_snapshot()
var second_clock = load("res://autoload/game_clock.gd").new()
var second_state = load("res://autoload/game_state.gd").new()
var second_runtime = load("res://autoload/simulation_runtime.gd").new()
second_clock.reset(1, 6, 0)
second_state.reset()
second_runtime.initialize(second_clock, second_state, 82_007)
second_clock.set_paused(true)
_expect(second_runtime.restore_snapshot(start), "24-hour replay clone restores exact v4 start")
_clock.set_paused(false)
second_clock.set_paused(false)
_clock.advance_minutes(1_440)
second_clock.advance_minutes(1_440)
_expect(
SimulationDigest.compute(_runtime.create_snapshot()) \
== SimulationDigest.compute(second_runtime.create_snapshot()),
"catalog, Staff hire, Progression, and six-Need state replay exactly for 24 hours"
)
second_runtime.free()
second_clock.free()
second_state.free()
func _direct_command(command_type: StringName, payload: Dictionary) -> SimulationCommand:
_direct_command_id += 1
return SimulationCommand.new(
_direct_command_id,
_clock.absolute_minute,
_clock.absolute_minute,
100,
command_type,
&"test",
&"",
payload,
&""
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://b46fufnam07fa
@@ -0,0 +1,484 @@
extends SceneTree
const FOURTEEN_DAYS_MINUTES := 14 * SimulationKernel.MINUTES_PER_DAY
const RUNTIME_SNAPSHOT_VERSION := 6
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("MVP: production Operations acceptance")
_test_waiting_choice_and_command_exactly_once()
_test_twenty_four_hour_actual_tick_smoke()
_test_full_partial_payroll_bankruptcy_and_v6_tamper()
_test_fourteen_day_boundary_snapshot_determinism()
await process_frame
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, enable_operations := true) -> 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")
var initial_snapshot: Dictionary = runtime.create_snapshot()
_expect(
int(initial_snapshot[&"systems"][&"runtime"].get(
&"runtime_schema_version", -1
)) == RUNTIME_SNAPSHOT_VERSION,
"fixture emits Runtime snapshot schema v6"
)
if enable_operations:
var enabled: Dictionary = runtime.enable_production_operations()
_expect(bool(enabled.get(&"ok", false)), "fixture enables production Operations")
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()
# Long acceptance slices deliberately exercise the production Operations
# adapter without claiming to execute unrelated actor AI on all 20,160 minutes.
# Aggregate clocks are aligned solely to preserve the Runtime transaction
# precondition; the separate 24-hour smoke above covers the actual Tick pipeline.
# One final rich snapshot (and the midpoint restore) is the Operations oracle.
func _advance_operations_boundary_slice(fixture: Dictionary, amount: int) -> int:
var clock: Variant = fixture[&"clock"]
var runtime: Variant = fixture[&"runtime"]
var advanced := 0
while advanced < amount:
if runtime.operations_coordinator != null \
and runtime.operations_coordinator.is_terminal():
break
# Operations has no recurring boundary finer than 30 minutes. The
# coordinator still performs its own unthinned internal minute traversal;
# unrelated Runtime AI is intentionally outside this accelerated oracle.
var step := mini(30, amount - advanced)
var target: int = clock.absolute_minute + step
clock.absolute_minute = target
clock.call("_sync_calendar_fields")
clock.snapshot_version += step
runtime.call("_advance_operations_time", target)
if bool(runtime.get_operations_report().get(&"runtime_failure_latched", false)):
break
if runtime.operations_coordinator.is_terminal():
# A terminal daily close commits TIME_BOUNDARIES and deliberately skips
# the Runtime suffix; its other aggregates therefore remain at X - 1.
_align_operations_fixture_clocks(runtime, target - 1)
advanced += step
break
# These acceptance fixtures contain no Facilities, logistics, raids, or
# Staff tasks. Their clocks are aligned for Operations cross-link checks;
# their skipped actor/Shift behavior is explicitly outside this fast oracle.
_align_operations_fixture_clocks(runtime, target)
advanced += step
return advanced
func _align_operations_fixture_clocks(runtime: Variant, minute: int) -> void:
runtime.inventory_service.current_minute = minute
runtime.facility_service.current_minute = minute
runtime.guest_service.current_minute = minute
runtime.staff_service.current_minute = minute
runtime.authority_service.current_minute = minute
runtime.defense_service.current_minute = minute
runtime.construction_service.current_minute = minute
runtime.excavation_service.current_minute = minute
func _operations_slice_is_healthy(fixture: Dictionary) -> bool:
var runtime: Variant = fixture[&"runtime"]
var report: Dictionary = runtime.get_operations_report()
var foreground: EventInstance = runtime.event_service.get_foreground_instance()
return not bool(report.get(&"runtime_failure_latched", true)) \
and int(report.get(&"deferred_runtime_minute", -2)) == -1 \
and not runtime.operations_coordinator.pause_requested \
and not runtime.operations_coordinator.is_terminal() \
and not runtime.operations_coordinator.is_external_quarantined() \
and (foreground == null or foreground.state != EventInstance.State.WAITING_CHOICE)
func _test_waiting_choice_and_command_exactly_once() -> void:
var fixture := _new_fixture(73_001)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var state: Variant = fixture[&"state"]
var queued: CommandResult = runtime.event_service.enqueue_candidate(
&"relationship.threshold",
[],
clock.absolute_minute,
{&"deduplication_key": &"operations-acceptance-choice"}
)
var started: CommandResult = runtime.event_service.start_next_event(
clock.absolute_minute, {}
)
_expect(
queued.is_success() and started.is_success()
and bool(started.details.get(&"waiting_for_choice", false)),
"foreground Event enters WAITING_CHOICE"
)
clock.set_paused(false)
var before_choice_minute: int = clock.absolute_minute
clock.advance_minutes(5)
var foreground: EventInstance = runtime.event_service.get_foreground_instance()
_expect(
clock.paused and clock.absolute_minute == before_choice_minute + 1,
"WAITING_CHOICE pauses at the first committed minute and stops catch-up"
)
_expect(
foreground != null and foreground.state == EventInstance.State.WAITING_CHOICE,
"paused foreground ownership remains intact"
)
if foreground != null:
var choice_result: CommandResult = runtime.submit_command(
&"submit_event_choice",
{
&"event_instance_id": foreground.event_instance_id,
&"choice_instance_id": foreground.choice_instance_id,
&"choice_id": &"acknowledge",
},
&"player",
foreground.event_instance_id,
100,
-1,
&"operations.acceptance.choice"
)
_expect(
choice_result.is_success()
and runtime.event_service.get_foreground_instance() == null
and not clock.paused,
"submitting the stable Choice token resolves the Event and resumes time"
)
clock.set_paused(true)
var gold_before: int = state.get_resource(&"gold")
var first: CommandResult = runtime.submit_command(
&"change_resources",
{&"deltas": {&"gold": 17}},
&"player",
&"",
50,
-1,
&"operations.acceptance.exactly-once"
)
var gold_after_first: int = state.get_resource(&"gold")
var retry: CommandResult = runtime.submit_command(
&"change_resources",
{&"deltas": {&"gold": 17}},
&"player",
&"",
50,
-1,
&"operations.acceptance.exactly-once"
)
_expect(
first.is_success() and gold_after_first == gold_before + 17,
"first public command commits its resource delta once"
)
_expect(
not retry.is_success()
and retry.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY
and state.get_resource(&"gold") == gold_after_first,
"transport retry with the same idempotency key cannot apply twice"
)
var command_snapshot: Dictionary = runtime.create_snapshot()
_expect(
runtime.restore_snapshot(command_snapshot),
"command history survives an exact v6 snapshot round trip"
)
var restored_retry: CommandResult = runtime.submit_command(
&"change_resources",
{&"deltas": {&"gold": 17}},
&"player",
&"",
50,
-1,
&"operations.acceptance.exactly-once"
)
_expect(
not restored_retry.is_success()
and restored_retry.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY
and state.get_resource(&"gold") == gold_after_first,
"restored idempotency history still rejects the same command"
)
_free_fixture(fixture)
func _test_twenty_four_hour_actual_tick_smoke() -> void:
var fixture := _new_fixture(73_004)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var start_minute: int = clock.absolute_minute
clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
_expect(
clock.absolute_minute - start_minute == SimulationKernel.MINUTES_PER_DAY,
"actual SimulationKernel ticks complete an uninterrupted 24-hour smoke slice"
)
_expect(
_operations_slice_is_healthy(fixture)
and runtime.operations_coordinator.current_minute == clock.absolute_minute
and runtime.operations_coordinator.get_day_summaries().size() == 1,
"24-hour actual-tick slice has no Choice, deferred minute, quarantine, or terminal stop"
)
var snapshot: Dictionary = runtime.create_snapshot()
_expect(
runtime.restore_snapshot(snapshot)
and runtime.latest_digest == SimulationDigest.compute(snapshot),
"24-hour actual-tick v6 snapshot round trip is exact"
)
_free_fixture(fixture)
func _test_full_partial_payroll_bankruptcy_and_v6_tamper() -> void:
var fixture := _new_fixture(73_002, false)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var state: Variant = fixture[&"state"]
var candidate: Dictionary = runtime.get_mvp_staff_candidate_payload(&"staff_akane")
var daily_wage := int(candidate[&"options"][&"daily_wage"])
var hired: CommandResult = runtime.submit_command(
&"hire_staff_candidate",
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
&"player",
&"staff_akane",
80,
-1,
&"operations.acceptance.hire"
)
_expect(hired.is_success() and daily_wage > 50, "payroll fixture hires one wage-bearing Staff member")
var enabled: Dictionary = runtime.enable_production_operations()
_expect(bool(enabled.get(&"ok", false)), "payroll fixture enables Operations after the pre-opening hire")
var full_advanced := _advance_operations_boundary_slice(
fixture, SimulationKernel.MINUTES_PER_DAY
)
var full: Dictionary = runtime.staff_service.get_payroll_settlement(&"payroll-day-00000001")
_expect(
full_advanced == SimulationKernel.MINUTES_PER_DAY
and _operations_slice_is_healthy(fixture)
and clock.absolute_minute == SimulationKernel.MINUTES_PER_DAY + 6 * 60
and int(full.get(&"new_wages_total", -1)) == daily_wage
and int(full.get(&"paid_total", -1)) == daily_wage
and int(full.get(&"unpaid_after_total", -1)) == 0,
"Day 1 payroll is paid in full and closes at exactly 06:00"
)
var cash_now: int = runtime.economy_service.get_balance(runtime.PLAYER_CASH_ACCOUNT)
var drained: CommandResult = runtime.submit_command(
&"change_resources",
{&"deltas": {&"gold": -(cash_now - 50)}},
&"player",
&"",
90,
-1,
&"operations.acceptance.cash-drain"
)
_expect(
drained.is_success()
and runtime.economy_service.get_balance(runtime.PLAYER_CASH_ACCOUNT) == 50,
"fixture leaves exactly 50 Gold for a deterministic partial payroll"
)
var partial_advanced := _advance_operations_boundary_slice(
fixture, SimulationKernel.MINUTES_PER_DAY
)
var partial: Dictionary = runtime.staff_service.get_payroll_settlement(&"payroll-day-00000002")
_expect(
partial_advanced == SimulationKernel.MINUTES_PER_DAY
and _operations_slice_is_healthy(fixture)
and clock.absolute_minute == 2 * SimulationKernel.MINUTES_PER_DAY + 6 * 60
and int(partial.get(&"new_wages_total", -1)) == daily_wage
and int(partial.get(&"paid_total", -1)) == 50
and int(partial.get(&"unpaid_after_total", -1)) == daily_wage - 50,
"Day 2 payroll pays available Gold only and carries exact arrears"
)
var terminal_advanced := _advance_operations_boundary_slice(
fixture, 2 * SimulationKernel.MINUTES_PER_DAY
)
var coordinator: OperationsCoordinator = runtime.operations_coordinator
var last_three_negative := true
for day in [2, 3, 4]:
var summary: Dictionary = coordinator.get_day_summary(day)
last_three_negative = last_three_negative \
and int(summary.get(&"net_profit", 0)) < 0 \
and int((summary.get(&"settlement", {}) as Dictionary).get(
&"arrears_total", 0
)) > 0
_expect(
terminal_advanced == 2 * SimulationKernel.MINUTES_PER_DAY
and clock.absolute_minute == 4 * SimulationKernel.MINUTES_PER_DAY + 6 * 60
and last_three_negative
and coordinator.get_consecutive_insolvency_count() == 3
and coordinator.get_terminal_state() == &"BANKRUPT",
"three consecutive negative closes with arrears enter BANKRUPT at the third boundary"
)
var terminal_snapshot: Dictionary = runtime.create_snapshot()
var terminal_digest := SimulationDigest.compute(terminal_snapshot)
_expect(
runtime.restore_snapshot(terminal_snapshot),
"BANKRUPT terminal v6 snapshot restores without clearing terminal state"
)
clock.advance_minutes(180)
_expect(
clock.absolute_minute == 4 * SimulationKernel.MINUTES_PER_DAY + 6 * 60
and SimulationDigest.compute(runtime.create_snapshot()) == terminal_digest,
"BANKRUPT is terminal: later time requests are exact no-ops"
)
_expect(
not bool(runtime.acknowledge_operations_pause().get(&"ok", true)),
"BANKRUPT cannot be acknowledged away"
)
_assert_v6_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"operations"][&"personal_action_points"] = 99,
"v6 rejects an out-of-range Operations action-point total"
)
_assert_v6_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"next_command_id"] = int(
tampered[&"systems"][&"runtime"][&"next_command_id"]
) + 1,
"v6 rejects command high-water gaps"
)
_assert_v6_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
var accepted: Array = tampered[&"systems"][&"runtime"][&"command_queue"][&"accepted"]
if not accepted.is_empty():
accepted[0][&"idempotency_key"] = &"tampered.accepted.key",
"v6 rejects accepted-command/idempotency cross-link tampering"
)
_assert_v6_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
var summaries: Array = tampered[&"systems"][&"runtime"][&"operations"][&"coordinator"][&"day_summaries"]
summaries[0][&"category_totals"][&"wages"] = int(
summaries[0][&"category_totals"][&"wages"]
) + 1,
"v6 rejects DaySummary/payroll wage reconciliation tampering"
)
_assert_v6_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"unexpected_v6_field"] = true,
"v6 rejects unknown Runtime fields"
)
_assert_v6_tamper_rejected(
runtime,
terminal_snapshot,
func(tampered: Dictionary) -> void:
tampered[&"systems"][&"runtime"][&"operations"][&"coordinator"][&"unexpected_field"] = true,
"v6 rejects unknown Coordinator fields"
)
_free_fixture(fixture)
func _assert_v6_tamper_rejected(
runtime: Variant,
canonical: Dictionary,
mutator: Callable,
label: String
) -> void:
var before: Dictionary = runtime.create_snapshot()
var tampered: Dictionary = canonical.duplicate(true)
mutator.call(tampered)
var rejected: bool = not runtime.restore_snapshot(tampered)
_expect(rejected and runtime.create_snapshot() == before, label + " atomically")
func _test_fourteen_day_boundary_snapshot_determinism() -> void:
var seed := 73_003
var continuous := _new_fixture(seed)
var continuous_clock: Variant = continuous[&"clock"]
var continuous_runtime: Variant = continuous[&"runtime"]
var continuous_start: int = continuous_clock.absolute_minute
var first_advanced := _advance_operations_boundary_slice(
continuous, FOURTEEN_DAYS_MINUTES / 2
)
var midpoint: Dictionary = continuous_runtime.create_snapshot()
_expect(
first_advanced == FOURTEEN_DAYS_MINUTES / 2
and continuous_clock.absolute_minute == continuous_start + FOURTEEN_DAYS_MINUTES / 2
and _operations_slice_is_healthy(continuous),
"boundary-accelerated midpoint is captured after seven healthy days"
)
var second_advanced := _advance_operations_boundary_slice(
continuous, FOURTEEN_DAYS_MINUTES / 2
)
var continuous_snapshot: Dictionary = continuous_runtime.create_snapshot()
var continuous_digest := SimulationDigest.compute(continuous_snapshot)
_expect(
first_advanced + second_advanced == FOURTEEN_DAYS_MINUTES
and continuous_clock.absolute_minute - continuous_start == FOURTEEN_DAYS_MINUTES
and _operations_slice_is_healthy(continuous),
"continuous boundary-accelerated Operations advances exactly 14 days = 20,160 minutes"
)
_expect(
continuous_runtime.operations_coordinator.get_day_summaries().size() == 14,
"continuous boundary run commits all fourteen daily closes"
)
var resumed := _new_fixture(seed, false)
var resumed_clock: Variant = resumed[&"clock"]
var resumed_runtime: Variant = resumed[&"runtime"]
var restored: bool = resumed_runtime.restore_snapshot(midpoint)
_expect(restored, "fresh Runtime restores the enabled v6 midpoint snapshot")
var resumed_advanced := 0
if restored:
resumed_advanced = _advance_operations_boundary_slice(
resumed, FOURTEEN_DAYS_MINUTES / 2
)
var resumed_snapshot: Dictionary = resumed_runtime.create_snapshot()
_expect(
restored and resumed_advanced == FOURTEEN_DAYS_MINUTES / 2
and resumed_clock.absolute_minute == 6 * 60 + FOURTEEN_DAYS_MINUTES
and _operations_slice_is_healthy(resumed),
"restored boundary-accelerated Operations completes seven healthy days"
)
_expect(
restored and SimulationDigest.compute(resumed_snapshot) == continuous_digest
and resumed_snapshot == continuous_snapshot,
"continuous and midpoint-restored 14-day boundary runs have identical snapshot and Digest"
)
_free_fixture(continuous)
_free_fixture(resumed)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://cmfsedkuddbhg
@@ -0,0 +1,381 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("MVP: Runtime operations integration")
_reset_runtime()
_test_disabled_compatibility_and_enable()
_test_enabled_snapshot_round_trip()
_test_seed_424242_second_arrival_boundary()
_test_after_mutation_notifications_are_atomic()
_test_deferred_failure_backlog_and_exact_retry()
_test_daily_payroll_financials_and_crosslink_tamper()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime(seed: int = 20260827) -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
_expect(bool(report.get(&"valid", false)), "Runtime initializes before Operations opt-in")
_clock.set_paused(true)
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_disabled_compatibility_and_enable() -> void:
var disabled: Dictionary = _runtime.get_operations_report()
_expect(not disabled[&"enabled"], "Operations remain disabled for existing Runtime tests")
var disabled_snapshot: Dictionary = _runtime.create_snapshot()
var disabled_runtime: Dictionary = disabled_snapshot[&"systems"][&"runtime"]
_expect(int(disabled_runtime[&"runtime_schema_version"]) == 6, "Runtime snapshot advances to schema v6")
_expect(
disabled_runtime[&"operations"] == {
&"wrapper_version": 1,
&"enabled": false,
&"personal_action_points": 0,
&"runtime_failure_latched": false,
&"runtime_failure_report": {},
&"deferred_runtime_minute": -1,
&"coordinator": {},
},
"disabled Operations wrapper has one canonical shape"
)
var enabled: Dictionary = _runtime.enable_production_operations()
var enabled_ok := bool(enabled.get(&"ok", false))
_expect(enabled_ok, "Production Operations opt in succeeds only at Day 1 06:00")
if not enabled_ok:
print(" enable diagnostic: %s" % JSON.stringify(enabled, "", true, true))
return
_expect(
_runtime.operations_coordinator.current_minute == 360,
"enable processes the 06:00 boundary from minute 359 exactly once"
)
_expect(
int(_runtime.get_operations_report()[&"personal_action_points"]) == 2,
"first morning boundary restores two personal action points"
)
func _test_enabled_snapshot_round_trip() -> void:
if _runtime.operations_coordinator == null:
return
var snapshot: Dictionary = _runtime.create_snapshot()
var expected_digest := SimulationDigest.compute(snapshot)
_expect(_runtime.restore_snapshot(snapshot), "enabled Runtime v5 snapshot restores")
_expect(_runtime.latest_digest == expected_digest, "enabled Runtime v5 round trip is exact")
var operations: Dictionary = _runtime.get_operations_report()
_expect(
operations[&"enabled"] and _runtime.operations_coordinator.current_minute == _clock.absolute_minute,
"restored Coordinator clock cross-link remains exact"
)
func _test_seed_424242_second_arrival_boundary() -> void:
_reset_runtime(424242)
var enabled: Dictionary = _runtime.enable_production_operations()
_expect(bool(enabled.get(&"ok", false)), "seed 424242 enables production Operations")
if not bool(enabled.get(&"ok", false)):
return
var before_count: int = _runtime.guest_service.guest_count()
_clock.advance_minutes(30)
var report: Dictionary = _runtime.get_operations_report()
_expect(
_clock.absolute_minute == 390 \
and int(report.get(&"deferred_runtime_minute", -2)) == -1 \
and not bool(report.get(&"runtime_failure_latched", true)) \
and not _runtime.operations_coordinator.is_external_quarantined(),
"seed 424242 completes the minute 390 arrival boundary without quarantine"
)
_expect(
_runtime.operations_coordinator.get_population_count() \
== _runtime.guest_service.guest_count() \
and _runtime.guest_service.guest_count() >= before_count,
"second arrival keeps Coordinator and Guest population cross-linked"
)
func _test_after_mutation_notifications_are_atomic() -> void:
_reset_runtime(424242)
_expect(
bool(_runtime.enable_production_operations().get(&"ok", false)),
"after-mutation rollback fixture enables Operations"
)
_clock.advance_minutes(29)
var before := {
&"economy": _runtime.economy_service.create_snapshot(),
&"guests": _runtime.guest_service.create_snapshot(),
&"path": _runtime.path_service.create_snapshot(),
&"movement": _runtime._movement_credits.duplicate(true),
&"progression": _runtime.progression_service.create_snapshot(),
&"events": _runtime.domain_events.create_snapshot(),
&"ids": _runtime.id_generator.create_snapshot(),
}
var signals := {&"guest": 0, &"economy": 0, &"arrival": 0}
_runtime.guest_service.guest_spawned.connect(
func(_guest: GuestRuntime) -> void: signals[&"guest"] += 1
)
_runtime.economy_service.transaction_posted.connect(
func(_transaction_id: StringName, _entries: Array) -> void:
signals[&"economy"] += 1
)
_runtime.operations_coordinator.arrivals_committed.connect(
func(_report: Dictionary) -> void: signals[&"arrival"] += 1
)
_runtime.set_operations_test_boundary_hook(
&"ARRIVAL_PLAN",
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
return {
&"ok": phase != _runtime.OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE,
&"reason": &"FORCED_AFTER_MUTATION_FAILURE",
}
)
_clock.advance_minutes(1)
var after := {
&"economy": _runtime.economy_service.create_snapshot(),
&"guests": _runtime.guest_service.create_snapshot(),
&"path": _runtime.path_service.create_snapshot(),
&"movement": _runtime._movement_credits.duplicate(true),
&"progression": _runtime.progression_service.create_snapshot(),
&"events": _runtime.domain_events.create_snapshot(),
&"ids": _runtime.id_generator.create_snapshot(),
}
_expect(
_clock.absolute_minute == 390 and _clock.paused and before == after,
"failure after the first arrival mutation restores every external domain exactly"
)
_expect(
int(signals[&"guest"]) == 0 and int(signals[&"economy"]) == 0 \
and int(signals[&"arrival"]) == 0,
"rolled-back arrival leaks no Guest, Economy, or Coordinator success signal"
)
_runtime.set_operations_test_boundary_hook(&"ARRIVAL_PLAN", Callable())
var retried: Dictionary = _runtime.acknowledge_operations_pause()
_expect(
bool(retried.get(&"ok", false)) and int(signals[&"guest"]) > 0 \
and int(signals[&"economy"]) > 0 and int(signals[&"arrival"]) == 1,
"successful retry flushes each staged service notification after commit"
)
func _test_deferred_failure_backlog_and_exact_retry() -> void:
_reset_runtime(424242)
_expect(
bool(_runtime.enable_production_operations().get(&"ok", false)),
"deferred retry fixture enables Operations"
)
_clock.advance_minutes(29)
var scheduled: CommandResult = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"scheduled_boundary_guest",
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
&"gold": 25,
&"options": {
&"current_cell": Vector3i.ZERO,
&"planned_exit_at": 2_000,
},
},
&"test", &"scheduled_boundary_guest", 100, 390,
&"operations.test.scheduled.spawn"
)
_expect(scheduled.is_success(), "minute 390 command is queued before the boundary")
var counts := {&"guest": 0, &"arrival": 0, &"hook": 0}
_runtime.guest_service.guest_spawned.connect(
func(_guest: GuestRuntime) -> void: counts[&"guest"] += 1
)
_runtime.operations_coordinator.arrivals_committed.connect(
func(_report: Dictionary) -> void: counts[&"arrival"] += 1
)
_runtime.set_operations_test_boundary_hook(
&"ARRIVAL_PLAN",
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
counts[&"hook"] += 1
return {
&"ok": false,
&"reason": &"FORCED_PREPARE_FAILURE",
&"phase": phase,
}
)
_clock.set_speed(4)
var processed: int = _clock.process_real_delta(5.0)
var failed_report: Dictionary = _runtime.get_operations_report()
_expect(
processed == 1 and _clock.absolute_minute == 390 and _clock.paused,
"boundary failure stops an eight-tick backlog at the failed minute"
)
_expect(
int(failed_report[&"deferred_runtime_minute"]) == 390 \
and failed_report[&"runtime_failure_latched"] \
and _runtime.operations_coordinator.current_minute == 389,
"failed boundary persists one canonical deferred shape"
)
_expect(
_runtime.guest_service.has_guest(&"scheduled_boundary_guest") \
and int(counts[&"guest"]) == 1 \
and int(counts[&"arrival"]) == 0,
"pre-boundary command commits once while failed callback emits no arrival notification"
)
var interrupted: Dictionary = _runtime.create_snapshot()
_expect(
_runtime.restore_snapshot(interrupted),
"deferred v5 snapshot restores before acknowledgement"
)
_runtime.set_operations_test_boundary_hook(&"ARRIVAL_PLAN", Callable())
var retry_guest_signals := {&"count": 0}
_runtime.guest_service.guest_spawned.connect(
func(_guest: GuestRuntime) -> void: retry_guest_signals[&"count"] += 1
)
var acknowledged: Dictionary = _runtime.acknowledge_operations_pause()
_expect(
bool(acknowledged.get(&"ok", false)) \
and bool(acknowledged.get(&"suffix_replayed", false)) \
and _clock.absolute_minute == 390 \
and _runtime.operations_coordinator.current_minute == 390,
"acknowledgement retries the same boundary and suffix exactly once"
)
var completion_count := 0
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == &"COMMAND_COMPLETED" \
and event.causation_command_id == scheduled.command_id:
completion_count += 1
_expect(
completion_count == 1 \
and _runtime.command_queue.processed_count() == 1 \
and _runtime.guest_service.current_minute == 390 \
and int(retry_guest_signals[&"count"]) \
== _runtime.operations_coordinator.get_arrival_reports()[-1][&"total_count"],
"retry never replays the command and advances the skipped Runtime suffix once"
)
var completed: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(completed), "completed deferred retry round-trips exactly")
func _test_daily_payroll_financials_and_crosslink_tamper() -> void:
_reset_runtime(811_004)
var hired: CommandResult = _runtime.submit_command(
&"hire_staff_candidate",
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
&"player", &"staff_akane", 100, -1, &"operations.payroll.hire.akane"
)
_expect(hired.is_success(), "daily payroll fixture hires one catalog Staff member")
_expect(
bool(_runtime.enable_production_operations().get(&"ok", false)),
"daily payroll fixture enables Operations after hiring"
)
_clock.advance_minutes(1_440)
var summary: Dictionary = _runtime.operations_coordinator.get_day_summary(1)
var settlement: Dictionary = summary.get(&"settlement", {})
_expect(
_clock.absolute_minute == 1_800 \
and int(summary.get(&"closed_at", -1)) == 1_800 \
and int(summary[&"category_totals"][&"wages"]) == 120,
"06:00 closes Day 1 once and accrues only the new daily wage"
)
_expect(
int(settlement.get(&"paid_total", -1)) == 120 \
and int(settlement.get(&"arrears_total", -1)) == 0 \
and _runtime.staff_service.get_staff(&"staff_akane").unpaid_wages == 0,
"sufficient Gold pays the full lexical payroll with no arrears"
)
var wage_transaction: StringName = _runtime.economy_service.get_transaction_id_for_idempotency_key(
&"operations.wage.day.00000001"
)
var wage_entries: Array[LedgerEntry] = _runtime.economy_service.get_transaction_entries(wage_transaction)
_expect(
wage_entries.size() == 1 \
and wage_entries[0].category == &"WAGE" \
and wage_entries[0].absolute_minute == 1_800 \
and wage_entries[0].amount == 120,
"paid payroll posts one canonical WAGE ledger line at the closing boundary"
)
var canonical: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(canonical), "Day 1 payroll/P&L v5 snapshot round-trips")
var signing_category: Dictionary = canonical.duplicate(true)
for raw_entry: Dictionary in signing_category[&"systems"][&"runtime"][&"economy"][&"entries"]:
if String(raw_entry[&"category"]) == "STAFF_SIGNING_COST":
raw_entry[&"category"] = "TRANSFER"
break
_expect(
_rejects_atomically(signing_category),
"closed non-WAGE ledger category tamper is rejected atomically"
)
var wage_category: Dictionary = canonical.duplicate(true)
for raw_entry: Dictionary in wage_category[&"systems"][&"runtime"][&"economy"][&"entries"]:
if String(raw_entry[&"category"]) == "WAGE":
raw_entry[&"category"] = "TRANSFER"
break
_expect(
_rejects_atomically(wage_category),
"Staff settlement cannot outlive its canonical WAGE ledger evidence"
)
var lifetime: Dictionary = canonical.duplicate(true)
lifetime[&"systems"][&"runtime"][&"progression"][&"metrics"][&"guest_lifetime"] += 1
_expect(
_rejects_atomically(lifetime),
"Progression lifetime Guest inflation is rejected against active plus persistent Guests"
)
var plan_class: Dictionary = canonical.duplicate(true)
var arrival_reports: Array = plan_class[&"systems"][&"runtime"][&"operations"][&"coordinator"][&"arrival_reports"]
var changed_plan := false
for report: Dictionary in arrival_reports:
if not (report[&"plans"] as Array).is_empty():
var plan: Dictionary = report[&"plans"][0]
plan[&"guest_class"] = "noble" if String(plan[&"guest_class"]) != "noble" else "laborer"
changed_plan = true
break
_expect(
changed_plan and _rejects_atomically(plan_class),
"arrival plan class cannot diverge from its persisted Guest definition"
)
var downgraded: Dictionary = canonical.duplicate(true)
downgraded[&"systems"][&"runtime"][&"runtime_schema_version"] = 4
_expect(
_rejects_atomically(downgraded),
"relabelled enabled v5 cannot discard Operations history through v4 migration"
)
func _rejects_atomically(candidate: Dictionary) -> bool:
var before: Dictionary = _runtime.create_snapshot()
return not _runtime.restore_snapshot(candidate) \
and _runtime.create_snapshot() == before
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://dxchxjl371ti
+22
View File
@@ -0,0 +1,22 @@
# PERF-MVP-001 acceptance harness
This standalone harness composes the real World, Path, Facility, Guest, Staff,
Inventory, Logistics, Event, Economy, and performance-budget services through
their public APIs.
The default command runs a smaller 24-hour smoke tier:
```sh
godot --headless --path . --script res://tests/performance/test_perf_mvp_001.gd
```
The opt-in command runs the exact PERF-MVP-001 fixture:
```sh
PERF_MVP_FULL=1 godot --headless --path . --script res://tests/performance/test_perf_mvp_001.gd
```
Exit status `0` means the selected tier passed, `1` means a validation or
performance budget failed, and `2` means the requested fixture was incomplete.
Every run prints a machine-readable `PERF_MVP_001_REPORT=<json>` line with exact
fixture sizes, timings, backlog ages, validation results, and limitations.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://b1s2fuc7d2x4i
+76
View File
@@ -0,0 +1,76 @@
extends SceneTree
const HarnessType = preload("res://tests/performance/perf_mvp_001_harness.gd")
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var tier := &"full" if _full_tier_requested() else &"smoke"
print("PERF-MVP-001: %s tier" % String(tier))
var report: Dictionary = HarnessType.new().run(tier)
_print_summary(report)
print("PERF_MVP_001_REPORT=%s" % JSON.stringify(report))
match String(report.get("status", "INCOMPLETE")):
"PASS", "SMOKE_PASS":
quit(0)
"INCOMPLETE":
quit(2)
_:
quit(1)
func _full_tier_requested() -> bool:
var environment_value := OS.get_environment("PERF_MVP_FULL").strip_edges().to_lower()
if environment_value in ["1", "true", "yes", "full"]:
return true
for argument: String in OS.get_cmdline_user_args():
if argument in ["--full", "full"]:
return true
return false
func _print_summary(report: Dictionary) -> void:
var initial: Dictionary = report.get("actual_initial", {})
var summary: Dictionary = report.get("check_summary", {})
print(
" status=%s cells=%d guests=%d staff=%d facilities=%d queue=%d logistics=%d events=%d"
% [
report.get("status", "INCOMPLETE"),
int(initial.get("open_cells", 0)),
int(initial.get("guests", 0)),
int(initial.get("staff", 0)),
int(initial.get("facilities", 0)),
int(initial.get("queue_entries", 0)),
int(initial.get("unfinished_logistics", 0)),
int(initial.get("active_events", 0)),
]
)
if report.has("simulation"):
var simulation: Dictionary = report["simulation"]
print(
" simulated=%d/%d minutes runtime=%.3f ms"
% [
int(simulation.get("completed_minutes", 0)),
int(simulation.get("requested_minutes", 0)),
float(simulation.get("duration_ms", 0.0)),
]
)
print(
" checks=%d passed=%d failed=%d skipped=%d"
% [
int(summary.get("total", 0)),
int(summary.get("passed", 0)),
int(summary.get("failed", 0)),
int(summary.get("skipped", 0)),
]
)
for validation: Dictionary in report.get("validations", []):
if validation.get("status", "FAIL") != "PASS":
print(" %s: %s %s" % [validation["status"], validation["id"], validation["details"]])
for unmet: Dictionary in report.get("unmet_fixture_fields", []):
print(" INCOMPLETE fixture: %s target=%s actual=%s" % [unmet["field"], unmet["target"], unmet["actual"]])
for error: Dictionary in report.get("runtime_errors", []):
print(" ERROR %s: %s" % [error["code"], error["message"]])
@@ -0,0 +1 @@
uid://irdmv2oju0ts
+61
View File
@@ -0,0 +1,61 @@
extends SceneTree
var _failures := 0
var _checks := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("The Tree: automated checks")
_test_game_clock()
_test_game_state()
_test_facility_data()
load("res://tests/s0/test_kernel_and_save.gd").run(Callable(self, "_expect"))
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_game_clock() -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
_expect(clock.get_phase() == clock.Phase.MORNING, "06:00 is morning")
clock.advance_minutes(4 * 60)
_expect(clock.get_hour() == 10, "clock advances to 10:00")
_expect(clock.get_phase() == clock.Phase.DAY, "10:00 is day phase")
clock.set_speed(4)
_expect(clock.speed == 4, "clock accepts the specified x4 speed")
clock.set_speed(3)
_expect(clock.speed == 4, "clock rejects unsupported speed values")
clock.advance_minutes(14 * 60)
_expect(clock.day == 2 and clock.get_hour() == 0, "midnight starts the next day")
clock.free()
func _test_game_state() -> void:
var state = load("res://autoload/game_state.gd").new()
state.reset()
_expect(state.can_afford({&"gold": 500, &"materials": 5}), "default resources can afford starter room")
_expect(state.spend({&"gold": 500, &"materials": 5}), "spending succeeds")
_expect(state.get_resource(&"gold") == 4500, "gold is deducted atomically")
_expect(not state.spend({&"mana": 1000}), "overspending is rejected")
state.free()
func _test_facility_data() -> void:
var catalog_script = load("res://scripts/data/facility_catalog.gd")
var facilities: Array = catalog_script.load_all()
_expect(facilities.size() == 16, "MVP facility catalog loads all baseline definitions")
var seen_ids := {}
for facility in facilities:
_expect(facility.is_valid(), "%s has valid data" % facility.resource_path)
_expect(not seen_ids.has(facility.id), "%s has a unique id" % facility.id)
seen_ids[facility.id] = true
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://pd4ln0bwygun
+203
View File
@@ -0,0 +1,203 @@
extends SceneTree
const CommandType = preload("res://scripts/core/command.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
const CommandQueueType = preload("res://scripts/core/command_queue.gd")
const TransactionType = preload("res://scripts/core/state_transaction.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S0: Command Queue and State Transaction")
_test_deterministic_queue_order()
_test_idempotency_and_result_retention()
_test_snapshot_preserves_accepted_idempotency_history()
_test_legacy_snapshot_migrates_accepted_metadata()
_test_transaction_commit()
_test_validation_is_atomic()
_test_apply_failure_rolls_back()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_deterministic_queue_order() -> void:
var queue = CommandQueueType.new()
_expect(queue.enqueue(_command(3, 11, 100, &"three")).is_success(), "command 3 accepted")
_expect(queue.enqueue(_command(2, 10, 10, &"two")).is_success(), "command 2 accepted")
_expect(queue.enqueue(_command(1, 10, 10, &"one")).is_success(), "command 1 accepted")
_expect(queue.enqueue(_command(4, 10, 90, &"four")).is_success(), "command 4 accepted")
var ready: Array = queue.drain_ready(10)
var ids: Array[int] = []
for command in ready:
ids.append(command.command_id)
_expect(ids == [4, 1, 2], "ready order is tick, descending priority, command id")
_expect(queue.next_execute_tick() == 11, "future command remains queued")
func _test_idempotency_and_result_retention() -> void:
var queue = CommandQueueType.new()
var original = _command(10, 1, 0, &"same-operation")
_expect(queue.enqueue(original).is_success(), "idempotent command accepted once")
var duplicate = _command(11, 1, 0, &"same-operation")
var duplicate_result = queue.enqueue(duplicate)
_expect(
duplicate_result.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY,
"duplicate idempotency key rejected"
)
queue.drain_ready(1)
queue.complete(10, CommandResultType.succeeded(10, {"value": 42}))
var retained = queue.get_result_by_idempotency_key(&"same-operation")
_expect(retained != null and retained.details["value"] == 42, "processed result retained by key")
_expect(retained.is_replay, "retained lookup is marked as replay")
func _test_snapshot_preserves_accepted_idempotency_history() -> void:
var queue = CommandQueueType.new()
_expect(queue.enqueue(_command(1, 0, 10, &"processed-key")).is_success(), "snapshot fixture accepts keyed command")
_expect(queue.enqueue(_command(2, 0, 5, &"")).is_success(), "snapshot fixture accepts keyless command")
_expect(queue.enqueue(_command(3, 5, 1, &"pending-key")).is_success(), "snapshot fixture accepts pending keyed command")
var completed: Array = queue.execute_ready(0, func(command: SimulationCommand) -> CommandResult:
return CommandResultType.succeeded(command.command_id, {"completed": true})
)
_expect(completed.size() == 2, "snapshot fixture completes its ready commands")
var snapshot: Dictionary = queue.create_snapshot()
_expect(int(snapshot["version"]) == CommandQueueType.SNAPSHOT_VERSION, "Command Queue writes the current snapshot version")
_expect(snapshot["accepted"] == [
{"command_id": 1, "idempotency_key": "processed-key"},
{"command_id": 2, "idempotency_key": ""},
{"command_id": 3, "idempotency_key": "pending-key"},
], "snapshot retains accepted metadata after Command inputs leave the queue")
var restored = CommandQueueType.new()
_expect(restored.restore_snapshot(snapshot), "strict current Command Queue snapshot restores")
_expect(restored.create_snapshot() == snapshot, "current Command Queue snapshot round trip is exact")
var metadata: Array[Dictionary] = restored.get_accepted_command_metadata()
metadata[0]["idempotency_key"] = "changed"
metadata.clear()
_expect(restored.get_accepted_command_metadata() == snapshot["accepted"], "accepted metadata query is a defensive deep copy")
var live_before: Dictionary = restored.create_snapshot()
var missing_map: Dictionary = snapshot.duplicate(true)
missing_map["idempotency"].erase("processed-key")
_expect(not restored.restore_snapshot(missing_map), "processed idempotency mapping deletion is rejected")
_expect(restored.create_snapshot() == live_before, "mapping deletion rejection is restore-atomic")
var changed_metadata: Dictionary = snapshot.duplicate(true)
changed_metadata["accepted"][0]["idempotency_key"] = ""
_expect(not restored.restore_snapshot(changed_metadata), "accepted metadata cannot disagree with the reverse map")
_expect(restored.create_snapshot() == live_before, "metadata mismatch rejection is restore-atomic")
var missing_metadata: Dictionary = snapshot.duplicate(true)
missing_metadata["accepted"].remove_at(0)
_expect(not restored.restore_snapshot(missing_metadata), "every known Command requires one accepted metadata row")
var unordered_metadata: Dictionary = snapshot.duplicate(true)
var first: Dictionary = unordered_metadata["accepted"][0]
unordered_metadata["accepted"][0] = unordered_metadata["accepted"][1]
unordered_metadata["accepted"][1] = first
_expect(not restored.restore_snapshot(unordered_metadata), "accepted metadata must be ordered by Command ID")
var unknown_field: Dictionary = snapshot.duplicate(true)
unknown_field["extra"] = true
_expect(not restored.restore_snapshot(unknown_field), "current snapshot rejects unknown root fields")
var string_version: Dictionary = snapshot.duplicate(true)
string_version["version"] = "2"
_expect(not restored.restore_snapshot(string_version), "current snapshot rejects a coerced version")
var float_result_id: Dictionary = snapshot.duplicate(true)
float_result_id["processed"][0]["command_id"] = 1.0
_expect(not restored.restore_snapshot(float_result_id), "current snapshot rejects a coerced processed Command ID")
_expect(restored.create_snapshot() == live_before, "all malformed current snapshots preserve live queue state")
func _test_legacy_snapshot_migrates_accepted_metadata() -> void:
var source = CommandQueueType.new()
source.enqueue(_command(7, 0, 1, &"legacy-processed"))
source.drain_ready(0)
source.complete(7, CommandResultType.succeeded(7))
source.enqueue(_command(9, 20, 1, &"legacy-pending"))
var legacy: Dictionary = source.create_snapshot()
legacy.erase("version")
legacy.erase("accepted")
var restored = CommandQueueType.new()
_expect(restored.restore_snapshot(legacy), "legacy unversioned Command Queue snapshot restores")
var migrated: Dictionary = restored.create_snapshot()
_expect(int(migrated["version"]) == CommandQueueType.SNAPSHOT_VERSION, "legacy restore writes the current version next time")
_expect(migrated["accepted"] == [
{"command_id": 7, "idempotency_key": "legacy-processed"},
{"command_id": 9, "idempotency_key": "legacy-pending"},
], "legacy reverse map reconstructs processed accepted idempotency metadata")
_expect(restored.get_result_by_idempotency_key(&"legacy-processed") != null, "legacy processed key remains exactly-once after migration")
func _test_transaction_commit() -> void:
var state := {"gold": 100, "stock": 3}
var transaction = TransactionType.new()
transaction.stage_dictionary_increment(state, "gold", -40, 0)
transaction.stage_dictionary_increment(state, "stock", -1, 0)
_expect(state == {"gold": 100, "stock": 3}, "staging does not mutate state")
var result = transaction.commit()
_expect(result.is_success(), "valid transaction commits")
_expect(state == {"gold": 60, "stock": 2}, "all staged changes commit together")
func _test_validation_is_atomic() -> void:
var state := {"gold": 100, "stock": 3}
var transaction = TransactionType.new()
transaction.stage_dictionary_set(state, "gold", 60)
transaction.stage_dictionary_set(state, "stock", 2)
transaction.add_validator(func() -> bool: return false)
var result = transaction.commit()
_expect(result.code == CommandResultType.Code.VALIDATION_FAILED, "validation failure reported")
_expect(state == {"gold": 100, "stock": 3}, "validation failure applies no changes")
func _test_apply_failure_rolls_back() -> void:
var state := {"gold": 100, "stock": 3}
var transaction = TransactionType.new()
transaction.stage_dictionary_set(state, "gold", 60)
transaction.stage_custom(
"failing stock update",
func() -> bool: return true,
func() -> bool:
state["stock"] = 2
return false,
func() -> void: state["stock"] = 3
)
var result = transaction.commit()
_expect(
result.code == CommandResultType.Code.TRANSACTION_APPLY_FAILED,
"apply failure reported"
)
_expect(state == {"gold": 100, "stock": 3}, "applied changes roll back in reverse order")
func _command(
command_id: int,
execute_tick: int,
priority: int,
idempotency_key: StringName
):
return CommandType.new(
command_id,
0,
execute_tick,
priority,
&"Test",
&"actor",
&"target",
{},
idempotency_key
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://dulabdbgejdnn
+291
View File
@@ -0,0 +1,291 @@
extends SceneTree
const DefinitionRegistryScript := preload("res://scripts/core/definition_registry.gd")
const FacilityCatalogScript := preload("res://scripts/data/facility_catalog.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_test_catalog_references()
_test_expansion_catalog()
_test_default_registry()
_test_detailed_definition_schema()
_test_invalid_detailed_service_is_rejected()
_test_canonical_added_baselines()
_test_queries()
_test_duplicate_detection_is_deterministic()
_test_missing_path_is_structured()
if _failures == 0:
print("PASS: DefinitionRegistry (%d checks)" % _checks)
else:
push_error("FAIL: DefinitionRegistry (%d of %d checks failed)" % [_failures, _checks])
quit(_failures)
func _test_default_registry() -> void:
var registry = DefinitionRegistryScript.new()
var report: Dictionary = registry.rebuild()
_expect(bool(report[&"valid"]), "all baseline definitions pass registry schema validation")
_expect(int(report[&"source_count"]) == 16, "all sixteen baseline source files are inspected")
_expect(int(report[&"definition_count"]) == 16, "all sixteen valid definitions are indexed")
_expect(int(report[&"error_count"]) == 0, "baseline definitions have no fatal schema errors")
_expect(int(report[&"warning_count"]) == 0, "baseline definitions have no schema warnings")
var expected_ids: Array[StringName] = [
&"adult_private_room",
&"cafeteria",
&"guest_corridor",
&"guest_elevator",
&"kitchen",
&"massage_room",
&"public_bath",
&"reception",
&"security_station",
&"service_corridor",
&"service_elevator",
&"staff_door",
&"staff_waiting_room",
&"standard_room",
&"tavern",
&"warehouse",
]
_expect(
registry.get_facility_ids() == expected_ids,
"all sixteen facility IDs are returned in deterministic lexical order"
)
_expect(registry.has_facility(&"reception"), "has_facility finds a known ID")
_expect(registry.has_facility(&"adult_private_room"), "has_facility finds the underground baseline ID")
_expect(registry.get_facility(&"missing") == null, "unknown lookup returns null")
func _test_catalog_references() -> void:
_expect(FacilityCatalogScript.FACILITY_PATHS.size() == 16, "catalog exposes sixteen stable resource references")
_expect(FacilityCatalogScript.BASELINE_FACILITY_IDS.size() == 16, "catalog exposes sixteen stable baseline IDs")
var loaded: Array[FacilityDefinition] = FacilityCatalogScript.load_all()
_expect(loaded.size() == 16, "every catalog resource reference resolves")
var loaded_ids: Array[StringName] = []
for index in FacilityCatalogScript.FACILITY_PATHS.size():
var path: String = FacilityCatalogScript.FACILITY_PATHS[index]
var expected_id: StringName = FacilityCatalogScript.BASELINE_FACILITY_IDS[index]
_expect(FileAccess.file_exists(path), "catalog reference %s exists" % path)
var definition := load(path) as FacilityDefinition
_expect(definition != null, "catalog reference %s loads as FacilityDefinition" % path)
if definition != null:
loaded_ids.append(definition.id)
_expect(definition.id == expected_id, "catalog reference %s preserves its expected ID" % path)
_expect(loaded_ids == FacilityCatalogScript.BASELINE_FACILITY_IDS, "catalog loading preserves the design-table order")
_expect(FacilityCatalogScript.by_id(&"service_elevator") != null, "catalog by_id resolves an added baseline")
_expect(FacilityCatalogScript.by_id(&"not_a_facility") == null, "catalog by_id rejects an unknown reference")
func _test_detailed_definition_schema() -> void:
var registry = DefinitionRegistryScript.new()
registry.rebuild()
var reception: FacilityDefinition = registry.get_facility(&"reception")
var kitchen: FacilityDefinition = registry.get_facility(&"kitchen")
var restricted: FacilityDefinition = registry.get_facility(&"adult_private_room")
_expect(reception.has_detailed_profile(), "reception uses the detailed facility schema")
_expect(reception.default_service_type == &"check_in", "reception declares its default service")
_expect(reception.service_profiles.size() == 2, "reception defines check-in and check-out profiles")
_expect(reception.service_profiles[&"check_out"][&"duration_minutes"] == 6, "check-out has an independent duration")
_expect(kitchen.has_detailed_profile(), "kitchen uses the detailed facility schema")
_expect(kitchen.service_profiles[&"meal_batch"][&"capacity_units"] == 4, "kitchen service produces a four-unit batch")
_expect(restricted.has_detailed_profile(), "restricted room uses the detailed facility schema")
_expect(restricted.opening_schedule == {&"morning": false, &"day": false, &"night": true}, "restricted room is night-only")
_expect(restricted.service_profiles[&"private_adult_service"][&"need_effects"][&"intimacy"] == -70, "restricted service records need relief")
_expect(restricted.content_replacement_id == &"massage_room", "restricted service declares a safe replacement")
func _test_invalid_detailed_service_is_rejected() -> void:
var registry = DefinitionRegistryScript.new()
var paths: Array[String] = ["res://tests/fixtures/facilities/invalid_service_profile.tres"]
var report: Dictionary = registry.rebuild_from_paths(paths)
var errors: Array[Dictionary] = registry.get_fatal_errors()
_expect(not bool(report[&"valid"]), "an invalid detailed service profile makes the registry invalid")
_expect(registry.facility_count() == 0, "an invalid detailed facility is not exposed through lookup")
_expect(errors.size() == 1, "invalid service duration produces one focused issue")
_expect(errors[0][&"code"] == &"invalid_service_profile_value", "invalid service duration has a stable issue code")
_expect(errors[0][&"field"] == &"service_profiles", "invalid service issue identifies its owning field")
func _test_expansion_catalog() -> void:
_expect(FacilityCatalogScript.EXPANSION_FACILITY_IDS.size() == 16, "catalog exposes sixteen optional expansion IDs")
_expect(FacilityCatalogScript.EXPANSION_FACILITY_PATHS.size() == 16, "catalog exposes sixteen optional expansion resources")
var loaded: Array[FacilityDefinition] = FacilityCatalogScript.load_expansion()
_expect(loaded.size() == 16, "every optional expansion resource resolves")
var loaded_ids: Array[StringName] = []
for index in FacilityCatalogScript.EXPANSION_FACILITY_PATHS.size():
var path: String = FacilityCatalogScript.EXPANSION_FACILITY_PATHS[index]
var expected_id: StringName = FacilityCatalogScript.EXPANSION_FACILITY_IDS[index]
_expect(FileAccess.file_exists(path), "expansion catalog reference %s exists" % path)
var definition := load(path) as FacilityDefinition
_expect(definition != null, "expansion catalog reference %s loads" % path)
if definition != null:
loaded_ids.append(definition.id)
_expect(definition.id == expected_id, "expansion resource preserves %s" % expected_id)
_expect(definition.is_valid(), "expansion definition %s passes its resource schema" % expected_id)
_expect(loaded_ids == FacilityCatalogScript.EXPANSION_FACILITY_IDS, "expansion loading preserves design order")
_expect(FacilityCatalogScript.by_id(&"clinic") != null, "catalog by_id resolves an optional expansion")
_expect(
FacilityCatalogScript.by_id(&"vip_private_salon").zone == FacilityDefinition.MapZone.UNDERGROUND,
"restricted expansion is constrained to the underground zone"
)
_expect(
FacilityCatalogScript.by_id(&"mana_control_room").layer == FacilityDefinition.ViewLayer.BACK,
"infrastructure expansion is constrained to the BACK layer"
)
func _test_canonical_added_baselines() -> void:
var registry = DefinitionRegistryScript.new()
registry.rebuild()
var rows: Array = [
[&"kitchen", 700, 10, Vector2i(4, 2), 1, 3, 20, 15, 0],
[&"public_bath", 1200, 16, Vector2i(6, 2), 1, 2, 12, 60, 20],
[&"staff_waiting_room", 400, 6, Vector2i(4, 2), 0, 0, 8, 1, 0],
[&"security_station", 900, 10, Vector2i(2, 2), 1, 3, 3, 1, 0],
[&"tavern", 1000, 12, Vector2i(4, 2), 2, 4, 14, 50, 40],
[&"massage_room", 800, 8, Vector2i(2, 1), 1, 2, 2, 60, 100],
[&"guest_elevator", 1500, 15, Vector2i(2, 2), 0, 0, 4, 10, 0],
[&"service_elevator", 2200, 20, Vector2i(3, 2), 0, 0, 8, 5, 0],
[&"staff_door", 100, 1, Vector2i(1, 1), 0, 0, 1, 1, 0],
[&"guest_corridor", 20, 0, Vector2i(1, 1), 0, 0, 3, 1, 0],
[&"service_corridor", 25, 0, Vector2i(1, 1), 0, 0, 2, 1, 0],
[&"adult_private_room", 1500, 14, Vector2i(2, 1), 1, 1, 2, 90, 250],
]
for raw_row: Variant in rows:
var row := raw_row as Array
var facility_id: StringName = row[0]
var definition: FacilityDefinition = registry.get_facility(facility_id)
_expect(definition != null, "%s canonical baseline is registered" % facility_id)
if definition == null:
continue
_expect(
definition.construction_cost == {&"gold": row[1], &"materials": row[2]},
"%s uses canonical Gold and Materials cost" % facility_id
)
_expect(definition.footprint == row[3], "%s uses canonical footprint" % facility_id)
_expect(
definition.staff_min == row[4] and definition.staff_max == row[5],
"%s uses canonical Min/Max staffing" % facility_id
)
_expect(definition.capacity == row[6], "%s uses canonical capacity" % facility_id)
_expect(definition.base_service_minutes == row[7], "%s uses canonical duration or one-minute proxy" % facility_id)
_expect(definition.base_price == row[8], "%s uses canonical direct price" % facility_id)
_expect(definition.is_valid(), "%s passes the FacilityDefinition schema" % facility_id)
var kitchen: FacilityDefinition = registry.get_facility(&"kitchen")
_expect(kitchen.inventory_outputs.get(&"ingredient/basic_meal", 0) == 4, "kitchen models the canonical four-meal production batch")
var restricted: FacilityDefinition = registry.get_facility(&"adult_private_room")
_expect(restricted.zone == FacilityDefinition.MapZone.UNDERGROUND, "restricted private room is constrained to the underground zone")
_expect(&"adult_only" in restricted.tags, "restricted private room carries an explicit adult-only systems guard tag")
func _test_queries() -> void:
var registry = DefinitionRegistryScript.new()
registry.rebuild()
var service_facilities: Array[StringName] = [
&"adult_private_room", &"massage_room", &"public_bath", &"reception", &"tavern",
]
var any_zone: Array[StringName] = [
&"cafeteria",
&"guest_corridor",
&"guest_elevator",
&"kitchen",
&"massage_room",
&"public_bath",
&"security_station",
&"service_corridor",
&"service_elevator",
&"staff_door",
&"staff_waiting_room",
&"tavern",
&"warehouse",
]
var back_facilities: Array[StringName] = [
&"kitchen",
&"security_station",
&"service_corridor",
&"service_elevator",
&"staff_door",
&"staff_waiting_room",
&"warehouse",
]
_expect(
_ids_of(registry.query_facilities_by_tag(&"service")) == service_facilities,
"service tag query returns every matching definition in stable order"
)
_expect(
_ids_of(registry.query_facilities_by_zone(FacilityDefinition.MapZone.ANY)) == any_zone,
"zone query preserves ID order"
)
_expect(
_ids_of(registry.query_facilities_by_zone(FacilityDefinition.MapZone.UNDERGROUND)) == [&"adult_private_room"],
"underground query isolates its restricted baseline"
)
_expect(
_ids_of(registry.query_facilities_by_layer(FacilityDefinition.ViewLayer.BACK)) == back_facilities,
"BACK layer query returns all logistics and staff definitions"
)
_expect(
_ids_of(registry.query_facilities_by_tag(&"corridor")) == [&"guest_corridor", &"service_corridor"],
"corridor tag identifies both route-subslot proxies"
)
_expect(
_ids_of(registry.query_facilities_by_tag(&"vertical_connector")) == [&"guest_elevator", &"service_elevator"],
"vertical connector tag identifies both elevators"
)
_expect(
_ids_of(registry.query_facilities_by_tag(&"cross_layer_connector")) == [&"staff_door"],
"cross-layer connector query identifies the staff door proxy"
)
func _test_duplicate_detection_is_deterministic() -> void:
var registry = DefinitionRegistryScript.new()
var duplicate_paths: Array[String] = [
"res://data/facilities/reception.tres",
"res://data/facilities/reception.tres",
]
var first_report: Dictionary = registry.rebuild_from_paths(duplicate_paths)
var first_errors: Array[Dictionary] = registry.get_fatal_errors()
var second_report: Dictionary = registry.rebuild_from_paths(duplicate_paths)
_expect(not bool(first_report[&"valid"]), "duplicate IDs make the registry invalid")
_expect(int(first_report[&"error_count"]) == 2, "every duplicate source receives a fatal issue")
_expect(registry.facility_count() == 0, "duplicate definitions are excluded from lookup")
_expect(first_errors == registry.get_fatal_errors(), "issue order is deterministic across rebuilds")
_expect(first_report[&"errors"][0][&"code"] == &"duplicate_id", "duplicate issue has a stable code")
_expect(int(second_report[&"warning_count"]) == 1, "duplicate source paths produce a warning")
func _test_missing_path_is_structured() -> void:
var registry = DefinitionRegistryScript.new()
var paths: Array[String] = ["res://data/facilities/does_not_exist.tres"]
registry.rebuild_from_paths(paths)
var errors: Array[Dictionary] = registry.get_fatal_errors()
_expect(errors.size() == 1, "missing path produces one fatal issue")
_expect(errors[0][&"severity"] == &"error", "issue carries severity")
_expect(errors[0][&"code"] == &"resource_not_found", "missing path has a stable code")
_expect(errors[0][&"field"] == &"resource_path", "issue identifies its field")
_expect(errors[0][&"resource_path"] == paths[0], "issue identifies its source")
func _ids_of(definitions: Array[FacilityDefinition]) -> Array[StringName]:
var result: Array[StringName] = []
for definition in definitions:
result.append(definition.id)
return result
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://cwok3f6pmjar5
+131
View File
@@ -0,0 +1,131 @@
extends SceneTree
const DeterministicRngScript = preload("res://scripts/core/deterministic_rng.gd")
const SimulationDigestScript = preload("res://scripts/core/simulation_digest.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S0: deterministic RNG and simulation digest")
_test_stream_reproducibility()
_test_stream_isolation()
_test_snapshot_restore()
_test_unbiased_range_contract()
_test_digest_normalization()
_test_digest_sequence_sensitivity()
_test_digest_float_normalization()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_stream_reproducibility() -> void:
var first = DeterministicRngScript.from_root_seed(20260827, &"guest_ai")
var second = DeterministicRngScript.from_root_seed(20260827, &"guest_ai")
var first_values: Array[int] = []
var second_values: Array[int] = []
for draw_index in range(64):
first_values.append(first.next_u32())
second_values.append(second.next_u32())
_expect(first_values == second_values, "same root seed and stream ID reproduce the sequence")
_expect(first.get_counter() == 64, "raw draw counter advances exactly once per sample")
func _test_stream_isolation() -> void:
var guest = DeterministicRngScript.from_root_seed(20260827, &"guest_ai")
var events = DeterministicRngScript.from_root_seed(20260827, &"event_service")
var guest_values: Array[int] = []
var event_values: Array[int] = []
for draw_index in range(16):
guest_values.append(guest.next_u32())
event_values.append(events.next_u32())
_expect(guest_values != event_values, "different subsystem stream IDs derive isolated sequences")
func _test_snapshot_restore() -> void:
var original = DeterministicRngScript.from_root_seed(-17, &"authority_service")
for draw_index in range(11):
original.next_u32()
var saved_state: Dictionary = original.snapshot()
var expected_tail: Array[int] = []
for draw_index in range(20):
expected_tail.append(original.next_u32())
var restored = DeterministicRngScript.from_root_seed(0, &"temporary")
_expect(restored.restore(saved_state), "valid RNG snapshot restores")
var actual_tail: Array[int] = []
for draw_index in range(20):
actual_tail.append(restored.next_u32())
_expect(actual_tail == expected_tail, "restored state continues at the exact next sample")
_expect(restored.get_counter() == 31, "snapshot restores and continues the draw counter")
func _test_unbiased_range_contract() -> void:
var rng = DeterministicRngScript.from_root_seed(99, &"range_test")
var all_samples_inside := true
for draw_index in range(4096):
var sample: int = rng.randi_range(-7, 13)
all_samples_inside = all_samples_inside and sample >= -7 and sample <= 13
_expect(all_samples_inside, "inclusive integer range never produces an out-of-range value")
var counter_before_constant: int = rng.get_counter()
_expect(rng.randi_range(5, 5) == 5, "single-value range returns that value")
_expect(rng.get_counter() == counter_before_constant, "single-value range consumes no random sample")
func _test_digest_normalization() -> void:
var first := {}
first[&"resources"] = {&"gold": 4500, &"mana": 100}
first[&"minute"] = 721
first[&"positions"] = [Vector2i(3, 4), Vector2i(-2, 8)]
var second := {}
second[&"positions"] = [Vector2i(3, 4), Vector2i(-2, 8)]
second[&"minute"] = 721
var reversed_resources := {}
reversed_resources[&"mana"] = 100
reversed_resources[&"gold"] = 4500
second[&"resources"] = reversed_resources
var first_digest: String = SimulationDigestScript.compute(first)
var second_digest: String = SimulationDigestScript.compute(second)
_expect(first_digest.length() == 64, "simulation digest is a 256-bit lowercase hexadecimal value")
_expect(first_digest == second_digest, "dictionary insertion order does not affect the digest")
func _test_digest_sequence_sensitivity() -> void:
var forward: String = SimulationDigestScript.compute([1, 2, 3])
var reversed: String = SimulationDigestScript.compute([3, 2, 1])
_expect(
forward == "4e32bb2921e1b879d5b8778789d3bc7e46559d4448e95f9c2fed427d7e5c7b93",
"canonical digest format matches its versioned reference vector"
)
_expect(forward != reversed, "array order remains significant")
_expect(
SimulationDigestScript.compute({"value": 1}) != SimulationDigestScript.compute({"value": "1"}),
"value types remain significant"
)
func _test_digest_float_normalization() -> void:
_expect(
SimulationDigestScript.compute({"zero": 0.0}) == SimulationDigestScript.compute({"zero": -0.0}),
"positive and negative floating-point zero share one canonical representation"
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://6ob7pdgspn68
+121
View File
@@ -0,0 +1,121 @@
extends SceneTree
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S0: domain events")
_test_single_publish_and_snapshot()
_test_atomic_batch_and_reentrant_delivery()
_test_batch_rejection_is_atomic()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_single_publish_and_snapshot() -> void:
var stream := DomainEventStream.new()
var delivery_order: Array[StringName] = []
stream.subscribe(&"SERVICE_COMPLETED", &"z_report", func(_event: SimulationDomainEvent) -> void: delivery_order.append(&"z_report"))
stream.subscribe(&"*", &"a_log", func(_event: SimulationDomainEvent) -> void: delivery_order.append(&"a_log"))
var event := SimulationDomainEvent.new(
&"domain_event.000000000001",
&"SERVICE_COMPLETED",
360,
&"facility_service",
[&"facility.000000000001"],
{&"quality": 80},
12
)
_expect(event.is_valid(), "complete domain event is valid")
_expect(stream.publish(event), "new fact is published")
_expect(delivery_order == [&"a_log", &"z_report"], "subscribers run in stable ID order")
_expect(not stream.publish(event), "same event ID cannot be applied twice")
_expect(delivery_order.size() == 2, "duplicate event is not redelivered")
var snapshot := stream.create_snapshot()
var restored := DomainEventStream.new()
_expect(restored.restore_snapshot(snapshot), "published event state restores")
_expect(restored.has_published(event.event_id), "deduplication IDs survive restore")
var recent := restored.recent_events()
_expect(recent.size() == 1 and recent[0].payload[&"quality"] == 80, "recent fact data survives restore")
func _test_atomic_batch_and_reentrant_delivery() -> void:
var stream := DomainEventStream.new()
var deliveries: Array[StringName] = []
var callback_probe := {
&"second_committed_before_delivery": false,
&"restore_rejected_during_delivery": false,
}
var event_a := _event(&"domain_event.batch.0001", &"A", 400)
var event_b := _event(&"domain_event.batch.0002", &"B", 400)
var event_c := _event(&"domain_event.batch.0003", &"C", 400)
var snapshot_before := stream.create_snapshot()
stream.subscribe(&"*", &"observer", func(event: SimulationDomainEvent) -> void:
deliveries.append(event.event_type)
if event.event_type == &"A":
callback_probe[&"second_committed_before_delivery"] = stream.has_published(event_b.event_id)
callback_probe[&"restore_rejected_during_delivery"] = not stream.restore_snapshot(snapshot_before)
_expect(stream.publish_batch([event_c]), "reentrant publication is accepted after the committed batch")
)
_expect(stream.publish_batch([event_a, event_b]), "valid event batch commits")
_expect(bool(callback_probe[&"second_committed_before_delivery"]), "every batch ID is committed before the first callback")
_expect(bool(callback_probe[&"restore_rejected_during_delivery"]), "subscriber cannot replace stream state during delivery")
_expect(deliveries == [&"A", &"B", &"C"], "reentrant event delivery waits behind the complete batch")
_expect(
stream.has_published(event_a.event_id) \
and stream.has_published(event_b.event_id) \
and stream.has_published(event_c.event_id),
"batch and reentrant event IDs are all retained"
)
stream.unsubscribe(&"*", &"observer")
func _test_batch_rejection_is_atomic() -> void:
var stream := DomainEventStream.new()
var deliveries: Array[StringName] = []
stream.subscribe(&"*", &"observer", func(event: SimulationDomainEvent) -> void:
deliveries.append(event.event_type)
)
var valid := _event(&"domain_event.reject.0001", &"VALID", 500)
var duplicate := _event(&"domain_event.reject.0001", &"DUPLICATE", 500)
var before := stream.create_snapshot()
_expect(not stream.publish_batch([valid, duplicate]), "duplicate ID inside a batch is rejected")
_expect(stream.create_snapshot() == before and deliveries.is_empty(), "duplicate batch rejection has no state or notification prefix")
var invalid := _event(&"domain_event.reject.0002", &"INVALID", 500)
invalid.event_type = &""
_expect(not stream.publish_batch([valid, invalid]), "invalid member rejects the whole batch")
_expect(stream.create_snapshot() == before and deliveries.is_empty(), "invalid batch rejection remains atomic")
_expect(stream.publish(valid), "single publish still delegates to the batch boundary")
var after_valid := stream.create_snapshot()
_expect(not stream.publish_batch([_event(&"domain_event.reject.0003", &"NEW", 500), valid]), "collision with published history rejects the whole batch")
_expect(stream.create_snapshot() == after_valid and deliveries == [&"VALID"], "history collision cannot commit a new prefix")
var oversized: Array = []
oversized.resize(DomainEventStream.MAX_BATCH_SIZE + 1)
_expect(not stream.publish_batch(oversized), "oversized batch is rejected before member access")
_expect(stream.create_snapshot() == after_valid, "oversized rejection preserves history")
func _event(event_id: StringName, event_type: StringName, minute: int) -> SimulationDomainEvent:
return SimulationDomainEvent.new(
event_id,
event_type,
minute,
&"test",
[],
{},
0
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://3a3nnlur4gv3
+123
View File
@@ -0,0 +1,123 @@
extends RefCounted
static func run(expect: Callable) -> void:
_test_fixed_tick_order(expect)
_test_frame_budget_and_overload(expect)
_test_boundary_pause_stops_catch_up(expect)
_test_apply_gate_aborts_before_clock_increment(expect)
_test_snapshot_restore(expect)
_test_save_round_trip(expect)
static func _test_fixed_tick_order(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
var phases: Array[int] = []
clock.tick_phase_started.connect(func(phase: int, _minute: int) -> void: phases.append(phase))
var handler_order: Array[StringName] = []
clock.register_tick_handler(clock.TickPhase.MANA, &"z_last", func(_minute: int) -> void: handler_order.append(&"z_last"))
clock.register_tick_handler(clock.TickPhase.MANA, &"a_first", func(_minute: int) -> void: handler_order.append(&"a_first"))
clock.advance_one_minute()
expect.call(clock.absolute_minute == 361, "fixed tick advances exactly one game minute")
expect.call(phases == range(clock.TickPhase.size()), "tick phases run in the specified order")
expect.call(handler_order == [&"a_first", &"z_last"], "phase handlers use stable id order")
clock.free()
static func _test_frame_budget_and_overload(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
var processed: int = clock.process_real_delta(5.0)
expect.call(processed == clock.MAX_TICKS_PER_FRAME, "one frame runs at most eight ticks")
expect.call(clock.absolute_minute == 368, "excess ticks remain queued instead of being skipped")
clock.reset(1, 6, 0)
clock.process_real_delta(100.0)
expect.call(clock.paused and clock.get_backlog_minutes() > 120, "large backlog pauses the simulation")
clock.free()
static func _test_boundary_pause_stops_catch_up(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
clock.register_tick_handler(
clock.TickPhase.TIME_BOUNDARIES,
&"test.pause_boundary",
func(minute: int) -> void:
if minute == 361:
clock.set_paused(true)
)
var processed: int = clock.process_real_delta(5.0)
expect.call(processed == 1, "a boundary pause stops the remaining frame tick budget")
expect.call(clock.absolute_minute == 361, "catch-up cannot advance beyond the pausing boundary")
clock.reset(1, 6, 0)
clock.advance_minutes(8)
expect.call(clock.absolute_minute == 361, "an initially-running manual batch stops when a handler pauses")
clock.unregister_tick_handler(clock.TickPhase.TIME_BOUNDARIES, &"test.pause_boundary")
clock.reset(1, 6, 0)
clock.set_paused(true)
clock.advance_minutes(2)
expect.call(clock.absolute_minute == 362, "an already-paused clock retains explicit manual stepping")
clock.free()
static func _test_apply_gate_aborts_before_clock_increment(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
clock.register_tick_handler(
clock.TickPhase.APPLY_COMMANDS,
&"test.abort_tick",
func(_minute: int) -> void:
clock.set_paused(true)
clock.request_current_tick_abort()
)
var starting_version: int = clock.snapshot_version
var processed: int = clock.process_real_delta(5.0)
expect.call(processed == 0, "an APPLY gate abort consumes no catch-up tick")
expect.call(
clock.absolute_minute == 360 and clock.snapshot_version == starting_version,
"an APPLY gate abort leaves Clock minute and snapshot version exact"
)
clock.advance_minutes(1)
expect.call(
clock.absolute_minute == 360 and clock.snapshot_version == starting_version,
"manual stepping cannot bypass an APPLY gate abort"
)
clock.free()
static func _test_snapshot_restore(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
var state = load("res://autoload/game_state.gd").new()
clock.reset(2, 5, 30)
state.reset()
var starting_revision: int = state.revision
expect.call(state.apply_resource_deltas({&"gold": -250, &"materials": -5}), "multi-resource transaction succeeds")
expect.call(state.revision == starting_revision + 1, "atomic resource transaction creates one revision")
var before_failed: Dictionary = state.create_snapshot()
expect.call(not state.apply_resource_deltas({&"gold": -999999, &"mana": 10}), "invalid transaction is rejected")
expect.call(state.create_snapshot() == before_failed, "rejected transaction changes no resource")
var snapshot := SimulationSnapshot.capture(clock, state)
clock.advance_minutes(10)
state.add_resource(&"gold", 999)
expect.call(SimulationSnapshot.restore(snapshot, clock, state), "valid snapshot restores atomically")
expect.call(clock.absolute_minute == int(snapshot[&"clock"][&"absolute_minute"]), "clock snapshot restores exact minute")
expect.call(state.create_snapshot() == snapshot[&"game_state"], "game state snapshot restores exactly")
var minute_before_skip: int = clock.absolute_minute
expect.call(clock.process_real_delta(1.0) == 0 and clock.absolute_minute == minute_before_skip, "first frame after load does not advance")
clock.free()
state.free()
static func _test_save_round_trip(expect: Callable) -> void:
var save_path := "user://saves/s0_round_trip"
SaveService.delete_save(save_path)
var clock = load("res://autoload/game_clock.gd").new()
var state = load("res://autoload/game_state.gd").new()
clock.reset(3, 18, 45)
state.reset()
state.apply_resource_deltas({&"gold": -700, &"mana": 25})
var snapshot := SimulationSnapshot.capture(clock, state, {&"next_entity_id": 42})
var save_result := SaveService.save_snapshot(save_path, snapshot, {&"display_name": "S0 Round Trip"})
expect.call(save_result[&"ok"], "snapshot is written atomically")
var load_result := SaveService.load_snapshot(save_path)
expect.call(load_result[&"ok"], "saved snapshot passes checksum and schema validation")
if load_result[&"ok"]:
expect.call(load_result[&"snapshot"] == snapshot, "save and load preserve the complete snapshot")
SaveService.delete_save(save_path)
clock.free()
state.free()
+1
View File
@@ -0,0 +1 @@
uid://w4p6kwpj1x46
+201
View File
@@ -0,0 +1,201 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S0: runtime integration")
_test_tick_command_execution()
_test_starter_world_layers()
_test_paused_command_execution()
_test_runtime_snapshot_restore()
_test_runtime_save_round_trip()
_test_twenty_four_hour_determinism()
_cleanup_runtime()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime(seed := 20260827) -> void:
_cleanup_runtime()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, seed)
func _cleanup_runtime() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_tick_command_execution() -> void:
_reset_runtime()
var initial_gold: int = _state.get_resource(&"gold")
var result = _runtime.submit_command(
&"spend_resources",
{&"cost": {&"gold": 100}},
&"player",
&"",
50,
_clock.absolute_minute + 5,
&"integration.spend.future"
)
_expect(result.is_success(), "future resource command enters the queue")
_clock.advance_minutes(4)
_expect(_state.get_resource(&"gold") == initial_gold, "future command does not execute early")
_clock.advance_one_minute()
_expect(_state.get_resource(&"gold") == initial_gold - 100, "command executes on its scheduled minute")
var processed = _runtime.command_queue.get_result_by_idempotency_key(&"integration.spend.future")
_expect(processed != null and processed.is_success(), "processed result remains available by idempotency key")
func _test_starter_world_layers() -> void:
_reset_runtime()
var front_start := Vector3i(-2, 0, WorldService.LAYER_FRONT)
var back_goal := Vector3i(2, 0, WorldService.LAYER_BACK)
var guest_path: Array[Vector3i] = _runtime.path_service.find_path(
front_start, back_goal, [WorldService.LAYER_FRONT, WorldService.LAYER_BACK]
)
_expect(guest_path.is_empty(), "guest access cannot cross the FRONT/BACK staff door")
var staff_path: Array[Vector3i] = _runtime.path_service.find_path(
front_start,
back_goal,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK],
[&"staff"]
)
_expect(not staff_path.is_empty(), "staff access crosses the explicit FRONT/BACK connector")
func _test_paused_command_execution() -> void:
_reset_runtime()
_clock.set_paused(true)
var minute_before: int = _clock.absolute_minute
var result = _runtime.submit_command(
&"change_resources",
{&"deltas": {&"faith": 7}},
&"player",
&"",
0,
-1,
&"integration.paused.faith"
)
_expect(result.is_success() and _state.get_resource(&"faith") == 7, "current-minute command can complete while paused")
_expect(_clock.absolute_minute == minute_before, "paused command does not advance simulation time")
var duplicate = _runtime.submit_command(
&"change_resources",
{&"deltas": {&"faith": 7}},
&"player",
&"",
0,
-1,
&"integration.paused.faith"
)
_expect(duplicate.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, "duplicate idempotency key cannot apply twice")
func _test_runtime_snapshot_restore() -> void:
_reset_runtime(77)
var authority_rng = _runtime.get_rng_stream(&"authority")
for _index in range(9):
authority_rng.next_u32()
_runtime.submit_command(
&"spend_resources",
{&"cost": {&"materials": 3}},
&"player",
&"",
0,
_clock.absolute_minute + 30,
&"integration.pending.materials"
)
var snapshot: Dictionary = _runtime.create_snapshot()
var expected_digest := SimulationDigest.compute(snapshot)
var expected_rng_tail: int = authority_rng.next_u32()
_clock.advance_minutes(12)
_state.add_resource(&"gold", 500)
_expect(_runtime.restore_snapshot(snapshot), "runtime restores clock, state, queue and RNG streams")
_expect(_runtime.latest_digest == expected_digest, "restored runtime recreates the exact digest")
_expect(_runtime.get_rng_stream(&"authority").next_u32() == expected_rng_tail, "restored RNG resumes at the exact next draw")
_clock.advance_minutes(30)
_expect(_state.get_resource(&"materials") == 47, "restored pending command executes once")
func _test_twenty_four_hour_determinism() -> void:
var first_snapshot := _run_day_scenario(99117)
var second_snapshot := _run_day_scenario(99117)
var first_digest := SimulationDigest.compute(first_snapshot)
var second_digest := SimulationDigest.compute(second_snapshot)
if first_digest != second_digest:
_report_digest_difference(first_snapshot, second_snapshot)
_expect(first_digest.length() == 64, "24-hour scenario produces a simulation digest")
_expect(first_digest == second_digest, "same seed and command sequence match after 24 game hours")
func _test_runtime_save_round_trip() -> void:
var save_path := "user://saves/s0_runtime_round_trip"
SaveService.delete_save(save_path)
_reset_runtime(54321)
_runtime.get_rng_stream(&"event").next_u32()
_runtime.submit_command(
&"spend_resources",
{&"cost": {&"gold": 333}},
&"player", &"", 0, _clock.absolute_minute + 20, &"runtime.save.pending"
)
var expected_digest := SimulationDigest.compute(_runtime.create_snapshot())
var save_result: Dictionary = _runtime.save_to_path(save_path, {&"display_name": "Runtime Round Trip"})
_expect(save_result[&"ok"], "runtime snapshot writes command queue and RNG state to disk")
_clock.advance_minutes(25)
_state.add_resource(&"gold", 999)
var load_result: Dictionary = _runtime.load_from_path(save_path)
_expect(load_result[&"ok"], "runtime snapshot loads from verified world data")
_expect(_runtime.latest_digest == expected_digest, "disk round trip restores the exact runtime digest")
_clock.advance_minutes(20)
_expect(_state.get_resource(&"gold") == 4667, "disk-restored pending command executes exactly once")
SaveService.delete_save(save_path)
func _run_day_scenario(seed: int) -> Dictionary:
_reset_runtime(seed)
var guest_rng = _runtime.get_rng_stream(&"guest_spawn")
for _index in range(16):
guest_rng.randi_range(1, 100)
_runtime.submit_command(
&"spend_resources",
{&"cost": {&"gold": 250, &"materials": 2}},
&"player", &"", 20, 6 * 60 + 60, &"scenario.build"
)
_runtime.submit_command(
&"change_resources",
{&"deltas": {&"mana": 15, &"faith": 3}},
&"event", &"", 10, 6 * 60 + 12 * 60, &"scenario.reward"
)
_clock.advance_minutes(24 * 60)
return _runtime.latest_snapshot.duplicate(true)
func _report_digest_difference(first: Dictionary, second: Dictionary) -> void:
var first_runtime: Dictionary = first[&"systems"][&"runtime"]
var second_runtime: Dictionary = second[&"systems"][&"runtime"]
var keys: Array = first_runtime.keys()
keys.sort()
for key: Variant in keys:
var first_part := SimulationDigest.compute(first_runtime[key])
var second_part := SimulationDigest.compute(second_runtime.get(key))
if first_part != second_part:
print(" digest mismatch in runtime.%s: %s != %s" % [key, first_part, second_part])
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://brilcgnxebg7u
+35
View File
@@ -0,0 +1,35 @@
extends SceneTree
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S0: stable ID generator")
var generator := StableIdGenerator.new()
_expect(generator.next_id(&"guest") == &"guest.000000000001", "first ID uses a stable padded sequence")
_expect(generator.next_id(&"guest") == &"guest.000000000002", "same prefix increments monotonically")
_expect(generator.next_id(&"staff") == &"staff.000000000001", "prefixes have independent counters")
var snapshot := generator.create_snapshot()
var restored := StableIdGenerator.new()
_expect(restored.restore_snapshot(snapshot), "valid ID counters restore")
_expect(restored.next_id(&"guest") == &"guest.000000000003", "restored counter never reuses an ID")
_expect(restored.next_id(&"staff") == &"staff.000000000002", "all prefix counters restore")
var before_invalid := restored.create_snapshot()
_expect(not restored.restore_snapshot({"version": 1, "counters": {"Bad Prefix": 2}}), "invalid prefix snapshot is rejected")
_expect(restored.create_snapshot() == before_invalid, "rejected restore leaves counters unchanged")
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://cfgmvvsmbwg8t
+752
View File
@@ -0,0 +1,752 @@
extends SceneTree
const FacilityDefinitionType = preload("res://scripts/data/facility_definition.gd")
const PlacementReportType = preload("res://scripts/construction/placement_report.gd")
const ConstructionJobType = preload("res://scripts/construction/construction_job.gd")
const ConstructionServiceType = preload("res://scripts/construction/construction_service.gd")
const WorldServiceType = preload("res://scripts/world/world_service.gd")
var _checks := 0
var _failures := 0
class FakeResourceState:
extends RefCounted
var resources := {&"gold": 1_000, &"materials": 100}
var mutate_then_fail_spend := false
var fail_apply := false
var fail_restore := false
var reentrant_service: Variant
var reentrant_attempted := false
var reentrant_succeeded := false
func get_resource(resource_name: StringName) -> int:
return int(resources.get(resource_name, 0))
func can_afford(cost: Dictionary) -> bool:
for resource_name: Variant in cost:
if get_resource(StringName(resource_name)) < int(cost[resource_name]):
return false
return true
func spend(cost: Dictionary) -> bool:
if reentrant_service != null and not reentrant_attempted:
reentrant_attempted = true
var nested: Variant = reentrant_service.start_construction(
_valid_definition_for_reentry(),
Vector3i(2, 2, 0)
)
reentrant_succeeded = nested.is_success()
if mutate_then_fail_spend:
resources[&"gold"] = int(resources[&"gold"]) - int(cost.get(&"gold", 0))
return false
if not can_afford(cost):
return false
var deltas: Dictionary = {}
for resource_name: Variant in cost:
deltas[StringName(resource_name)] = -int(cost[resource_name])
return apply_resource_deltas(deltas)
func apply_resource_deltas(deltas: Dictionary) -> bool:
if fail_apply:
return false
var next := resources.duplicate(true)
for resource_name: Variant in deltas:
var key := StringName(resource_name)
next[key] = int(next.get(key, 0)) + int(deltas[resource_name])
if int(next[key]) < 0:
return false
resources = next
return true
func create_snapshot() -> Dictionary:
return {&"resources": resources.duplicate(true)}
func restore_snapshot(snapshot: Dictionary) -> bool:
if fail_restore:
return false
if not snapshot.get(&"resources", null) is Dictionary:
return false
resources = snapshot[&"resources"].duplicate(true)
return true
func _valid_definition_for_reentry() -> Dictionary:
return {
&"id": &"nested_room",
&"footprint": Vector2i.ONE,
&"zone": FacilityDefinitionType.MapZone.ANY,
&"layer": FacilityDefinitionType.ViewLayer.FRONT,
&"construction_cost": {&"gold": 100, &"materials": 10},
&"build_minutes": 60,
&"weight_per_cell": 1,
}
class FakeWorld:
extends RefCounted
var min_x := 0
var max_x := 8
var min_y := -2
var max_y := 8
var occupancy: Dictionary = {}
var unsupported_cells: Array[Vector3i] = []
var fail_next_occupy := false
var fail_next_release := false
var fail_all_occupy := false
var excavation: Dictionary = {}
var forced_cells: Array[Vector3i] = []
func cells_for_footprint(
origin: Vector3i,
footprint: Vector2i,
rotation: int,
_layer: int
) -> Array[Vector3i]:
if not forced_cells.is_empty():
return forced_cells.duplicate()
var width := footprint.x
var height := footprint.y
if rotation % 180 != 0:
width = footprint.y
height = footprint.x
var cells: Array[Vector3i] = []
for y_offset in height:
for x_offset in width:
cells.append(origin + Vector3i(x_offset, y_offset, 0))
return cells
func is_in_bounds(cell: Vector3i) -> bool:
return cell.x >= min_x and cell.x <= max_x and cell.y >= min_y and cell.y <= max_y
func get_zone(cell: Vector3i) -> int:
if cell.y > 0:
return FacilityDefinitionType.MapZone.ABOVE_GROUND
if cell.y < 0:
return FacilityDefinitionType.MapZone.UNDERGROUND
return FacilityDefinitionType.MapZone.BOUNDARY
func get_occupant(cell: Vector3i) -> Variant:
return occupancy.get(cell, &"")
func is_excavated(cell: Vector3i) -> bool:
return bool(excavation.get(Vector2i(cell.x, cell.y), true))
func validate_support(_cells: Array[Vector3i], _weight_per_cell: int) -> Dictionary:
return {
&"ok": unsupported_cells.is_empty(),
&"unsupported_cells": unsupported_cells.duplicate(),
&"reason": "Support capacity is insufficient.",
}
func occupy(occupant_id: StringName, cells: Array[Vector3i], _kind: StringName) -> bool:
if fail_all_occupy:
return false
if fail_next_occupy:
fail_next_occupy = false
return false
for cell: Vector3i in cells:
if not String(occupancy.get(cell, &"")).is_empty():
return false
for cell: Vector3i in cells:
occupancy[cell] = occupant_id
return true
func release(occupant_id: StringName) -> bool:
if fail_next_release:
fail_next_release = false
return false
var cells: Array[Vector3i] = []
for cell: Vector3i in occupancy:
if occupancy[cell] == occupant_id:
cells.append(cell)
if cells.is_empty():
return false
for cell: Vector3i in cells:
occupancy.erase(cell)
return true
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: Construction Service")
_test_report_collects_every_reason()
_test_start_is_atomic()
_test_cancellation_refund()
_test_completion_and_demolition()
_test_completion_rollback_and_retry()
_test_completion_rollback_failure_quarantines()
_test_progress_replay_is_idempotent()
_test_deferred_completion_commit_and_reentry()
_test_deferred_failure_discard_and_restore()
_test_ordinary_advance_notifications_unchanged()
_test_snapshot_round_trip()
_test_snapshot_strictness_and_v1_migration()
_test_definition_type_spoof_and_reentry()
_test_failed_rollback_quarantines_service()
_test_real_world_service_integration()
_test_underground_requires_excavation()
_test_every_footprint_cell_must_match_zone()
_test_boundary_facility_anchor_rule()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_report_collects_every_reason() -> void:
var world := FakeWorld.new()
world.max_x = 2
var state := FakeResourceState.new()
state.resources = {&"gold": 0, &"materials": 0}
var definition := _definition(
&"invalid_location",
Vector2i(2, 1),
FacilityDefinitionType.MapZone.ABOVE_GROUND,
FacilityDefinitionType.ViewLayer.FRONT,
{&"gold": 100, &"materials": 10}
)
var origin := Vector3i(2, -1, FacilityDefinitionType.ViewLayer.BACK)
world.occupancy[origin] = &"existing"
world.unsupported_cells = [origin]
var service := ConstructionServiceType.new(world, state)
var report = service.validate_placement(definition, origin)
_expect(not report.can_build, "invalid placement is rejected")
_expect(report.has_error(PlacementReportType.ERROR_OUT_OF_BOUNDS), "bounds error is reported")
_expect(report.has_error(PlacementReportType.ERROR_ZONE_MISMATCH), "zone error is reported")
_expect(report.has_error(PlacementReportType.ERROR_LAYER_MISMATCH), "layer error is reported")
_expect(report.has_error(PlacementReportType.ERROR_OVERLAP), "overlap error is reported")
_expect(report.has_error(PlacementReportType.ERROR_UNSUPPORTED), "support error is reported")
_expect(
report.has_error(PlacementReportType.ERROR_INSUFFICIENT_RESOURCES),
"cost error is reported"
)
_expect(report.reasons.size() == 6, "all independent placement failures are retained")
func _test_start_is_atomic() -> void:
var world := FakeWorld.new()
var state := FakeResourceState.new()
var service := ConstructionServiceType.new(world, state)
var definition := _valid_definition()
var resources_before := state.resources.duplicate(true)
world.fail_next_occupy = true
var failed = service.start_construction(definition, Vector3i(1, 1, 0))
_expect(not failed.is_success(), "failed world reservation rejects construction")
_expect(state.resources == resources_before, "failed reservation rolls resource payment back")
_expect(world.occupancy.is_empty(), "failed reservation leaves no occupied cell")
_expect(service.get_jobs().is_empty(), "failed reservation creates no job")
var started = service.start_construction(definition, Vector3i(1, 1, 0), 0, 4, 600)
_expect(started.is_success(), "valid construction starts")
var job = service.get_job(started.details[&"job_id"])
_expect(job != null and job.state == ConstructionJobType.State.PLANNED, "new job starts PLANNED")
_expect(state.resources == {&"gold": 900, &"materials": 90}, "full construction cost is paid once")
_expect(world.get_occupant(Vector3i(1, 1, 0)) == job.job_id, "job atomically reserves its cells")
func _test_cancellation_refund() -> void:
var world := FakeWorld.new()
var state := FakeResourceState.new()
var service := ConstructionServiceType.new(world, state)
var first = service.start_construction(_valid_definition(), Vector3i(1, 1, 0), 0, 4)
var first_job_id: StringName = first.details[&"job_id"]
var cancelled = service.cancel_job(first_job_id)
_expect(cancelled.is_success(), "zero-progress job cancels")
_expect(state.resources == {&"gold": 1_000, &"materials": 100}, "zero-progress cancellation refunds all")
_expect(world.occupancy.is_empty(), "cancellation releases reserved cells")
var second = service.start_construction(_valid_definition(), Vector3i(2, 2, 0), 0, 4)
var second_job_id: StringName = second.details[&"job_id"]
service.progress_job(second_job_id)
var job = service.get_job(second_job_id)
_expect(job.progress == 2_500 and job.state == ConstructionJobType.State.BUILDING, "one minute advances exact integer progress")
var partial = service.cancel_job(second_job_id)
_expect(partial.details[&"refund"] == {&"gold": 75, &"materials": 7}, "partial cancellation refunds the unbuilt ratio")
_expect(state.resources == {&"gold": 975, &"materials": 97}, "partial refund is applied once")
func _test_completion_and_demolition() -> void:
var world := FakeWorld.new()
var state := FakeResourceState.new()
var service := ConstructionServiceType.new(world, state)
var started = service.start_construction(_valid_definition(), Vector3i(3, 3, 0), 0, 2)
var job_id: StringName = started.details[&"job_id"]
var first_tick = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 601)
_expect(first_tick.is_success() and service.get_job(job_id).progress == 5_000, "first build minute reaches half progress")
var second_tick = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 602)
var job = service.get_job(job_id)
_expect(second_tick.is_success() and job.state == ConstructionJobType.State.COMPLETED, "final minute completes the job")
var instance_id: StringName = second_tick.details[&"instance_id"]
_expect(not service.get_facility(instance_id).is_empty(), "completion creates a facility instance")
_expect(world.get_occupant(Vector3i(3, 3, 0)) == instance_id, "completion replaces job reservation with facility occupancy")
var demolished = service.demolish_facility(instance_id)
_expect(demolished.is_success(), "completed facility can be demolished")
_expect(world.occupancy.is_empty(), "demolition releases facility cells")
_expect(state.resources == {&"gold": 900, &"materials": 92}, "demolition returns 25 percent of materials")
func _test_completion_rollback_and_retry() -> void:
var world := FakeWorld.new()
var service := ConstructionServiceType.new(world, FakeResourceState.new())
var started = service.start_construction(_valid_definition(), Vector3i(3, 3, 0), 0, 2)
var job_id: StringName = started.details[&"job_id"]
service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 10)
world.fail_next_occupy = true
var failed = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 11)
var job = service.get_job(job_id)
_expect(not failed.is_success() and failed.details[&"rolled_back"], "failed completion reports a verified world rollback")
_expect(job.state == ConstructionJobType.State.BLOCKED and job.progress == ConstructionJobType.MAX_PROGRESS, "interrupted completion retains all finished work")
_expect(world.get_occupant(Vector3i(3, 3, 0)) == job_id, "failed facility transfer restores the construction reservation")
var retry = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 12)
_expect(retry.is_success() and service.get_job(job_id).state == ConstructionJobType.State.COMPLETED, "a later minute deterministically retries the blocked completion")
func _test_completion_rollback_failure_quarantines() -> void:
var world := FakeWorld.new()
var service := ConstructionServiceType.new(world, FakeResourceState.new())
var job_id: StringName = service.start_construction(_valid_definition(), Vector3i(3, 3, 0), 0, 2).details[&"job_id"]
service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 30)
world.fail_all_occupy = true
var failed = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 31)
_expect(not failed.is_success() and failed.details[&"rollback_failed"], "completion exposes failure to restore its reservation")
_expect(service.is_quarantined(), "unrecoverable completion rollback quarantines construction mutations")
_expect(not service.cancel_job(job_id).is_success(), "quarantined completion state cannot be mutated further")
func _test_progress_replay_is_idempotent() -> void:
var world := FakeWorld.new()
var service := ConstructionServiceType.new(world, FakeResourceState.new())
var job_id: StringName = service.start_construction(_valid_definition(), Vector3i(1, 1, 0), 0, 4).details[&"job_id"]
service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 20)
var progress_once := service.get_job(job_id).progress
var replay = service.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 20)
_expect(replay.is_success() and replay.is_replay, "same-minute construction retry is an explicit replay")
_expect(service.get_job(job_id).progress == progress_once, "same-minute construction retry cannot add work twice")
func _test_deferred_completion_commit_and_reentry() -> void:
var world := FakeWorld.new()
var resources := FakeResourceState.new()
var service := ConstructionServiceType.new(world, resources)
var definition := _valid_definition()
var started = service.start_construction(definition, Vector3i(1, 1, 0), 0, 1)
var job_id: StringName = started.details[&"job_id"]
var before_advance: Dictionary = service.create_snapshot()
var order: Array[StringName] = []
var delivered_jobs: Array[Dictionary] = []
var delivered_facilities: Array[Dictionary] = []
var reentry: Dictionary = {}
var job_changed_callback := func(job: ConstructionJob) -> void:
order.append(&"job_changed")
delivered_jobs.append(job.create_snapshot())
job.assigned_builder_ids.append(&"listener_tamper")
var nested_advance: Array[CommandResult] = service.advance_one_minute(102)
reentry[&"advance_rejected"] = (
nested_advance.size() == 1 and not nested_advance[0].is_success()
)
reentry[&"commit_rejected"] = not service.commit_deferred_notifications()
reentry[&"discard_rejected"] = not service.discard_deferred_notifications()
reentry[&"snapshot_rejected"] = service.create_snapshot().is_empty()
reentry[&"restore_rejected"] = not service.restore_snapshot(before_advance)
reentry[&"mutation_rejected"] = not service.start_construction(
definition, Vector3i(5, 5, 0)
).is_success()
var facility_completed_callback := func(instance: Dictionary) -> void:
order.append(&"facility_completed")
delivered_facilities.append(instance.duplicate(true))
(instance[&"occupied_cells"] as Array).clear()
instance[&"definition_id"] = &"listener_tamper"
service.job_changed.connect(job_changed_callback)
service.facility_completed.connect(facility_completed_callback)
var advanced: Array[CommandResult] = service.advance_one_minute(101, {}, true)
_expect(
advanced.size() == 1 and advanced[0].is_success(),
"deferred advance completes construction state"
)
var instance_id := StringName(advanced[0].details.get(&"instance_id", &""))
_expect(
order.is_empty() and delivered_jobs.is_empty() and delivered_facilities.is_empty(),
"deferred completion emits no raw signal before commit"
)
_expect(service.has_deferred_notifications(), "deferred completion exposes a pending notification batch")
_expect(service.create_snapshot().is_empty(), "pending notifications fail closed for snapshot capture")
_expect(
not service.restore_snapshot(before_advance),
"pending notifications reject snapshot restore"
)
var minute_before_rejected_advance := service.current_minute
var pending_advance: Array[CommandResult] = service.advance_one_minute(102)
_expect(
pending_advance.size() == 1
and not pending_advance[0].is_success()
and pending_advance[0].details.get(&"code", &"") == &"DEFERRED_NOTIFICATIONS_PENDING"
and service.current_minute == minute_before_rejected_advance,
"a pending batch rejects another advance before changing its minute"
)
_expect(
not service.start_construction(definition, Vector3i(5, 5, 0)).is_success(),
"a pending batch rejects unrelated mutation until its disposition is chosen"
)
# get_job historically returns the live aggregate. Mutating it after staging
# proves the queued notification captured its own defensive point-in-time copy.
service.get_job(job_id).assigned_builder_ids.append(&"late_live_builder")
_expect(service.commit_deferred_notifications(), "deferred notifications commit explicitly")
_expect(
order == [&"job_changed", &"facility_completed"]
and delivered_jobs.size() == 1
and delivered_facilities.size() == 1,
"commit emits completion signals exactly once in Construction order"
)
_expect(
delivered_jobs[0][&"assigned_builder_ids"].is_empty()
and service.get_job(job_id).assigned_builder_ids == [&"late_live_builder"],
"queued and delivered ConstructionJob payloads are defensive copies"
)
var live_facility: Dictionary = service.get_facility(instance_id)
_expect(
live_facility.get(&"definition_id", &"") == definition.id
and live_facility.get(&"occupied_cells", []) == [Vector3i(1, 1, 0)],
"listener mutation of a deferred facility payload cannot alter live state"
)
_expect(
bool(reentry.get(&"advance_rejected", false))
and bool(reentry.get(&"commit_rejected", false))
and bool(reentry.get(&"discard_rejected", false))
and bool(reentry.get(&"snapshot_rejected", false))
and bool(reentry.get(&"restore_rejected", false))
and bool(reentry.get(&"mutation_rejected", false)),
"signal callbacks cannot reenter notification dispatch, restore, snapshot, or mutation"
)
var committed_order := order.duplicate()
_expect(
not service.has_deferred_notifications()
and service.commit_deferred_notifications()
and order == committed_order,
"an empty repeat commit succeeds without emitting twice"
)
service.job_changed.disconnect(job_changed_callback)
service.facility_completed.disconnect(facility_completed_callback)
func _test_deferred_failure_discard_and_restore() -> void:
var world := FakeWorld.new()
var service := ConstructionServiceType.new(world, FakeResourceState.new())
var started = service.start_construction(_valid_definition(), Vector3i(2, 2, 0), 0, 1)
var before_advance: Dictionary = service.create_snapshot()
var changed_count := [0]
var completed_count := [0]
var job_changed_callback := func(_job: ConstructionJob) -> void:
changed_count[0] += 1
var facility_completed_callback := func(_instance: Dictionary) -> void:
completed_count[0] += 1
service.job_changed.connect(job_changed_callback)
service.facility_completed.connect(facility_completed_callback)
world.fail_next_occupy = true
var advanced: Array[CommandResult] = service.advance_one_minute(201, {}, true)
_expect(
advanced.size() == 1 and not advanced[0].is_success(),
"a deferred completion retains its internal failure result"
)
_expect(
service.has_deferred_notifications()
and changed_count[0] == 0
and completed_count[0] == 0,
"notifications queued before an internal failure remain silent and pending"
)
_expect(
service.create_snapshot().is_empty()
and not service.restore_snapshot(before_advance),
"failed deferred advances still block snapshot and restore while pending"
)
_expect(service.discard_deferred_notifications(), "a failed deferred batch can be discarded")
_expect(
not service.has_deferred_notifications()
and service.discard_deferred_notifications()
and service.commit_deferred_notifications()
and changed_count[0] == 0
and completed_count[0] == 0,
"discard is replay-safe and never emits the rejected batch"
)
_expect(
service.restore_snapshot(before_advance)
and service.create_snapshot() == before_advance,
"discard permits the caller to restore Construction after failure"
)
service.job_changed.disconnect(job_changed_callback)
service.facility_completed.disconnect(facility_completed_callback)
func _test_ordinary_advance_notifications_unchanged() -> void:
var service := ConstructionServiceType.new(FakeWorld.new(), FakeResourceState.new())
var job_id: StringName = service.start_construction(
_valid_definition(), Vector3i(3, 3, 0), 0, 1
).details[&"job_id"]
var order: Array[StringName] = []
var delivered_jobs: Array[ConstructionJob] = []
var job_changed_callback := func(job: ConstructionJob) -> void:
order.append(&"job_changed")
delivered_jobs.append(job)
var facility_completed_callback := func(_instance: Dictionary) -> void:
order.append(&"facility_completed")
service.job_changed.connect(job_changed_callback)
service.facility_completed.connect(facility_completed_callback)
var advanced: Array[CommandResult] = service.advance_one_minute(301, {})
_expect(
advanced.size() == 1 and advanced[0].is_success(),
"the existing two-argument advance call still completes normally"
)
_expect(
order == [&"job_changed", &"facility_completed"]
and delivered_jobs.size() == 1,
"ordinary advance still emits both signals immediately in prior order"
)
_expect(
delivered_jobs[0] == service.get_job(job_id),
"ordinary job_changed retains its historical live-job payload"
)
_expect(
not service.has_deferred_notifications() and not service.create_snapshot().is_empty(),
"ordinary advance creates no ephemeral pending batch"
)
service.job_changed.disconnect(job_changed_callback)
service.facility_completed.disconnect(facility_completed_callback)
func _test_snapshot_round_trip() -> void:
var world := FakeWorld.new()
var state := FakeResourceState.new()
var service := ConstructionServiceType.new(world, state)
var started = service.start_construction(_valid_definition(), Vector3i(4, 4, 0), 0, 8, 700)
var job_id: StringName = started.details[&"job_id"]
service.assign_builders(job_id, [&"builder_b", &"builder_a"])
service.progress_job(job_id, 5_000, 701)
var snapshot := service.create_snapshot()
var restored := ConstructionServiceType.new(world, state)
_expect(restored.restore_snapshot(snapshot), "valid construction snapshot restores")
_expect(restored.create_snapshot() == snapshot, "snapshot restore preserves all construction state")
var before_invalid := restored.create_snapshot()
_expect(not restored.restore_snapshot({&"schema_version": 999}), "unsupported snapshot schema is rejected")
_expect(restored.create_snapshot() == before_invalid, "rejected restore does not mutate live state")
func _test_snapshot_strictness_and_v1_migration() -> void:
var world := FakeWorld.new()
var source := ConstructionServiceType.new(world, FakeResourceState.new())
var job_id: StringName = source.start_construction(_valid_definition(), Vector3i(4, 4, 0), 0, 8, 700).details[&"job_id"]
source.progress_job(job_id, 5_000, 701)
var snapshot := source.create_snapshot()
var restored := ConstructionServiceType.new(world, FakeResourceState.new())
var unknown := snapshot.duplicate(true)
unknown[&"unknown"] = 1
_expect(not restored.restore_snapshot(unknown), "current construction schema rejects unknown top-level keys")
var type_spoof := snapshot.duplicate(true)
type_spoof[&"schema_version"] = "2"
_expect(not restored.restore_snapshot(type_spoof), "current construction schema rejects a string version")
var cost_spoof := snapshot.duplicate(true)
cost_spoof[&"jobs"][0][&"construction_cost"][&"gold"] = "100"
_expect(not restored.restore_snapshot(cost_spoof), "current construction schema rejects numeric-string costs")
var derived_tamper := snapshot.duplicate(true)
derived_tamper[&"jobs"][0][&"progress"] += 1
_expect(not restored.restore_snapshot(derived_tamper), "current construction schema rejects derived progress tampering")
var cell_tamper := snapshot.duplicate(true)
cell_tamper[&"jobs"][0][&"reserved_cells"] = [[5, 4, 0]]
_expect(not restored.restore_snapshot(cell_tamper), "construction restore rejects world reservation cross-link tampering")
var high_water_tamper := snapshot.duplicate(true)
high_water_tamper[&"next_job_sequence"] = 1
_expect(not restored.restore_snapshot(high_water_tamper), "construction restore rejects a sequence below the ID high-water mark")
var before := restored.create_snapshot()
_expect(before[&"jobs"].is_empty(), "rejected construction snapshots leave the target untouched")
var legacy := snapshot.duplicate(true)
legacy[&"schema_version"] = source.LEGACY_SNAPSHOT_VERSION
legacy.erase(&"quarantined")
legacy.erase(&"quarantine_reason")
for saved_job: Dictionary in legacy[&"jobs"]:
saved_job.erase(&"last_progress_at")
_expect(restored.restore_snapshot(legacy), "legacy v1 construction snapshot migrates through its exact old shape")
_expect(restored.create_snapshot()[&"schema_version"] == source.SNAPSHOT_VERSION, "legacy construction state is re-emitted as strict v2")
_expect(restored.progress_job(job_id, ConstructionJobType.NORMAL_WORK_RATE, 701).is_replay, "v1 migration conservatively prevents replaying prior work")
func _test_definition_type_spoof_and_reentry() -> void:
var world := FakeWorld.new()
var resources := FakeResourceState.new()
var service := ConstructionServiceType.new(world, resources)
var spoofed := {
&"id": &"spoofed",
&"footprint": Vector2i.ONE,
&"zone": FacilityDefinitionType.MapZone.ANY,
&"layer": FacilityDefinitionType.ViewLayer.FRONT,
&"construction_cost": {&"gold": "100", &"materials": 10},
&"build_minutes": 60,
}
_expect(not service.validate_placement(spoofed, Vector3i(1, 1, 0)).can_build, "numeric-string definition costs are rejected instead of normalized")
resources.reentrant_service = service
var started = service.start_construction(_valid_definition(), Vector3i(1, 1, 0))
_expect(started.is_success() and resources.reentrant_attempted, "resource callback attempts a nested construction command")
_expect(not resources.reentrant_succeeded and service.get_jobs().size() == 1, "reentrant construction mutation cannot create or charge a second job")
resources.reentrant_service = null
func _test_failed_rollback_quarantines_service() -> void:
var world := FakeWorld.new()
var resources := FakeResourceState.new()
resources.mutate_then_fail_spend = true
resources.fail_restore = true
var service := ConstructionServiceType.new(world, resources)
var failed = service.start_construction(_valid_definition(), Vector3i(1, 1, 0))
_expect(not failed.is_success() and failed.details[&"rollback_failed"], "failed construction payment reports an unrecoverable rollback")
_expect(service.is_quarantined(), "unrecoverable construction rollback quarantines the service")
_expect(not service.start_construction(_valid_definition(), Vector3i(2, 2, 0)).is_success(), "quarantined construction service refuses later mutations")
func _test_real_world_service_integration() -> void:
var world = WorldServiceType.new()
world.reset({&"min_x": 0, &"max_x": 5, &"min_y": 0, &"max_y": 5})
world.set_support(Vector3i(1, 1, 0), 0, true)
var state := FakeResourceState.new()
var service := ConstructionServiceType.new(world, state)
var definition := _definition(
&"rotated_room",
Vector2i(2, 1),
FacilityDefinitionType.MapZone.ABOVE_GROUND,
FacilityDefinitionType.ViewLayer.FRONT,
{&"gold": 100, &"materials": 10}
)
var expected_cells: Array[Vector3i] = [Vector3i(1, 1, 0), Vector3i(1, 2, 0)]
var report = service.validate_placement(definition, Vector3i(1, 1, 0), 90)
_expect(report.can_build, "real WorldService accepts enum-to-StringName zone matching")
_expect(report.cells == expected_cells, "90-degree placement swaps rectangular footprint axes")
var report_270 = service.validate_placement(definition, Vector3i(1, 1, 0), 270)
_expect(report_270.cells == expected_cells, "270-degree placement swaps rectangular footprint axes")
var started = service.start_construction(definition, Vector3i(1, 1, 0), 90, 4)
_expect(started.is_success(), "real WorldService occupancy signature starts construction")
var job_id: StringName = started.details[&"job_id"]
_expect(world.get_entity_cells(job_id) == expected_cells, "real WorldService stores the complete rotated reservation")
_expect(service.cancel_job(job_id).is_success(), "real WorldService release signature cancels construction")
_expect(world.get_entity_cells(job_id).is_empty(), "real WorldService release removes the reservation")
var restarted = service.start_construction(definition, Vector3i(1, 1, 0), 270, 1)
var restarted_job_id: StringName = restarted.details[&"job_id"]
var completed = service.progress_job(restarted_job_id, ConstructionJobType.NORMAL_WORK_RATE, 900)
_expect(completed.is_success(), "real WorldService completes a construction reservation")
var instance_id: StringName = completed.details[&"instance_id"]
_expect(world.get_entity_cells(instance_id) == expected_cells, "completion transfers real world occupancy to the facility")
_expect(service.demolish_facility(instance_id).is_success(), "real WorldService facility can be demolished")
_expect(world.get_entity_cells(instance_id).is_empty(), "real WorldService demolition releases facility occupancy")
func _test_underground_requires_excavation() -> void:
var world = WorldServiceType.new()
world.reset({&"min_x": 0, &"max_x": 3, &"min_y": -2, &"max_y": 0})
world.set_support(Vector3i(0, -1, 0), 0, true)
var state := FakeResourceState.new()
var service := ConstructionServiceType.new(world, state)
var definition := _definition(
&"underground_room",
Vector2i(2, 1),
FacilityDefinitionType.MapZone.UNDERGROUND,
FacilityDefinitionType.ViewLayer.FRONT,
{&"gold": 100, &"materials": 10}
)
var blocked = service.validate_placement(definition, Vector3i(0, -1, 0))
_expect(not blocked.can_build, "underground facility cannot occupy solid earth")
_expect(blocked.has_error(PlacementReportType.ERROR_NOT_EXCAVATED), "placement report identifies unexcavated footprint cells")
_expect(blocked.unexcavated_cells == [Vector3i(0, -1, 0), Vector3i(1, -1, 0)], "every blocked underground footprint cell is reported")
world.set_excavated(Vector3i(0, -1, 0), true)
var half_open = service.validate_placement(definition, Vector3i(0, -1, 0))
_expect(not half_open.can_build and half_open.unexcavated_cells == [Vector3i(1, -1, 0)], "partial excavation is still insufficient")
world.set_excavated(Vector3i(1, -1, 1), true)
var ready = service.validate_placement(definition, Vector3i(0, -1, 0))
_expect(ready.can_build, "fully excavated spatial cells permit underground construction on either layer")
func _test_every_footprint_cell_must_match_zone() -> void:
var world = WorldServiceType.new()
world.reset({&"min_x": 0, &"max_x": 2, &"min_y": 0, &"max_y": 2})
world.set_support(Vector3i(0, 0, 0), 0, true)
var service := ConstructionServiceType.new(world, FakeResourceState.new())
var definition := _definition(
&"above_ground_tower",
Vector2i(1, 2),
FacilityDefinitionType.MapZone.ABOVE_GROUND,
FacilityDefinitionType.ViewLayer.FRONT,
{&"gold": 100, &"materials": 10}
)
var crossing = service.validate_placement(definition, Vector3i(0, 0, 0))
_expect(not crossing.can_build, "a footprint cannot straddle the boundary and above-ground zones")
_expect(crossing.zone_mismatch_cells == [Vector3i(0, 0, 0)], "zone validation reports the exact mismatching footprint cell")
var valid = service.validate_placement(definition, Vector3i(0, 1, 0))
_expect(valid.can_build, "the same full footprint is valid when every cell matches its zone")
func _test_boundary_facility_anchor_rule() -> void:
var world := FakeWorld.new()
world.max_x = 8
var service := ConstructionServiceType.new(world, FakeResourceState.new())
var reception := _definition(
&"boundary_reception",
Vector2i(4, 2),
FacilityDefinitionType.MapZone.BOUNDARY,
FacilityDefinitionType.ViewLayer.FRONT,
{&"gold": 100, &"materials": 10}
)
var anchored = service.validate_placement(reception, Vector3i(0, 0, 0))
_expect(anchored.can_build, "boundary reception anchored at y=0 may extend into above-ground cells")
var above_anchor = service.validate_placement(reception, Vector3i(0, 1, 0))
_expect(not above_anchor.can_build and above_anchor.has_error(PlacementReportType.ERROR_ZONE_MISMATCH), "boundary reception rejects an above-ground y=1 anchor")
var below_anchor = service.validate_placement(reception, Vector3i(0, -1, 0))
_expect(not below_anchor.can_build and below_anchor.has_error(PlacementReportType.ERROR_ZONE_MISMATCH), "boundary reception rejects an underground y=-1 anchor")
world.forced_cells = [
Vector3i(0, -1, 0), Vector3i(1, -1, 0), Vector3i(2, -1, 0), Vector3i(3, -1, 0),
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0), Vector3i(3, 0, 0),
]
var underground_spill = service.validate_placement(reception, Vector3i(0, 0, 0))
_expect(not underground_spill.can_build, "boundary footprint may not spill below y=0 even with a valid anchor")
_expect(underground_spill.zone_mismatch_cells.has(Vector3i(0, -1, 0)), "boundary spill reports the exact underground cells")
func _definition(
id: StringName,
footprint: Vector2i,
zone: int,
layer: int,
cost: Dictionary
) -> FacilityDefinition:
var definition := FacilityDefinitionType.new()
definition.id = id
definition.display_name = String(id)
definition.footprint = footprint
definition.zone = zone
definition.layer = layer
definition.construction_cost = cost.duplicate(true)
return definition
func _valid_definition() -> FacilityDefinition:
return _definition(
&"test_room",
Vector2i(1, 1),
FacilityDefinitionType.MapZone.ANY,
FacilityDefinitionType.ViewLayer.FRONT,
{&"gold": 100, &"materials": 10}
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://d1wplc6ml6vei
+285
View File
@@ -0,0 +1,285 @@
extends SceneTree
const ExcavationServiceType = preload("res://scripts/construction/excavation_service.gd")
const WorldServiceType = preload("res://scripts/world/world_service.gd")
var _checks := 0
var _failures := 0
class FakeResourceState:
extends RefCounted
var resources := {&"gold": 1_000, &"materials": 100}
var mutate_then_fail_spend := false
var fail_restore := false
var reentrant_service: Variant
var reentrant_attempted := false
var reentrant_succeeded := false
func get_resource(resource_name: StringName) -> int:
return int(resources.get(resource_name, 0))
func spend(cost: Dictionary) -> bool:
if reentrant_service != null and not reentrant_attempted:
reentrant_attempted = true
var nested: Variant = reentrant_service.start_excavation(Vector3i(1, -1, 0), 1)
reentrant_succeeded = nested.is_success()
if mutate_then_fail_spend:
resources[&"gold"] = int(resources[&"gold"]) - int(cost.get(&"gold", 0))
return false
for resource_name: Variant in cost:
if get_resource(StringName(resource_name)) < int(cost[resource_name]):
return false
for resource_name: Variant in cost:
var key := StringName(resource_name)
resources[key] = int(resources[key]) - int(cost[resource_name])
return true
func apply_resource_deltas(deltas: Dictionary) -> bool:
var candidate := resources.duplicate(true)
for resource_name: Variant in deltas:
var key := StringName(resource_name)
candidate[key] = int(candidate.get(key, 0)) + int(deltas[resource_name])
if int(candidate[key]) < 0:
return false
resources = candidate
return true
func create_snapshot() -> Dictionary:
return {&"resources": resources.duplicate(true)}
func restore_snapshot(snapshot: Dictionary) -> bool:
if fail_restore:
return false
if not snapshot.get(&"resources", null) is Dictionary:
return false
resources = (snapshot[&"resources"] as Dictionary).duplicate(true)
return true
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: Excavation Service")
_test_rules_adjacency_and_two_layer_unlock()
_test_disconnected_island_is_rejected()
_test_pause_resume_preserves_progress()
_test_hard_width_requires_support()
_test_hard_cluster_is_not_a_four_cell_span()
_test_progress_replay_is_idempotent()
_test_snapshot_round_trip_and_atomic_rejection()
_test_snapshot_schema_strictness_and_v1_migration()
_test_reentry_and_failed_rollback_quarantine()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_rules_adjacency_and_two_layer_unlock() -> void:
var world := _world(0, 3, -3, 0)
var resources := FakeResourceState.new()
var service := ExcavationServiceType.new(world, resources)
_expect(service.get_rule(0) == {&"gold": 20, &"materials": 1, &"minutes": 30}, "hardness 0 uses the canonical cost and time")
_expect(service.get_rule(1) == {&"gold": 50, &"materials": 1, &"minutes": 60}, "hardness 1 uses the canonical cost and time")
_expect(service.get_rule(2) == {&"gold": 100, &"materials": 2, &"minutes": 120}, "hardness 2 uses the canonical cost and time")
_expect(service.get_rule(3) == {&"gold": 200, &"materials": 4, &"minutes": 240}, "hardness 3 uses the canonical cost and time")
_expect(service.set_hardness(Vector2i(0, -1), 1), "underground hardness can be assigned before work")
var report := service.validate_excavation(Vector3i(0, -1, 1))
_expect(report[&"ok"] and report[&"entrance_connected"], "first underground cell connects to the boundary entrance")
_expect(report[&"cost"] == {&"gold": 50, &"materials": 1} and report[&"build_minutes"] == 60, "validation exposes exact hardness tuning")
var started := service.start_excavation(Vector3i(0, -1, 0), 360)
_expect(started.is_success(), "valid connected excavation starts")
_expect(resources.resources == {&"gold": 950, &"materials": 99}, "excavation payment is charged exactly once")
var job_id: StringName = started.details[&"job_id"]
for minute: int in range(360, 420):
service.progress_job(job_id, service.NORMAL_WORK_RATE, minute)
_expect(world.is_excavated(Vector3i(0, -1, 0)), "completed excavation unlocks FRONT")
_expect(world.is_excavated(Vector3i(0, -1, 1)), "the same completed excavation unlocks BACK")
_expect(service.get_job(job_id)[&"state"] == service.STATE_COMPLETED, "completed job becomes terminal")
_expect(not service.validate_excavation(Vector3i(0, -1, 0))[&"ok"], "an excavated cell cannot be charged twice")
_expect(service.validate_invariants()[&"ok"], "completed excavation retains valid invariants")
func _test_disconnected_island_is_rejected() -> void:
var world := _world(0, 4, -4, 0)
var resources := FakeResourceState.new()
var service := ExcavationServiceType.new(world, resources)
world.set_excavated(Vector3i(4, -4, 0), true)
var report := service.validate_excavation(Vector3i(3, -4, 0))
_expect(not report[&"ok"], "an island next to an excavated cell is still rejected")
_expect(report[&"errors"].has(service.ERROR_NO_ENTRANCE_CONNECTION), "island rejection identifies the missing entrance connection")
_expect(resources.resources == {&"gold": 1_000, &"materials": 100}, "failed validation changes no resources")
var no_neighbor := service.validate_excavation(Vector3i(1, -3, 0))
_expect(no_neighbor[&"errors"].has(service.ERROR_NO_EXCAVATED_NEIGHBOR), "a cell without any excavated neighbor has a distinct reason")
func _test_pause_resume_preserves_progress() -> void:
var world := _world(0, 2, -2, 0)
var resources := FakeResourceState.new()
var service := ExcavationServiceType.new(world, resources)
var started := service.start_excavation(Vector3i(0, -1, 0), 100)
var job_id: StringName = started.details[&"job_id"]
for minute: int in range(100, 110):
service.progress_job(job_id, service.NORMAL_WORK_RATE, minute)
var progress_before := int(service.get_job(job_id)[&"progress"])
_expect(progress_before > 0 and progress_before < service.MAX_PROGRESS, "work accumulates as deterministic fixed-point progress")
_expect(service.pause_job(job_id).is_success(), "active excavation can be paused")
service.advance_one_minute(111)
_expect(service.get_job(job_id)[&"progress"] == progress_before, "paused time does not discard or advance progress")
_expect(service.resume_job(job_id).is_success(), "connected paused excavation can resume")
for minute: int in range(112, 132):
service.progress_job(job_id, service.NORMAL_WORK_RATE, minute)
_expect(service.get_job(job_id)[&"state"] == service.STATE_COMPLETED, "resumed work completes from retained progress")
_expect(service.pause_job(job_id).details[&"code"] == &"TERMINAL_STATE", "completed excavation cannot return to a mutable state")
func _test_hard_width_requires_support() -> void:
var world := _world(0, 4, -1, 0)
var resources := FakeResourceState.new()
var service := ExcavationServiceType.new(world, resources)
for x: int in range(4):
_expect(service.set_hardness(Vector2i(x, -1), 2), "hard cell %d is configured" % x)
for x: int in range(3):
world.set_excavated(Vector3i(x, -1, 0), true)
var unsupported := service.validate_excavation(Vector3i(3, -1, 0))
_expect(unsupported[&"support_required"], "four connected hardness-2 cells trigger support")
_expect(unsupported[&"errors"].has(service.ERROR_SUPPORT_REQUIRED), "unsupported hard span cannot start")
world.set_support(Vector3i(2, -1, 1), 1)
var supported := service.validate_excavation(Vector3i(3, -1, 0))
_expect(supported[&"ok"] and supported[&"support_required"], "nearby support makes the same hard span legal")
_expect(service.start_excavation(Vector3i(3, -1, 1)).is_success(), "supported hard excavation can reserve its spatial cell")
_expect(not service.start_excavation(Vector3i(3, -1, 0)).is_success(), "FRONT and BACK cannot reserve the same spatial excavation twice")
func _test_hard_cluster_is_not_a_four_cell_span() -> void:
var world := _world(0, 1, -2, 0)
var service := ExcavationServiceType.new(world, FakeResourceState.new())
for position: Vector2i in [Vector2i(0, -1), Vector2i(1, -1), Vector2i(0, -2), Vector2i(1, -2)]:
service.set_hardness(position, 2)
world.set_excavated(Vector3i(0, -1, 0), true)
world.set_excavated(Vector3i(1, -1, 0), true)
world.set_excavated(Vector3i(0, -2, 0), true)
var report := service.validate_excavation(Vector3i(1, -2, 0))
_expect(report[&"ok"], "a connected 2x2 hard cluster remains excavatable without support")
_expect(not report[&"support_required"], "support is based on four collinear cells, not component cardinality")
func _test_progress_replay_is_idempotent() -> void:
var world := _world(0, 2, -2, 0)
var service := ExcavationServiceType.new(world, FakeResourceState.new())
var started := service.start_excavation(Vector3i(0, -1, 0), 5)
var job_id: StringName = started.details[&"job_id"]
var first := service.progress_job(job_id, service.NORMAL_WORK_RATE, 10)
var progress_once := int(service.get_job(job_id)[&"progress"])
var duplicate := service.progress_job(job_id, service.NORMAL_WORK_RATE, 10)
_expect(first.is_success() and duplicate.is_success() and duplicate.is_replay, "same-minute excavation retry is an explicit replay")
_expect(service.get_job(job_id)[&"progress"] == progress_once, "same-minute retry cannot add work twice")
var snapshot := service.create_snapshot()
var restored := ExcavationServiceType.new(world, FakeResourceState.new())
_expect(restored.restore_snapshot(snapshot), "last processed excavation minute survives snapshot restore")
var restored_retry := restored.progress_job(job_id, service.NORMAL_WORK_RATE, 10)
_expect(restored_retry.is_replay and restored.get_job(job_id)[&"progress"] == progress_once, "restored service also rejects the already-applied minute")
func _test_snapshot_round_trip_and_atomic_rejection() -> void:
var world := _world(0, 3, -2, 0)
var resources := FakeResourceState.new()
var source := ExcavationServiceType.new(world, resources)
source.set_hardness(Vector2i(0, -1), 1)
var completed_id: StringName = source.start_excavation(Vector3i(0, -1, 0), 10).details[&"job_id"]
for minute: int in range(10, 70):
source.progress_job(completed_id, source.NORMAL_WORK_RATE, minute)
source.set_hardness(Vector2i(0, -2), 3)
var active_id: StringName = source.start_excavation(Vector3i(0, -2, 0), 70).details[&"job_id"]
for minute: int in range(70, 75):
source.progress_job(active_id, source.NORMAL_WORK_RATE, minute)
var snapshot := source.create_snapshot()
var restored := ExcavationServiceType.new(world, resources)
_expect(restored.restore_snapshot(snapshot), "valid excavation snapshot restores against the same world")
_expect(restored.create_snapshot() == snapshot, "excavation snapshot round trip is exact")
_expect(restored.validate_invariants()[&"ok"], "restored active and completed jobs satisfy cross-links")
var before := restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"jobs"][1][&"progress"] = 9_999
_expect(not restored.restore_snapshot(corrupt), "derived progress tampering is rejected")
_expect(restored.create_snapshot() == before, "failed excavation restore preserves the live state atomically")
var duplicate := snapshot.duplicate(true)
duplicate[&"jobs"].append(duplicate[&"jobs"][1].duplicate(true))
_expect(not restored.restore_snapshot(duplicate), "duplicate job identity is rejected")
_expect(restored.create_snapshot() == before, "all rejected snapshot shapes remain non-mutating")
func _test_snapshot_schema_strictness_and_v1_migration() -> void:
var world := _world(0, 2, -2, 0)
var source := ExcavationServiceType.new(world, FakeResourceState.new())
var job_id: StringName = source.start_excavation(Vector3i(0, -1, 0), 10).details[&"job_id"]
source.progress_job(job_id, source.NORMAL_WORK_RATE, 11)
var snapshot := source.create_snapshot()
var restored := ExcavationServiceType.new(world, FakeResourceState.new())
var unknown := snapshot.duplicate(true)
unknown[&"unknown"] = 1
_expect(not restored.restore_snapshot(unknown), "current excavation schema rejects unknown top-level keys")
var type_spoof := snapshot.duplicate(true)
type_spoof[&"schema_version"] = "2"
_expect(not restored.restore_snapshot(type_spoof), "current excavation schema rejects a string version")
var cost_spoof := snapshot.duplicate(true)
cost_spoof[&"jobs"][0][&"cost"][&"gold"] = "20"
_expect(not restored.restore_snapshot(cost_spoof), "current excavation schema rejects numeric-string costs")
var derived_tamper := snapshot.duplicate(true)
derived_tamper[&"jobs"][0][&"build_minutes"] = 31
_expect(not restored.restore_snapshot(derived_tamper), "current excavation schema rejects canonical-rule tampering")
var legacy := snapshot.duplicate(true)
legacy[&"schema_version"] = source.LEGACY_SNAPSHOT_VERSION
legacy.erase(&"quarantined")
legacy.erase(&"quarantine_reason")
for saved_job: Dictionary in legacy[&"jobs"]:
saved_job.erase(&"last_progress_at")
_expect(restored.restore_snapshot(legacy), "legacy v1 excavation snapshot migrates through its exact old shape")
_expect(restored.create_snapshot()[&"schema_version"] == source.SNAPSHOT_VERSION, "legacy excavation state is re-emitted as strict v2")
_expect(restored.progress_job(job_id, source.NORMAL_WORK_RATE, 11).is_replay, "v1 migration conservatively prevents replaying prior progress")
func _test_reentry_and_failed_rollback_quarantine() -> void:
var world := _world(0, 2, -2, 0)
var reentrant_resources := FakeResourceState.new()
var service := ExcavationServiceType.new(world, reentrant_resources)
reentrant_resources.reentrant_service = service
var started := service.start_excavation(Vector3i(0, -1, 0), 1)
_expect(started.is_success() and reentrant_resources.reentrant_attempted, "resource callback attempts a nested excavation command")
_expect(not reentrant_resources.reentrant_succeeded and service.get_jobs().size() == 1, "reentrant excavation mutation is rejected before a second charge or reservation")
reentrant_resources.reentrant_service = null
var broken_resources := FakeResourceState.new()
broken_resources.mutate_then_fail_spend = true
broken_resources.fail_restore = true
var broken := ExcavationServiceType.new(_world(0, 1, -1, 0), broken_resources)
var failed := broken.start_excavation(Vector3i(0, -1, 0), 2)
_expect(not failed.is_success() and failed.details[&"rollback_failed"], "failed excavation payment reports an unrecoverable rollback")
_expect(broken.is_quarantined(), "unrecoverable excavation rollback quarantines the service")
_expect(not broken.start_excavation(Vector3i(1, -1, 0), 3).is_success(), "quarantined excavation service refuses later mutations")
func _world(min_x: int, max_x: int, min_y: int, max_y: int) -> WorldService:
var world := WorldServiceType.new()
world.reset({
&"min_x": min_x,
&"max_x": max_x,
&"min_y": min_y,
&"max_y": max_y,
})
return world
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://b5tbd0rocd1ir
@@ -0,0 +1,571 @@
extends SceneTree
const ProjectorType = preload("res://scripts/world/facility_infrastructure_projector.gd")
const AdapterType = preload("res://scripts/world/route_subslot_path_adapter.gd")
const SpatialType = preload("res://scripts/world/spatial_subslot_service.gd")
const PathType = preload("res://scripts/world/path_service.gd")
const WorldType = preload("res://scripts/world/world_service.gd")
const FacilityDefinitionType = preload("res://scripts/data/facility_definition.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
const GUEST_CORRIDOR = preload("res://data/facilities/guest_corridor.tres")
const SERVICE_CORRIDOR = preload("res://data/facilities/service_corridor.tres")
const STAFF_DOOR = preload("res://data/facilities/staff_door.tres")
const GUEST_ELEVATOR = preload("res://data/facilities/guest_elevator.tres")
const SERVICE_ELEVATOR = preload("res://data/facilities/service_elevator.tres")
class ControlledPathService:
extends PathService
var fail_next_rebuild_after_mutation := false
var fail_next_connector_after_mutation := false
var fail_next_remove_after_mutation := false
var reject_restore := false
var reenter_next_connector := false
var tamper_projector_next_connector := false
var projector: Variant = null
var reentry_result: Variant = null
func rebuild_graph(passable_cells: Variant) -> bool:
var should_fail := fail_next_rebuild_after_mutation
fail_next_rebuild_after_mutation = false
var result := super.rebuild_graph(passable_cells)
if should_fail:
last_error = "Injected graph rebuild failure after mutation."
return false
return result
func configure_connector(
connector_id: StringName,
from_cell: Vector3i,
to_cell: Vector3i,
cost := DEFAULT_CONNECTOR_COST,
bidirectional := true,
access_id: StringName = &"",
enabled := true,
options: Dictionary = {}
) -> bool:
var should_fail := fail_next_connector_after_mutation
fail_next_connector_after_mutation = false
var should_reenter := reenter_next_connector
reenter_next_connector = false
var should_tamper := tamper_projector_next_connector
tamper_projector_next_connector = false
var result := super.configure_connector(
connector_id, from_cell, to_cell, cost, bidirectional, access_id, enabled, options
)
if should_reenter and projector != null:
reentry_result = projector.unproject_facility(&"nested_unproject", &"facility_nested")
if should_tamper and projector != null:
projector.projection_revision += 100
if should_fail:
last_error = "Injected connector failure after mutation."
return false
return result
func remove_connector(connector_id: StringName) -> bool:
var should_fail := fail_next_remove_after_mutation
fail_next_remove_after_mutation = false
var result := super.remove_connector(connector_id)
if should_fail:
last_error = "Injected connector removal failure after mutation."
return false
return result
func restore_snapshot(snapshot: Dictionary) -> bool:
if reject_restore:
last_error = "Injected Path snapshot restore rejection."
return false
return super.restore_snapshot(snapshot)
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: completed-facility infrastructure projection")
_test_corridors_and_primary_world_coexistence()
_test_staff_door_access_and_stable_ids()
_test_project_unproject_replay_and_defensive_queries()
_test_vertical_topology_rejects_unchanged()
_test_definition_and_instance_crosslink_validation()
_test_project_and_unproject_failures_roll_back_exactly()
_test_callback_reentry_tamper_and_quarantine_recovery()
_test_strict_snapshot_round_trip_and_atomic_rejection()
_test_configuration_and_stale_dependency_boundaries()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_corridors_and_primary_world_coexistence() -> void:
var front_start := Vector3i(0, 0, PathType.FRONT)
var front_mid := Vector3i(1, 0, PathType.FRONT)
var front_goal := Vector3i(2, 0, PathType.FRONT)
var back_start := Vector3i(0, 2, PathType.BACK)
var back_mid := Vector3i(1, 2, PathType.BACK)
var back_goal := Vector3i(2, 2, PathType.BACK)
var context := _context([front_start, front_goal, back_start, back_goal])
var world := WorldType.new()
world.reset({&"min_x": -2, &"max_x": 4, &"min_y": -2, &"max_y": 4})
_expect(world.occupy(&"facility_00000001", [front_mid], &"facility"), "completed facility occupies the primary World slot")
var guest_instance := _instance(GUEST_CORRIDOR, &"facility_00000001", front_mid, 10, &"construction_00000001")
var guest_result = context.projector.project_facility(&"project_guest", GUEST_CORRIDOR, guest_instance)
_expect(guest_result.is_success(), "guest corridor projects through the atomic boundary")
_expect(context.path.find_path(front_start, front_goal) == [front_start, front_mid, front_goal], "guest corridor makes the real FRONT Path reachable")
_expect(world.get_occupant(front_mid) == &"facility_00000001", "ROUTE subslot projection preserves primary World occupancy")
var guest_projection: Dictionary = context.projector.get_projection(&"facility_00000001")
_expect(guest_projection[&"layer"] == PathType.FRONT and not guest_projection[&"staff_only"], "guest corridor metadata preserves FRONT guest meaning")
var service_instance := _instance(SERVICE_CORRIDOR, &"facility_00000002", back_mid, 11, &"construction_00000002")
var service_result = context.projector.project_facility(&"project_service", SERVICE_CORRIDOR, service_instance)
_expect(service_result.is_success(), "service corridor projects through the same boundary")
_expect(context.path.find_path(back_start, back_goal) == [back_start, back_mid, back_goal], "service corridor makes the real BACK Path reachable")
_expect(context.path.find_path(back_start, back_goal, [PathType.FRONT]).is_empty(), "FRONT-only guest profile cannot use the staff-only BACK corridor")
var service_projection: Dictionary = context.projector.get_projection(&"facility_00000002")
_expect(service_projection[&"layer"] == PathType.BACK and service_projection[&"staff_only"], "service corridor retains explicit staff-only semantics")
_expect(context.path.find_path(front_start, back_goal, [PathType.FRONT, PathType.BACK]).is_empty(), "FRONT and BACK remain separated without a staff door")
_expect(context.projector.projection_revision == 2, "corridor projections advance one deterministic revision each")
_expect(context.projector.validate_invariants()[&"ok"], "corridor route/entity/cell crosslinks are valid")
func _test_staff_door_access_and_stable_ids() -> void:
var front := Vector3i(5, 0, PathType.FRONT)
var back := Vector3i(5, 0, PathType.BACK)
var context := _context([front])
var instance := _instance(STAFF_DOOR, &"facility_00000003", back, 20, &"construction_00000003")
var result = context.projector.project_facility(&"project_staff_door", STAFF_DOOR, instance)
_expect(result.is_success(), "staff door projects its BACK endpoint and connector atomically")
var projection: Dictionary = context.projector.get_projection(&"facility_00000003")
_expect(projection[&"route_entity_id"] == &"infra.route:facility_00000003", "staff door route entity ID is stable")
_expect(projection[&"connector_id"] == &"infra.connector:facility_00000003", "staff door connector ID is stable")
_expect(context.source.get_entity_cells(projection[&"route_entity_id"]) == [back], "staff door owns exactly its BACK ROUTE endpoint")
var connector: Dictionary = context.path.get_connector(projection[&"connector_id"])
_expect(connector[&"from"] == front and connector[&"to"] == back, "connector joins the same coordinate across FRONT/BACK")
_expect(connector[&"cost"] == 120 and connector[&"bidirectional"] and connector[&"access_id"] == &"staff", "connector is bidirectional, cost 120, and staff-gated")
_expect(context.path.find_path(front, back, [PathType.FRONT, PathType.BACK], []).is_empty(), "guest access cannot traverse the staff door")
_expect(context.path.last_no_route_reason != context.path.ROUTE_OK, "guest denial reports a real no-route reason rather than fake reachability")
_expect(context.path.find_path(front, back, [PathType.FRONT, PathType.BACK], [&"staff"]) == [front, back], "staff connector access traverses the real door")
_expect(context.path.last_path_cost == 120, "staff door route report uses the exact specification cost")
_expect(context.path.calculate_path_cost([front, back], [&"staff"]) == 120, "explicit Path cost calculation preserves staff-door cost 120")
_expect(context.projector.validate_invariants()[&"ok"], "staff-door connector and route crosslinks are exact")
func _test_project_unproject_replay_and_defensive_queries() -> void:
var cell := Vector3i(1, 4, PathType.FRONT)
var context := _context([])
var instance := _instance(GUEST_CORRIDOR, &"facility_00000004", cell, 30, &"construction_00000004")
var first = context.projector.project_facility(&"idem_project", GUEST_CORRIDOR, instance)
var after_projector: Dictionary = context.projector.create_snapshot()
var after_source: Dictionary = context.source.create_snapshot()
var after_adapter: Dictionary = context.adapter.create_snapshot()
var after_path: Dictionary = context.path.create_snapshot()
var replay = context.projector.project_facility(&"idem_project", GUEST_CORRIDOR, instance)
_expect(first.is_success() and replay.is_success() and replay.is_replay, "same project request and fingerprint replays")
_expect(replay.details == first.details, "project replay preserves exact result details")
_expect(context.projector.create_snapshot() == after_projector, "project replay does not advance projector state")
_expect(context.source.create_snapshot() == after_source and context.adapter.create_snapshot() == after_adapter and context.path.create_snapshot() == after_path, "project replay leaves all three dependencies exact")
var changed_instance := instance.duplicate(true)
changed_instance[&"completed_at"] = 31
var conflict = context.projector.project_facility(&"idem_project", GUEST_CORRIDOR, changed_instance)
_expect(conflict.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "same request ID with changed completed input conflicts")
var duplicate = context.projector.project_facility(&"another_project", GUEST_CORRIDOR, instance)
_expect(duplicate.code == CommandResultType.Code.DUPLICATE_COMMAND_ID, "another request cannot double-project one facility")
var returned: Dictionary = context.projector.get_projection(&"facility_00000004")
returned[&"cells"].clear()
var ids: Array[StringName] = context.projector.get_projection_ids()
ids.clear()
var status: Dictionary = context.projector.get_status()
status[&"projection_count"] = 99
_expect(context.projector.get_projection(&"facility_00000004")[&"cells"] == [cell], "projection query is a defensive deep copy")
_expect(context.projector.get_projection_ids() == [&"facility_00000004"], "projection ID query is defensive and lexical")
_expect(context.projector.get_status()[&"projection_count"] == 1, "status query is defensive")
var removed = context.projector.unproject_facility(&"idem_remove", &"facility_00000004")
_expect(removed.is_success(), "facility infrastructure unprojects atomically")
_expect(not context.source.has_entity(&"infra.route:facility_00000004") and not context.path.has_cell(cell), "unproject removes the route from both source and real Path")
var removed_snapshot: Dictionary = context.projector.create_snapshot()
var remove_replay = context.projector.unproject_facility(&"idem_remove", &"facility_00000004")
_expect(remove_replay.is_success() and remove_replay.is_replay, "same unproject request replays after the entity is gone")
_expect(context.projector.create_snapshot() == removed_snapshot, "unproject replay is mutation-free")
var remove_conflict = context.projector.unproject_facility(&"idem_remove", &"facility_other")
_expect(remove_conflict.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "unproject request conflict is rejected")
_expect(context.projector.projection_revision == 2, "project and unproject each advance exactly once")
func _test_vertical_topology_rejects_unchanged() -> void:
var context := _context([Vector3i(0, 0, PathType.FRONT)])
var before_projector: Dictionary = context.projector.create_snapshot()
var before_source: Dictionary = context.source.create_snapshot()
var before_adapter: Dictionary = context.adapter.create_snapshot()
var before_path: Dictionary = context.path.create_snapshot()
var guest_instance := _instance(GUEST_ELEVATOR, &"facility_00000005", Vector3i(4, 4, PathType.FRONT), 40, &"construction_00000005")
var guest = context.projector.project_facility(&"guest_elevator", GUEST_ELEVATOR, guest_instance)
_expect(guest.code == CommandResultType.Code.VALIDATION_FAILED and guest.details[&"reason"] == context.projector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY, "guest elevator explicitly reports unsupported vertical topology")
var service_instance := _instance(SERVICE_ELEVATOR, &"facility_00000006", Vector3i(8, 4, PathType.BACK), 41, &"construction_00000006")
var service = context.projector.project_facility(&"service_elevator", SERVICE_ELEVATOR, service_instance)
_expect(service.code == CommandResultType.Code.VALIDATION_FAILED and service.details[&"reason"] == context.projector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY, "service elevator explicitly reports unsupported vertical topology")
_expect(context.projector.create_snapshot() == before_projector, "unsupported vertical facilities leave projector state unchanged")
_expect(context.source.create_snapshot() == before_source and context.adapter.create_snapshot() == before_adapter and context.path.create_snapshot() == before_path, "unsupported vertical facilities leave all authoritative dependencies unchanged")
_expect(context.projector.get_processed_request_ids().is_empty(), "unsupported vertical rejection does not consume request IDs")
func _test_definition_and_instance_crosslink_validation() -> void:
var context := _context([])
var base_instance := _instance(GUEST_CORRIDOR, &"facility_00000007", Vector3i(0, 6, PathType.FRONT), 50, &"construction_00000007")
var before := _all_snapshots(context)
var wrong_layer: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
wrong_layer.layer = FacilityDefinitionType.ViewLayer.BACK
_expect(not context.projector.project_facility(&"bad_definition_layer", wrong_layer, base_instance).is_success(), "definition layer mismatch is rejected")
var wrong_tags: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
wrong_tags.tags = [&"corridor", &"movement", &"route_subslot", &"schema_facility_proxy"]
_expect(not context.projector.project_facility(&"bad_definition_tags", wrong_tags, base_instance).is_success(), "definition tag mismatch is rejected")
var wrong_pass: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
wrong_pass.pass_through = false
_expect(not context.projector.project_facility(&"bad_definition_pass", wrong_pass, base_instance).is_success(), "definition pass-through mismatch is rejected")
var unknown: FacilityDefinition = GUEST_CORRIDOR.duplicate(true)
unknown.id = &"unknown_proxy"
_expect(not context.projector.project_facility(&"unknown_definition", unknown, base_instance).is_success(), "unknown proxy definition is rejected")
var extra := base_instance.duplicate(true)
extra[&"extra"] = true
_expect(not context.projector.project_facility(&"instance_extra", GUEST_CORRIDOR, extra).is_success(), "completed instance with an extra field is rejected")
var wrong_definition := base_instance.duplicate(true)
wrong_definition[&"definition_id"] = &"service_corridor"
_expect(not context.projector.project_facility(&"instance_definition", GUEST_CORRIDOR, wrong_definition).is_success(), "instance-to-definition ID mismatch is rejected")
var wrong_state := base_instance.duplicate(true)
wrong_state[&"state"] = &"PLANNED"
_expect(not context.projector.project_facility(&"instance_state", GUEST_CORRIDOR, wrong_state).is_success(), "non-ACTIVE completed instance is rejected")
var wrong_layer_instance := base_instance.duplicate(true)
wrong_layer_instance[&"layer"] = PathType.BACK
_expect(not context.projector.project_facility(&"instance_layer", GUEST_CORRIDOR, wrong_layer_instance).is_success(), "instance layer crosslink mismatch is rejected")
var wrong_cells := base_instance.duplicate(true)
wrong_cells[&"occupied_cells"] = [Vector3i(1, 6, PathType.FRONT)]
_expect(not context.projector.project_facility(&"instance_cells", GUEST_CORRIDOR, wrong_cells).is_success(), "instance cells outside its definition footprint are rejected")
var duplicate_cells := base_instance.duplicate(true)
duplicate_cells[&"occupied_cells"] = [base_instance[&"origin"], base_instance[&"origin"]]
_expect(not context.projector.project_facility(&"instance_duplicate_cells", GUEST_CORRIDOR, duplicate_cells).is_success(), "duplicate instance cells are rejected")
var float_cost := base_instance.duplicate(true)
float_cost[&"construction_cost"] = {&"gold": 20.0}
_expect(not context.projector.project_facility(&"instance_cost_type", GUEST_CORRIDOR, float_cost).is_success(), "non-integer construction cost is rejected")
var negative_cost := base_instance.duplicate(true)
negative_cost[&"construction_cost"] = {&"gold": -1}
_expect(not context.projector.project_facility(&"instance_cost_negative", GUEST_CORRIDOR, negative_cost).is_success(), "negative construction cost is rejected")
var bad_request = context.projector.project_facility(4, GUEST_CORRIDOR, base_instance)
_expect(bad_request.code == CommandResultType.Code.INVALID_PAYLOAD, "numeric request ID is rejected rather than coerced")
_expect(_all_snapshots(context) == before, "all definition/instance validation failures preserve every service exactly")
func _test_project_and_unproject_failures_roll_back_exactly() -> void:
var path := ControlledPathService.new()
var context := _context([Vector3i(0, 8, PathType.FRONT)], path)
path.projector = context.projector
var guest := _instance(GUEST_CORRIDOR, &"facility_00000008", Vector3i(1, 8, PathType.FRONT), 60, &"construction_00000008")
var before := _all_snapshots(context)
path.fail_next_rebuild_after_mutation = true
var route_failure = context.projector.project_facility(&"retry_route_failure", GUEST_CORRIDOR, guest)
_assert_exact_rollback(context, before, route_failure, "route adapter Path rebuild failure")
_expect(context.projector.project_facility(&"retry_route_failure", GUEST_CORRIDOR, guest).is_success(), "failed project request can retry after exact rollback")
var door := _instance(STAFF_DOOR, &"facility_00000009", Vector3i(3, 8, PathType.BACK), 61, &"construction_00000009")
before = _all_snapshots(context)
path.fail_next_connector_after_mutation = true
var connector_failure = context.projector.project_facility(&"retry_connector_failure", STAFF_DOOR, door)
_assert_exact_rollback(context, before, connector_failure, "staff connector apply failure")
_expect(not context.source.has_entity(&"infra.route:facility_00000009") and not context.path.has_connector(&"infra.connector:facility_00000009"), "failed staff-door projection leaves neither route nor connector")
_expect(context.projector.project_facility(&"retry_connector_failure", STAFF_DOOR, door).is_success(), "failed staff-door request retries after exact rollback")
before = _all_snapshots(context)
path.fail_next_remove_after_mutation = true
var remove_failure = context.projector.unproject_facility(&"retry_remove_failure", &"facility_00000009")
_assert_exact_rollback(context, before, remove_failure, "staff connector removal failure")
_expect(context.source.has_entity(&"infra.route:facility_00000009") and context.path.has_connector(&"infra.connector:facility_00000009"), "failed unproject restores route and connector")
_expect(context.projector.unproject_facility(&"retry_remove_failure", &"facility_00000009").is_success(), "failed unproject request retries after exact rollback")
path.projector = null
func _test_callback_reentry_tamper_and_quarantine_recovery() -> void:
var path := ControlledPathService.new()
var front := Vector3i(5, 10, PathType.FRONT)
var context := _context([front], path)
path.projector = context.projector
var first_door := _instance(STAFF_DOOR, &"facility_00000010", Vector3i(5, 10, PathType.BACK), 70, &"construction_00000010")
path.reenter_next_connector = true
var outer = context.projector.project_facility(&"outer_reentry", STAFF_DOOR, first_door)
_expect(outer.is_success(), "outer staff-door transaction commits when callback only attempts reentry")
_expect(path.reentry_result != null and path.reentry_result.code == CommandResultType.Code.TRANSACTION_CLOSED, "Path callback reentry is rejected")
_expect(context.projector.projection_revision == 1, "reentry cannot advance a nested projector revision")
var second_door := _instance(STAFF_DOOR, &"facility_00000011", Vector3i(7, 10, PathType.BACK), 71, &"construction_00000011")
var before := _all_snapshots(context)
path.tamper_projector_next_connector = true
var tampered = context.projector.project_facility(&"callback_tamper", STAFF_DOOR, second_door)
_assert_exact_rollback(context, before, tampered, "Path callback projector tamper")
_expect(context.projector.projection_revision == 1 and not context.projector.has_projection(&"facility_00000011"), "callback tamper cannot leak owned metadata")
var recovery := _all_snapshots(context)
path.fail_next_connector_after_mutation = true
path.reject_restore = true
var uncertain_door := _instance(STAFF_DOOR, &"facility_00000012", Vector3i(9, 10, PathType.BACK), 72, &"construction_00000012")
var uncertain = context.projector.project_facility(&"uncertain_project", STAFF_DOOR, uncertain_door)
_expect(not uncertain.is_success() and not uncertain.details[&"rollback_ok"], "uncertain dependency rollback reports its uncertainty")
_expect(context.projector.is_quarantined() and context.projector.get_quarantine_reason() == context.projector.QUARANTINE_TRANSACTION_ROLLBACK, "uncertain rollback quarantines the projector")
_expect(context.projector.project_facility(&"blocked_quarantine", GUEST_CORRIDOR, _instance(GUEST_CORRIDOR, &"facility_00000013", Vector3i(11, 10, 0), 73, &"construction_00000013")).code == CommandResultType.Code.TRANSACTION_CLOSED, "quarantine blocks further projection")
path.reject_restore = false
_expect(context.source.restore_snapshot(recovery.source), "quarantine recovery restores the Spatial snapshot")
_restore_path_for_test(context.path, recovery.path_cells, recovery.path)
_expect(context.adapter.restore_snapshot(recovery.adapter), "quarantine recovery restores the Route adapter snapshot")
_expect(context.projector.restore_snapshot(recovery.projector), "matching projector snapshot clears quarantine")
_expect(not context.projector.is_quarantined() and context.projector.validate_invariants()[&"ok"], "recovered projector returns to a valid available state")
path.projector = null
func _test_strict_snapshot_round_trip_and_atomic_rejection() -> void:
var base := [
Vector3i(0, 12, PathType.FRONT),
Vector3i(4, 12, PathType.FRONT),
]
var context := _context(base)
var guest := _instance(GUEST_CORRIDOR, &"facility_00000020", Vector3i(1, 12, 0), 80, &"construction_00000020")
var door := _instance(STAFF_DOOR, &"facility_00000021", Vector3i(3, 12, 1), 81, &"construction_00000021")
context.projector.project_facility(&"z_project", GUEST_CORRIDOR, guest)
context.projector.project_facility(&"a_project", STAFF_DOOR, door)
context.projector.unproject_facility(&"m_unproject", &"facility_00000020")
var snapshot: Dictionary = context.projector.create_snapshot()
_expect(snapshot[&"projections"].map(func(row: Dictionary) -> String: return row[&"instance_id"]) == ["facility_00000021"], "snapshot stores only current projections in lexical order")
_expect(snapshot[&"processed_requests"].map(func(row: Dictionary) -> String: return row[&"request_id"]) == ["a_project", "m_unproject", "z_project"], "snapshot history is lexical independently of revision order")
_expect(snapshot[&"projection_revision"] == 3, "snapshot preserves contiguous successful revision history")
var restored_source := SpatialType.new()
var restored_path := PathType.new()
var restored_adapter := AdapterType.new()
_expect(restored_adapter.configure(restored_source, restored_path, base), "round-trip target adapter configures empty dependencies")
var restored_projector := ProjectorType.new()
_expect(restored_projector.configure(restored_adapter, restored_source, restored_path), "round-trip target projector configures before dependency restore")
_expect(restored_source.restore_snapshot(context.source.create_snapshot()), "round-trip target restores Spatial state")
_restore_path_for_test(restored_path, _path_cells(context.path), context.path.create_snapshot())
_expect(restored_adapter.restore_snapshot(context.adapter.create_snapshot()), "round-trip target restores Route adapter state")
_expect(restored_projector.restore_snapshot(snapshot), "strict current projector snapshot restores against matching dependencies")
_expect(restored_projector.create_snapshot() == snapshot, "projector snapshot round trip is exact")
_expect(restored_projector.validate_invariants()[&"ok"], "restored history/projection/dependency crosslinks validate")
var replay_source: Dictionary = restored_source.create_snapshot()
var replay_adapter: Dictionary = restored_adapter.create_snapshot()
var replay_path: Dictionary = restored_path.create_snapshot()
var restored_replay = restored_projector.project_facility(&"a_project", STAFF_DOOR, door)
_expect(restored_replay.is_success() and restored_replay.is_replay, "restored project history replays with the same canonical input")
_expect(restored_projector.create_snapshot() == snapshot and restored_source.create_snapshot() == replay_source and restored_adapter.create_snapshot() == replay_adapter and restored_path.create_snapshot() == replay_path, "restored replay leaves metadata and dependencies unchanged")
var restored_context := {
&"source": restored_source,
&"path": restored_path,
&"adapter": restored_adapter,
&"projector": restored_projector,
}
var before := _all_snapshots(restored_context)
var wrong_version: Dictionary = snapshot.duplicate(true)
wrong_version[&"schema_version"] = 2
_atomic_snapshot_reject(restored_context, wrong_version, before, "unknown snapshot version")
var version_type: Dictionary = snapshot.duplicate(true)
version_type[&"schema_version"] = "1"
_atomic_snapshot_reject(restored_context, version_type, before, "string snapshot version")
var unknown_field: Dictionary = snapshot.duplicate(true)
unknown_field[&"extra"] = true
_atomic_snapshot_reject(restored_context, unknown_field, before, "unknown root field")
var revision_type: Dictionary = snapshot.duplicate(true)
revision_type[&"projection_revision"] = 3.0
_atomic_snapshot_reject(restored_context, revision_type, before, "floating projection revision")
var projection_order: Dictionary = snapshot.duplicate(true)
projection_order[&"projections"].append(projection_order[&"projections"][0].duplicate(true))
_atomic_snapshot_reject(restored_context, projection_order, before, "duplicate projection order")
var request_order: Dictionary = snapshot.duplicate(true)
request_order[&"processed_requests"].reverse()
_atomic_snapshot_reject(restored_context, request_order, before, "unordered request history")
var fingerprint: Dictionary = snapshot.duplicate(true)
fingerprint[&"processed_requests"][0][&"fingerprint"] = "0".repeat(64)
_atomic_snapshot_reject(restored_context, fingerprint, before, "tampered request fingerprint")
var result_projection: Dictionary = snapshot.duplicate(true)
result_projection[&"processed_requests"][0][&"details"][&"projection"][&"staff_only"] = false
_atomic_snapshot_reject(restored_context, result_projection, before, "tampered result projection semantics")
var history_revision: Dictionary = snapshot.duplicate(true)
history_revision[&"processed_requests"][0][&"details"][&"projection_revision"] = 3
_atomic_snapshot_reject(restored_context, history_revision, before, "duplicate history revision")
var final_crosslink: Dictionary = snapshot.duplicate(true)
final_crosslink[&"projections"].clear()
_atomic_snapshot_reject(restored_context, final_crosslink, before, "history-to-current projection mismatch")
var record_fingerprint: Dictionary = snapshot.duplicate(true)
record_fingerprint[&"projections"][0][&"record_fingerprint"] = "bad"
_atomic_snapshot_reject(restored_context, record_fingerprint, before, "tampered projection record fingerprint")
var extra_projection_field: Dictionary = snapshot.duplicate(true)
extra_projection_field[&"projections"][0][&"extra"] = true
_atomic_snapshot_reject(restored_context, extra_projection_field, before, "unknown projection field")
var deep: Dictionary = snapshot.duplicate(true)
var deep_value: Variant = []
for _index: int in range(restored_projector.MAX_SNAPSHOT_DEPTH + 2):
deep_value = [deep_value]
deep[&"processed_requests"] = deep_value
_atomic_snapshot_reject(restored_context, deep, before, "excessive snapshot depth")
var connector_id := &"infra.connector:facility_00000021"
var connector_before := restored_path.get_connector(connector_id)
_expect(restored_path.configure_connector(connector_id, connector_before[&"from"], connector_before[&"to"], 2, true, &"staff"), "test can introduce a live connector crosslink mismatch")
var stale_owned_before := restored_projector.create_snapshot()
var dependencies_before_restore := {
&"source": restored_source.create_snapshot(),
&"adapter": restored_adapter.create_snapshot(),
&"path": restored_path.create_snapshot(),
}
_expect(not restored_projector.restore_snapshot(snapshot), "valid metadata snapshot rejects a mismatched live staff connector")
_expect(restored_projector.create_snapshot() == stale_owned_before, "live-crosslink rejection is projector-atomic")
_expect(restored_source.create_snapshot() == dependencies_before_restore.source and restored_adapter.create_snapshot() == dependencies_before_restore.adapter and restored_path.create_snapshot() == dependencies_before_restore.path, "live-crosslink rejection preserves all dependency snapshots")
func _test_configuration_and_stale_dependency_boundaries() -> void:
var source_a := SpatialType.new()
var source_b := SpatialType.new()
var path_a := PathType.new()
var path_b := PathType.new()
var adapter := AdapterType.new()
adapter.configure(source_a, path_a, [])
var projector := ProjectorType.new()
_expect(not projector.configure(adapter, source_b, path_a), "configuration rejects a different Spatial instance with equal state")
_expect(not projector.configure(adapter, source_a, path_b), "configuration rejects a different Path instance with equal state")
_expect(projector.configure(adapter, source_a, path_a), "configuration accepts the exact adapter binding")
_expect(not projector.configure(adapter, source_a, path_a), "configured projector rejects reconfiguration")
var context := _context([])
var instance := _instance(GUEST_CORRIDOR, &"facility_00000030", Vector3i(1, 14, 0), 90, &"construction_00000030")
context.projector.project_facility(&"stale_project", GUEST_CORRIDOR, instance)
var before := _all_snapshots(context)
_expect(context.source.place(&"external_route", context.source.CATEGORY_ROUTE, [Vector3i(2, 14, 0)]).is_success(), "external ROUTE mutation can stale the lower adapter")
var source_after_external: Dictionary = context.source.create_snapshot()
var blocked = context.projector.unproject_facility(&"blocked_stale", &"facility_00000030")
_expect(blocked.code == CommandResultType.Code.TRANSACTION_CLOSED, "stale dependencies block projector mutation")
_expect(context.projector.create_snapshot() == before.projector and context.adapter.create_snapshot() == before.adapter and context.path.create_snapshot() == before.path, "stale rejection preserves projector, adapter, and Path state")
_expect(context.source.create_snapshot() == source_after_external, "stale rejection does not undo an external source mutation")
_expect(context.projector.is_projection_stale(), "projector exposes stale dependency status")
var collision_source := SpatialType.new()
var collision_path := PathType.new()
var collision_adapter := AdapterType.new()
collision_adapter.configure(collision_source, collision_path, [])
collision_adapter.place_route(&"preexisting_managed", &"infra.route:foreign", [Vector3i(4, 14, 0)])
var collision_projector := ProjectorType.new()
_expect(not collision_projector.configure(collision_adapter, collision_source, collision_path), "configuration rejects an untracked managed-ID namespace collision")
func _context(base_cells: Array, custom_path: Variant = null) -> Dictionary:
var source := SpatialType.new()
var path: PathService = custom_path if custom_path != null else PathType.new()
var adapter := AdapterType.new()
_expect(adapter.configure(source, path, base_cells), "test context configures Route adapter")
var projector := ProjectorType.new()
_expect(projector.configure(adapter, source, path), "test context configures Facility projector")
return {
&"source": source,
&"path": path,
&"adapter": adapter,
&"projector": projector,
}
func _instance(
definition: FacilityDefinition,
instance_id: StringName,
origin: Vector3i,
completed_at: int,
source_job_id: StringName,
rotation: int = 0
) -> Dictionary:
var dimensions := definition.footprint
if rotation in [90, 270]:
dimensions = Vector2i(definition.footprint.y, definition.footprint.x)
var cells: Array[Vector3i] = []
for y_offset: int in range(dimensions.y):
for x_offset: int in range(dimensions.x):
cells.append(origin + Vector3i(x_offset, y_offset, 0))
return {
&"instance_id": instance_id,
&"definition_id": definition.id,
&"origin": origin,
&"rotation": rotation,
&"layer": definition.layer,
&"occupied_cells": cells,
&"construction_cost": definition.construction_cost.duplicate(true),
&"source_job_id": source_job_id,
&"state": &"ACTIVE",
&"completed_at": completed_at,
}
func _all_snapshots(context: Dictionary) -> Dictionary:
return {
&"projector": context.projector.create_snapshot(),
&"source": context.source.create_snapshot(),
&"adapter": context.adapter.create_snapshot(),
&"path": context.path.create_snapshot(),
&"path_cells": _path_cells(context.path),
}
func _path_cells(path: PathService) -> Array[Vector3i]:
var result: Array[Vector3i] = []
for row: Variant in path.create_snapshot().get(&"cell_traffic", []):
if row is Dictionary and row.get(&"cell", null) is Vector3i:
result.append(row[&"cell"])
result.sort_custom(func(left: Vector3i, right: Vector3i) -> bool:
if left.y != right.y:
return left.y < right.y
if left.x != right.x:
return left.x < right.x
return left.z < right.z
)
return result
func _restore_path_for_test(path: PathService, cells: Array, snapshot: Dictionary) -> void:
path.rebuild_graph(cells)
path.restore_snapshot(snapshot)
path.rebuild_graph(cells)
func _assert_exact_rollback(
context: Dictionary,
before: Dictionary,
result: CommandResult,
label: String
) -> void:
_expect(not result.is_success() and bool(result.details.get(&"rollback_ok", false)), "%s reports exact rollback" % label)
_expect(context.projector.create_snapshot() == before.projector, "%s restores exact projector snapshot" % label)
_expect(context.source.create_snapshot() == before.source, "%s restores exact Spatial snapshot" % label)
_expect(context.adapter.create_snapshot() == before.adapter, "%s restores exact Route adapter snapshot" % label)
_expect(context.path.create_snapshot() == before.path, "%s restores exact Path snapshot" % label)
func _atomic_snapshot_reject(
context: Dictionary,
candidate: Dictionary,
before: Dictionary,
label: String
) -> void:
_expect(not context.projector.restore_snapshot(candidate), "%s is rejected" % label)
_expect(context.projector.create_snapshot() == before.projector, "%s rejection is projector-atomic" % label)
_expect(context.source.create_snapshot() == before.source and context.adapter.create_snapshot() == before.adapter and context.path.create_snapshot() == before.path, "%s rejection leaves every dependency untouched" % label)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://dabxeosrueu1a
+517
View File
@@ -0,0 +1,517 @@
extends SceneTree
const PathServiceScript = preload("res://scripts/world/path_service.gd")
var _checks := 0
var _failures := 0
var _timing_samples: Array[int] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: congestion, elevator, and stuck recovery")
_test_cell_traffic_and_dynamic_route()
_test_cache_decrease_and_unblock_invalidation()
_test_query_timing_boundary()
_test_two_hundred_actor_chokepoint()
_test_stopped_elevator_fallback_and_no_route_reason()
_test_normal_connector_outage_snapshot_stability()
_test_bounded_elevator_queue_and_order()
_test_elevator_request_global_uniqueness()
_test_parallel_connector_access_cost()
_test_stuck_detection_and_explicit_recovery()
_test_actor_progress_cleanup_contract()
_test_relevant_no_route_diagnosis()
_test_snapshot_roundtrip_and_atomic_rejection()
_test_input_order_determinism()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_cell_traffic_and_dynamic_route() -> void:
var service = PathServiceScript.new()
var cells := _diamond_cells()
_expect(service.rebuild_graph(cells), "traffic test graph rebuilds")
var start := Vector3i(0, 1, 0)
var goal := Vector3i(2, 1, 0)
var top_middle := Vector3i(1, 0, 0)
var bottom_middle := Vector3i(1, 2, 0)
var baseline: Array[Vector3i] = service.find_path(start, goal, service.FRONT)
_expect(baseline.has(top_middle), "equal-cost baseline uses the deterministic upper route")
_expect(
service.configure_cell_traffic(top_middle, 1, 25),
"per-cell capacity and dynamic cost are configurable"
)
for index: int in range(4):
_expect(
service.actor_enter_cell(StringName("crowd_%03d" % index), top_middle),
"soft collision admits crowd member %d" % index
)
var traffic := service.get_cell_traffic(top_middle)
_expect(int(traffic[&"base_capacity"]) == 1, "cell reports its base capacity")
_expect(int(traffic[&"current_density"]) == 4, "cell reports projected current density")
_expect(int(traffic[&"dynamic_cost"]) == 25, "cell reports configured dynamic cost")
_expect(service.get_congestion_cost(top_middle) == 225, "density excess costs 75 units per actor")
_expect(is_equal_approx(service.get_speed_multiplier(top_middle), 0.25), "soft collision speed clamps at 0.25")
var diverted: Array[Vector3i] = service.find_path(start, goal, service.FRONT)
_expect(diverted.has(bottom_middle), "A* avoids the dynamically congested route")
_expect(not diverted.has(top_middle), "locally invalidated cache never returns the stale congested route")
_expect(service.last_path_cost == 400, "reported route cost includes four orthogonal steps")
_expect(service.traffic_revision == 5, "one configuration and four entries advance traffic revision deterministically")
func _test_query_timing_boundary() -> void:
var service = PathServiceScript.new()
service.rebuild_graph([Vector3i(0, 0, 0), Vector3i(1, 0, 0)])
_timing_samples.clear()
service.path_query_measured.connect(Callable(self, "_capture_path_sample"))
service.find_path(Vector3i(0, 0, 0), Vector3i(1, 0, 0))
_expect(_timing_samples.size() == 1, "one public path query emits exactly one timing sample")
_expect(_timing_samples[0] >= 0, "path timing sample is non-negative")
service.find_path_report(Vector3i(0, 0, 0), Vector3i(1, 0, 0))
_expect(_timing_samples.size() == 2, "route report also emits exactly one sample through its single search")
func _test_cache_decrease_and_unblock_invalidation() -> void:
var service = PathServiceScript.new()
service.rebuild_graph(_diamond_cells())
var start := Vector3i(0, 1, 0)
var goal := Vector3i(2, 1, 0)
var top_middle := Vector3i(1, 0, 0)
var bottom_middle := Vector3i(1, 2, 0)
service.configure_cell_traffic(top_middle, 4, 500)
var expensive_route: Array[Vector3i] = service.find_path(start, goal)
_expect(expensive_route.has(bottom_middle), "high-cost upper alternative leaves lower route cached")
_expect(service.cached_path_count() == 1, "selected lower route is present in cache")
service.set_cell_dynamic_cost(top_middle, 0)
_expect(service.cached_path_count() == 0, "cost decrease broadly invalidates paths that did not touch the cheaper cell")
_expect(service.find_path(start, goal).has(top_middle), "query selects newly-cheaper non-cached alternative")
service.set_cell_temporary_block(top_middle, true)
_expect(service.find_path(start, goal).has(bottom_middle), "temporary block diverts route to lower path")
service.set_cell_temporary_block(top_middle, false)
_expect(service.cached_path_count() == 0, "block removal broadly invalidates the previously selected alternative")
_expect(service.find_path(start, goal).has(top_middle), "unblocked deterministic upper route becomes selectable immediately")
func _test_two_hundred_actor_chokepoint() -> void:
var service = PathServiceScript.new()
var entry := Vector3i(0, 0, 0)
var choke := Vector3i(1, 0, 0)
var exit_cell := Vector3i(2, 0, 0)
service.rebuild_graph([entry, choke, exit_cell])
service.configure_cell_traffic(choke, 4)
var started_usec := Time.get_ticks_usec()
var all_entered := true
for index: int in range(200):
all_entered = all_entered and service.actor_enter_cell(
StringName("guest_%03d" % index),
choke
)
var entry_elapsed_usec := Time.get_ticks_usec() - started_usec
_expect(all_entered, "all 200 actors can share the chokepoint without a hard-cap deadlock")
_expect(service.get_current_density(choke) == 200, "chokepoint density reaches 200 exactly")
_expect(service.get_congestion_cost(choke) == 14_700, "200-actor chokepoint congestion cost is exact")
_expect(is_equal_approx(service.get_speed_multiplier(choke), 0.25), "extreme density remains bounded by minimum speed")
var all_exited := true
for index: int in range(200):
var actor_id := StringName("guest_%03d" % index)
all_exited = all_exited and service.project_actor_position(actor_id, exit_cell)
all_exited = all_exited and service.actor_leave_cell(actor_id)
_expect(all_exited, "all 200 actors deterministically traverse and leave the chokepoint")
_expect(service.get_current_density(choke) == 0, "chokepoint density returns to zero")
_expect(service.get_current_density(exit_cell) == 0, "exit density returns to zero")
_expect(not service.actor_leave_cell(&"guest_000"), "duplicate leave is rejected")
_expect(service.get_current_density(choke) == 0, "rejected duplicate leave never creates negative density")
_expect(entry_elapsed_usec < 2_000_000, "200-actor admission completes within a two-second safety budget")
print(" i 200-actor admission: %d usec" % entry_elapsed_usec)
func _test_stopped_elevator_fallback_and_no_route_reason() -> void:
var service = PathServiceScript.new()
var front_start := Vector3i(0, 0, 0)
var front_fallback := Vector3i(1, 0, 0)
var back_goal := Vector3i(0, 0, 1)
var back_fallback := Vector3i(1, 0, 1)
service.rebuild_graph([front_start, front_fallback, back_goal, back_fallback])
_expect(
service.configure_elevator(&"main_lift", front_start, back_goal, {
&"cost": 100,
&"cabin_capacity": 4,
&"queue_capacity": 8,
}),
"main elevator configuration is accepted"
)
_expect(
service.configure_connector(
&"staff_stairs",
front_fallback,
back_fallback,
300,
true
),
"fallback stair connector is accepted"
)
var main_route: Array[Vector3i] = service.find_path(front_start, back_goal)
_expect(main_route == [front_start, back_goal], "operational elevator is selected while cheapest")
var revision_before_stop: int = service.graph_revision
_expect(
service.set_connector_operational_state(&"main_lift", service.CONNECTOR_STOPPED),
"elevator can enter stopped state"
)
_expect(service.graph_revision == revision_before_stop + 1, "connector state change advances graph revision")
var fallback_route: Array[Vector3i] = service.find_path(front_start, back_goal)
_expect(fallback_route.has(front_fallback), "stopped elevator triggers deterministic fallback routing")
_expect(fallback_route.has(back_fallback), "fallback route crosses the operational stairs")
_expect(not fallback_route.is_empty(), "stopped elevator does not make a reachable goal fail")
service.set_connector_operational_state(&"staff_stairs", service.CONNECTOR_OUTAGE)
var report := service.find_path_report(front_start, back_goal)
_expect(not bool(report[&"reachable"]), "route report explicitly marks unreachable goal")
_expect(report[&"reason"] == service.ROUTE_CONNECTOR_UNAVAILABLE, "no-route report names connector unavailability")
_expect(not service.last_error.is_empty(), "legacy last_error also carries an explicit no-route reason")
func _test_bounded_elevator_queue_and_order() -> void:
var service = PathServiceScript.new()
var from_cell := Vector3i(0, 0, 0)
var to_cell := Vector3i(0, 0, 1)
service.rebuild_graph([from_cell, to_cell])
service.configure_elevator(&"lift", from_cell, to_cell, {
&"cabin_capacity": 2,
&"queue_capacity": 3,
})
var normal_b := _request(&"r_normal_b", &"unit_b", 0, 1, &"NORMAL", 10)
var vip := _request(&"r_vip", &"unit_z", 0, 1, &"VIP", 10)
var normal_a := _request(&"r_normal_a", &"unit_a", 0, 1, &"NORMAL", 10)
_expect(bool(service.enqueue_elevator_request(&"lift", normal_b)[&"ok"]), "first request enters bounded queue")
_expect(bool(service.enqueue_elevator_request(&"lift", vip)[&"ok"]), "second request enters bounded queue")
_expect(bool(service.enqueue_elevator_request(&"lift", normal_a)[&"ok"]), "third request enters bounded queue")
var overflow := service.enqueue_elevator_request(
&"lift",
_request(&"r_overflow", &"unit_x", 0, 1, &"RESCUE", 10)
)
_expect(not bool(overflow[&"ok"]) and overflow[&"reason"] == &"QUEUE_FULL", "fourth request is rejected at queue capacity")
var ordered := service.get_elevator_queue(&"lift", 10)
_expect(ordered.size() == 3, "queue remains bounded after overflow rejection")
_expect(ordered[0][&"request_id"] == &"r_vip", "VIP precedes fresh normal requests")
_expect(ordered[1][&"request_id"] == &"r_normal_a", "same-priority tie breaks by unit id")
_expect(ordered[2][&"request_id"] == &"r_normal_b", "same-minute ordering is deterministic")
var starved_order := service.get_elevator_queue(&"lift", 70)
_expect(starved_order[0][&"request_id"] == &"r_normal_a", "60-minute normal request is promoted ahead of VIP")
var boarded := service.dequeue_elevator_boarding(&"lift", 0, 70)
_expect(boarded.size() == 2, "boarding never exceeds cabin capacity")
_expect(boarded[0][&"request_id"] == &"r_normal_a", "starvation promotion applies to boarding order")
_expect(boarded[1][&"request_id"] == &"r_normal_b", "second starved normal boards deterministically")
_expect(service.elevator_queue_size(&"lift") == 1, "unboarded VIP remains queued without loss")
service.set_elevator_operational(&"lift", false)
_expect(service.dequeue_elevator_boarding(&"lift", 0, 71).is_empty(), "outage elevator boards nobody")
_expect(service.elevator_queue_size(&"lift") == 1, "outage preserves queued requests")
func _test_normal_connector_outage_snapshot_stability() -> void:
var from_cell := Vector3i(0, 0, 0)
var to_cell := Vector3i(0, 0, 1)
var original = PathServiceScript.new()
original.rebuild_graph([from_cell, to_cell])
original.configure_connector(&"ordinary_door", from_cell, to_cell)
original.set_connector_operational_state(&"ordinary_door", original.CONNECTOR_OUTAGE)
var before := original.create_snapshot()
_expect(original.get_connector(&"ordinary_door")[&"cabin_state"] == original.CABIN_IDLE, "ordinary connector keeps canonical IDLE cabin placeholder during outage")
var restored = PathServiceScript.new()
_expect(restored.restore_snapshot(before), "outage connector snapshot restores")
_expect(restored.rebuild_graph([from_cell, to_cell]), "outage connector rebuilds against authoritative cells")
_expect(restored.get_connector(&"ordinary_door")[&"cabin_state"] == restored.CABIN_IDLE, "restore does not mutate ordinary connector cabin placeholder")
_expect(restored.create_snapshot() == before, "ordinary connector outage snapshot roundtrip preserves exact digest state")
func _test_elevator_request_global_uniqueness() -> void:
var service = PathServiceScript.new()
var a_from := Vector3i(0, 0, 0)
var a_to := Vector3i(0, 0, 1)
var b_from := Vector3i(2, 0, 0)
var b_to := Vector3i(2, 0, 1)
service.rebuild_graph([a_from, a_to, b_from, b_to])
service.configure_elevator(&"lift_a", a_from, a_to, {&"queue_capacity": 4})
service.configure_elevator(&"lift_b", b_from, b_to, {&"queue_capacity": 4})
var original_request := _request(&"global_request", &"shared_unit", 0, 1, &"NORMAL", 5)
_expect(bool(service.enqueue_elevator_request(&"lift_a", original_request)[&"ok"]), "first global request is accepted")
var cross_connector := service.enqueue_elevator_request(
&"lift_b",
_request(&"global_request", &"other_unit", 0, 1, &"NORMAL", 5)
)
_expect(not bool(cross_connector[&"ok"]) and cross_connector[&"reason"] == &"DUPLICATE_REQUEST", "request id cannot be reused across connectors")
var same_unit := service.enqueue_elevator_request(
&"lift_b",
_request(&"other_request", &"shared_unit", 0, 1, &"NORMAL", 5)
)
_expect(not bool(same_unit[&"ok"]) and same_unit[&"reason"] == &"UNIT_ALREADY_ACTIVE", "one unit cannot own multiple active elevator requests")
var boarded := service.dequeue_elevator_boarding(&"lift_a", 0, 5)
_expect(boarded.size() == 1, "original request boards exactly once")
_expect(service.get_active_elevator_request(&"shared_unit")[&"state"] == service.ELEVATOR_REQUEST_BOARDED, "boarding persists an active BOARDED unit index")
var boarded_reentry := service.enqueue_elevator_request(
&"lift_b",
_request(&"while_boarded", &"shared_unit", 0, 1, &"NORMAL", 6)
)
_expect(not bool(boarded_reentry[&"ok"]) and boarded_reentry[&"reason"] == &"UNIT_ALREADY_ACTIVE", "boarded unit cannot enqueue a second ride")
var mid_trip_snapshot := service.create_snapshot()
var restored = PathServiceScript.new()
_expect(restored.restore_snapshot(mid_trip_snapshot), "boarded request indexes restore")
_expect(restored.rebuild_graph([a_from, a_to, b_from, b_to]), "boarded request snapshot rebuilds")
_expect(restored.create_snapshot() == mid_trip_snapshot, "known and active request indexes roundtrip exactly")
_expect(restored.complete_elevator_trip(&"lift_a", &"global_request"), "explicit trip completion releases the unit index")
_expect(bool(restored.enqueue_elevator_request(&"lift_b", _request(&"new_trip", &"shared_unit", 0, 1, &"NORMAL", 7))[&"ok"]), "completed unit may enqueue a new request id")
var reused_retired := restored.enqueue_elevator_request(
&"lift_b",
_request(&"global_request", &"third_unit", 0, 1, &"NORMAL", 7)
)
_expect(not bool(reused_retired[&"ok"]), "boarded and completed request id remains permanently tombstoned")
func _test_parallel_connector_access_cost() -> void:
var service = PathServiceScript.new()
var from_cell := Vector3i(0, 0, 0)
var to_cell := Vector3i(0, 0, 1)
service.rebuild_graph([from_cell, to_cell])
service.configure_connector(&"a_staff_shortcut", from_cell, to_cell, 100, true, &"staff")
service.configure_connector(&"z_public_passage", from_cell, to_cell, 400)
var public_report := service.find_path_report(from_cell, to_cell)
_expect(bool(public_report[&"reachable"]), "public actor can use unrestricted parallel connector")
_expect(int(public_report[&"cost"]) == 400, "public route report excludes cheaper staff-only parallel edge")
var staff_report := service.find_path_report(from_cell, to_cell, null, [&"staff"])
_expect(int(staff_report[&"cost"]) == 100, "staff route report uses accessible restricted shortcut")
_expect(service.calculate_path_cost(public_report[&"path"]) == 400, "public path cost API honors default connector access")
_expect(service.calculate_path_cost(staff_report[&"path"], [&"staff"]) == 100, "profiled path cost API selects the exact accessible edge")
func _test_stuck_detection_and_explicit_recovery() -> void:
var service = PathServiceScript.new()
var left := Vector3i(0, 0, 0)
var occupied := Vector3i(2, 0, 0)
service.rebuild_graph([left, occupied])
service.actor_enter_cell(&"guest_stuck", occupied)
_expect(
bool(service.record_actor_progress(&"guest_stuck", Vector3i(1, 0, 0), 100)[&"ok"]),
"progress tracker accepts an actor observation"
)
var early := service.get_actor_stuck_report(&"guest_stuck", 129)
_expect(not bool(early[&"should_recover"]), "29 minutes without progress is not yet stuck")
var stuck := service.get_actor_stuck_report(&"guest_stuck", 130)
_expect(bool(stuck[&"should_recover"]), "30 minutes without progress triggers stuck recovery")
_expect(stuck[&"reason"] == &"STUCK", "stuck report carries an explicit reason")
_expect(bool(stuck[&"requires_explicit_move"]), "recovery requires an explicit caller action")
_expect(bool(stuck[&"recovery"][&"found"]), "stuck report provides a safe recovery candidate")
_expect(stuck[&"recovery"][&"cell"] == left, "nearest-cell tie uses deterministic coordinate order")
_expect(not bool(stuck[&"recovery"][&"implicit_move_applied"]), "PathService never teleports during stuck evaluation")
_expect(service.get_actor_projected_cell(&"guest_stuck") == occupied, "actor occupancy is unchanged by recovery proposal")
service.record_actor_progress(&"guest_stuck", occupied, 131)
var reset := service.get_actor_stuck_report(&"guest_stuck", 160)
_expect(not bool(reset[&"should_recover"]), "actual cell progress resets the 30-minute timer")
func _test_actor_progress_cleanup_contract() -> void:
var service = PathServiceScript.new()
var left := Vector3i(0, 0, 0)
var right := Vector3i(1, 0, 0)
service.rebuild_graph([left, right])
service.actor_enter_cell(&"despawned", left, 10)
_expect(service.actor_leave_cell(&"despawned"), "default leave removes occupancy")
_expect(not bool(service.get_actor_stuck_report(&"despawned", 40)[&"ok"]), "default leave also removes actor progress")
service.actor_enter_cell(&"preserved", left, 10)
_expect(service.actor_leave_cell(&"preserved", false), "leave can explicitly retain progress for a transfer")
_expect(bool(service.get_actor_stuck_report(&"preserved", 40)[&"ok"]), "opt-out cleanup retains transfer progress")
service.project_occupancy([
{&"actor_id": &"kept", &"cell": left},
{&"actor_id": &"removed", &"cell": right},
])
service.record_actor_progress(&"kept", left, 50)
service.record_actor_progress(&"removed", right, 50)
service.project_occupancy([{&"actor_id": &"kept", &"cell": left}])
_expect(bool(service.get_actor_stuck_report(&"kept", 50)[&"ok"]), "authoritative occupancy replacement preserves retained actor progress")
_expect(not bool(service.get_actor_stuck_report(&"removed", 50)[&"ok"]), "authoritative occupancy replacement cleans unprojected progress")
service.record_actor_progress(&"progress_only", right, 60)
_expect(service.despawn_actor(&"progress_only"), "despawn API cleans a progress-only actor")
_expect(not bool(service.get_actor_stuck_report(&"progress_only", 60)[&"ok"]), "despawn leaves no stale progress record")
func _test_relevant_no_route_diagnosis() -> void:
var service = PathServiceScript.new()
var start := Vector3i(0, 0, 0)
var goal := Vector3i(10, 0, 0)
var unrelated_a := Vector3i(20, 0, 0)
var unrelated_b := Vector3i(20, 0, 1)
service.rebuild_graph([start, goal, unrelated_a, unrelated_b])
service.set_cell_temporary_block(unrelated_a, true)
service.configure_connector(&"unrelated_lift", unrelated_a, unrelated_b)
service.set_connector_operational_state(&"unrelated_lift", service.CONNECTOR_OUTAGE)
var unrelated_report := service.find_path_report(start, goal)
_expect(unrelated_report[&"reason"] == service.ROUTE_GOAL_UNREACHABLE, "unrelated block and outage do not misclassify a disconnected route")
var blocked = PathServiceScript.new()
blocked.rebuild_graph([Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0)])
blocked.set_cell_temporary_block(Vector3i(1, 0, 0), true)
_expect(blocked.find_path_report(Vector3i(0, 0, 0), Vector3i(2, 0, 0))[&"reason"] == blocked.ROUTE_TEMPORARILY_BLOCKED, "relevant temporary block is diagnosed by auxiliary reachability")
var outage = PathServiceScript.new()
var outage_from := Vector3i(0, 0, 0)
var outage_to := Vector3i(0, 0, 1)
outage.rebuild_graph([outage_from, outage_to])
outage.configure_connector(&"relevant_door", outage_from, outage_to)
outage.set_connector_operational_state(&"relevant_door", outage.CONNECTOR_OUTAGE)
_expect(outage.find_path_report(outage_from, outage_to)[&"reason"] == outage.ROUTE_CONNECTOR_UNAVAILABLE, "relevant stopped connector is diagnosed precisely")
func _test_snapshot_roundtrip_and_atomic_rejection() -> void:
var original = PathServiceScript.new()
var from_cell := Vector3i(0, 0, 0)
var to_cell := Vector3i(0, 0, 1)
original.rebuild_graph([from_cell, to_cell])
original.configure_cell_traffic(from_cell, 2, 7)
original.actor_enter_cell(&"guest_a", from_cell, 40)
original.configure_elevator(&"lift_save", from_cell, to_cell, {
&"cabin_capacity": 3,
&"queue_capacity": 4,
&"travel_minutes_per_floor": 2,
})
original.enqueue_elevator_request(
&"lift_save",
_request(&"saved_request", &"guest_a", 0, 1, &"NORMAL", 40)
)
original.record_actor_progress(&"off_graph_actor", Vector3i(4, 4, 0), 12)
var snapshot := original.create_snapshot()
_expect(int(snapshot[&"schema_version"]) == 3, "request-safe traffic snapshot uses schema version 3")
_expect(snapshot.has(&"cell_traffic") and snapshot.has(&"actor_cells"), "snapshot persists traffic and occupancy projection")
_expect(not snapshot.has(&"graph") and not snapshot.has(&"cache"), "snapshot still excludes derived graph and cache")
var restored = PathServiceScript.new()
_expect(restored.restore_snapshot(snapshot), "v3 snapshot restores before authoritative world graph")
_expect(restored.rebuild_graph([from_cell, to_cell]), "restored traffic validates against authoritative cells")
_expect(restored.create_snapshot() == snapshot, "v3 snapshot roundtrip is exact")
_expect(restored.get_current_density(from_cell) == 1, "restored density is derived consistently from actors")
_expect(restored.elevator_queue_size(&"lift_save") == 1, "restored elevator retains its request queue")
var before_corrupt := restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"cell_traffic"][0][&"current_density"] = 99
_expect(not restored.restore_snapshot(corrupt), "corrupt density snapshot is rejected")
_expect(restored.create_snapshot() == before_corrupt, "failed restore is atomic and preserves live state")
var corrupt_index := snapshot.duplicate(true)
corrupt_index[&"active_elevator_units"][0][&"state"] = &"INVALID"
_expect(not restored.restore_snapshot(corrupt_index), "corrupt elevator active index is rejected")
_expect(restored.create_snapshot() == before_corrupt, "active-index rejection is also fully atomic")
var v2_snapshot := snapshot.duplicate(true)
v2_snapshot[&"schema_version"] = 2
v2_snapshot.erase(&"known_elevator_request_ids")
v2_snapshot.erase(&"active_elevator_units")
var v2_restored = PathServiceScript.new()
_expect(v2_restored.restore_snapshot(v2_snapshot), "schema v2 snapshot remains readable")
_expect(v2_restored.rebuild_graph([from_cell, to_cell]), "schema v2 traffic validates against authoritative cells")
_expect(v2_restored.get_active_elevator_request(&"guest_a")[&"state"] == v2_restored.ELEVATOR_REQUEST_QUEUED, "v2 queued requests derive new active indexes deterministically")
var legacy_connector := {
&"id": &"legacy_door",
&"from": from_cell,
&"to": to_cell,
&"cost": 150,
&"bidirectional": true,
&"access_id": &"",
&"enabled": true,
}
var legacy = PathServiceScript.new()
_expect(
legacy.restore_snapshot({&"schema_version": 1, &"connectors": [legacy_connector]}),
"legacy connector-only snapshot remains readable"
)
_expect(legacy.rebuild_graph([from_cell, to_cell]), "legacy restore rebuilds against current world cells")
_expect(legacy.find_path(from_cell, to_cell).size() == 2, "legacy connector remains operational")
func _test_input_order_determinism() -> void:
var first = _build_ordered_service(false)
var reversed = _build_ordered_service(true)
_expect(first.create_snapshot() == reversed.create_snapshot(), "actor and request insertion order does not change canonical snapshot")
var first_path: Array[Vector3i] = first.find_path(Vector3i(0, 0, 0), Vector3i(0, 0, 1))
var reversed_path: Array[Vector3i] = reversed.find_path(Vector3i(0, 0, 0), Vector3i(0, 0, 1))
_expect(first_path == reversed_path, "input order does not change route selection")
_expect(
first.get_elevator_queue(&"deterministic_lift", 20) == reversed.get_elevator_queue(&"deterministic_lift", 20),
"input order does not change elevator queue order"
)
_expect(first.get_current_density(Vector3i(1, 0, 0)) == 3, "canonical projection retains all actors")
func _build_ordered_service(reverse_input: bool):
var service = PathServiceScript.new()
var cells := [Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(0, 0, 1)]
if reverse_input:
cells.reverse()
service.rebuild_graph(cells)
service.configure_elevator(&"deterministic_lift", Vector3i(0, 0, 0), Vector3i(0, 0, 1), {
&"queue_capacity": 5,
})
var occupancy := [
{&"actor_id": &"actor_c", &"cell": Vector3i(1, 0, 0)},
{&"actor_id": &"actor_a", &"cell": Vector3i(1, 0, 0)},
{&"actor_id": &"actor_b", &"cell": Vector3i(1, 0, 0)},
]
var requests := [
_request(&"req_c", &"unit_c", 0, 1, &"NORMAL", 20),
_request(&"req_a", &"unit_a", 0, 1, &"NORMAL", 20),
_request(&"req_b", &"unit_b", 0, 1, &"NORMAL", 20),
]
if reverse_input:
occupancy.reverse()
requests.reverse()
service.project_occupancy(occupancy)
for request: Dictionary in requests:
service.enqueue_elevator_request(&"deterministic_lift", request)
return service
func _diamond_cells() -> Array[Vector3i]:
return [
Vector3i(0, 1, 0),
Vector3i(0, 0, 0),
Vector3i(1, 0, 0),
Vector3i(2, 0, 0),
Vector3i(2, 1, 0),
Vector3i(0, 2, 0),
Vector3i(1, 2, 0),
Vector3i(2, 2, 0),
]
func _request(
request_id: StringName,
unit_id: StringName,
floor_from: int,
floor_to: int,
priority: StringName,
enqueued_at: int
) -> Dictionary:
return {
&"request_id": request_id,
&"unit_id": unit_id,
&"floor_from": floor_from,
&"floor_to": floor_to,
&"direction": &"UP" if floor_to > floor_from else &"DOWN",
&"priority": priority,
&"enqueued_at": enqueued_at,
}
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
func _capture_path_sample(duration_us: int) -> void:
_timing_samples.append(duration_us)
+1
View File
@@ -0,0 +1 @@
uid://qbem45x6fu4h
+182
View File
@@ -0,0 +1,182 @@
extends SceneTree
const PathServiceScript = preload("res://scripts/world/path_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: deterministic PathService")
_test_deterministic_tie_break()
_test_layer_separation_and_connector()
_test_connector_access_and_direction()
_test_cache_invalidation_and_revision()
_test_connector_only_snapshot()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_deterministic_tie_break() -> void:
var service = PathServiceScript.new()
var passable := [
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0),
Vector3i(0, 1, 0), Vector3i(2, 1, 0),
Vector3i(0, 2, 0), Vector3i(1, 2, 0), Vector3i(2, 2, 0),
]
_expect(service.rebuild_graph(passable), "array of Vector3i cells rebuilds the path graph")
var expected: Array[Vector3i] = [
Vector3i(0, 1, 0),
Vector3i(0, 0, 0),
Vector3i(1, 0, 0),
Vector3i(2, 0, 0),
Vector3i(2, 1, 0),
]
var first: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), service.FRONT)
var second: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), service.FRONT)
_expect(first == expected, "equal-cost routes use coordinate dictionary order as the tie break")
_expect(second == first, "the cached route exactly reproduces the calculated route")
_expect(service.cache_hits == 1 and service.cache_misses == 1, "path cache records one miss followed by one hit")
func _test_layer_separation_and_connector() -> void:
var service = PathServiceScript.new()
var passable_set := {}
for layer in [service.FRONT, service.BACK]:
for x in range(3):
passable_set[Vector3i(x, 0, layer)] = true
_expect(service.rebuild_graph(passable_set), "Dictionary set can supply passable cells without a WorldService dependency")
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1)).is_empty(),
"FRONT and BACK never connect through ordinary cardinal edges"
)
_expect(
service.configure_connector(
&"service_door",
Vector3i(2, 0, 0),
Vector3i(0, 0, 1),
120,
true
),
"an explicit layer connector is accepted"
)
var connected_path: Array[Vector3i] = service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1))
_expect(
connected_path.has(Vector3i(2, 0, 0)) and connected_path.has(Vector3i(0, 0, 1)),
"an explicit connector joins the two isolated layers"
)
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1), service.FRONT).is_empty(),
"an access-layer restriction rejects a route that enters BACK"
)
func _test_connector_access_and_direction() -> void:
var service = PathServiceScript.new()
var cells := [Vector3i(0, 0, 0), Vector3i(0, 2, 0)]
service.rebuild_graph(cells)
service.configure_connector(
&"staff_stairs",
Vector3i(0, 0, 0),
Vector3i(0, 2, 0),
150,
false,
&"staff"
)
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0)).is_empty(),
"a restricted connector requires its access id"
)
var staff_profile := {
&"allowed_layers": [service.FRONT],
&"connector_access_ids": [&"staff"],
}
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0), staff_profile).size() == 2,
"an access profile enables its matching connector"
)
_expect(
service.find_path(Vector3i(0, 2, 0), Vector3i(0, 0, 0), staff_profile).is_empty(),
"a one-way connector cannot be traversed in reverse"
)
func _test_cache_invalidation_and_revision() -> void:
var service = PathServiceScript.new()
var passable := [
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0),
Vector3i(0, 1, 0), Vector3i(2, 1, 0),
Vector3i(0, 2, 0), Vector3i(1, 2, 0), Vector3i(2, 2, 0),
]
service.rebuild_graph(passable)
var initial_revision: int = service.graph_revision
service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
_expect(service.cached_path_count() == 1, "a calculated path is cached")
_expect(
service.invalidate_cache_for_cells([Vector3i(1, 0, 0)]) == 1,
"local invalidation removes a cached path that crosses an affected cell"
)
_expect(service.cached_path_count() == 0, "local invalidation leaves no matching stale cache entry")
service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
_expect(service.invalidate_all_cache() == 1, "full invalidation reports and removes every cache entry")
passable.erase(Vector3i(1, 0, 0))
service.rebuild_graph(passable)
_expect(service.graph_revision == initial_revision + 1, "a changed topology advances graph_revision exactly once")
var rebuilt_path: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
_expect(rebuilt_path.has(Vector3i(1, 2, 0)), "rebuild discards the old route and uses the remaining corridor")
func _test_connector_only_snapshot() -> void:
var cells := [Vector3i(0, 0, 0), Vector3i(0, 0, 1)]
var original = PathServiceScript.new()
original.rebuild_graph(cells)
original.configure_connector(
&"lift_a",
Vector3i(0, 0, 0),
Vector3i(0, 0, 1),
250,
true,
&"lift_pass"
)
original.find_path(
Vector3i(0, 0, 0),
Vector3i(0, 0, 1),
null,
[&"lift_pass"]
)
var snapshot: Dictionary = original.create_snapshot()
_expect(
snapshot.has(&"connectors") and not snapshot.has(&"graph") and not snapshot.has(&"cache") and not snapshot.has(&"passable_cells"),
"snapshot persists connector configuration but no derived graph or cache"
)
var restored = PathServiceScript.new()
_expect(restored.restore_snapshot(snapshot), "connector configuration restores independently of world cells")
_expect(restored.rebuild_graph(cells), "restored service rebuilds its graph from authoritative passable cells")
_expect(
restored.find_path(
Vector3i(0, 0, 0),
Vector3i(0, 0, 1),
null,
[&"lift_pass"]
).size() == 2,
"restored connector produces the same cross-layer route after rebuild"
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://bbqw1dome2hdp
+496
View File
@@ -0,0 +1,496 @@
extends SceneTree
const AdapterType = preload("res://scripts/world/route_subslot_path_adapter.gd")
const SpatialType = preload("res://scripts/world/spatial_subslot_service.gd")
const PathType = preload("res://scripts/world/path_service.gd")
const WorldType = preload("res://scripts/world/world_service.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
class ControlledPathService:
extends PathService
var fail_next_rebuild_after_mutation := false
var reject_restore := false
var reenter_next_rebuild := false
var tamper_adapter_next_rebuild := false
var adapter: Variant = null
var reentry_result: Variant = null
func rebuild_graph(passable_cells: Variant) -> bool:
var should_fail := fail_next_rebuild_after_mutation
fail_next_rebuild_after_mutation = false
var should_reenter := reenter_next_rebuild
reenter_next_rebuild = false
var should_tamper := tamper_adapter_next_rebuild
tamper_adapter_next_rebuild = false
var rebuilt := super.rebuild_graph(passable_cells)
if should_reenter and adapter != null:
reentry_result = adapter.place_route(
&"nested_path_request",
&"nested_path_route",
[Vector3i(40, 0, 0)]
)
if should_tamper and adapter != null:
adapter.projection_revision += 99
if should_fail:
last_error = "Injected Path rebuild failure after mutation."
return false
return rebuilt
func restore_snapshot(snapshot: Dictionary) -> bool:
if reject_restore:
last_error = "Injected Path restore failure."
return false
return super.restore_snapshot(snapshot)
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: atomic ROUTE subslot Path projection")
_test_basic_projection_and_world_coexistence()
_test_replace_and_layer_separation()
_test_idempotency_and_defensive_queries()
_test_stale_detection_and_synchronization()
_test_path_failure_rolls_both_services_back()
_test_callback_reentry_and_tamper_defense()
_test_uncertain_rollback_quarantines_and_recovers()
_test_strict_snapshot_round_trip_and_atomic_rejection()
_test_strict_input_and_route_only_boundary()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_basic_projection_and_world_coexistence() -> void:
var source := SpatialType.new()
var path := PathType.new()
var world := WorldType.new()
world.reset({&"min_x": -2, &"max_x": 4, &"min_y": -2, &"max_y": 2})
var start := Vector3i(0, 0, 0)
var bridge := Vector3i(1, 0, 0)
var goal := Vector3i(2, 0, 0)
_expect(world.occupy(&"major_facility", [bridge], &"facility_large"), "major World occupancy exists independently")
var adapter := AdapterType.new()
_expect(adapter.configure(source, path, [goal, start]), "base walkable cells configure the initial Path projection")
_expect(path.find_path(start, goal).is_empty(), "disconnected base cells are initially unreachable")
var placed = adapter.place_route(&"route_place_1", &"route_bridge", [bridge])
_expect(placed.is_success(), "ROUTE placement and Path rebuild commit together")
_expect(path.find_path(start, goal) == [start, bridge, goal], "ROUTE subslot makes the real Path graph reachable")
_expect(world.get_occupant(bridge) == &"major_facility", "ROUTE projection preserves primary World occupancy")
_expect(adapter.source_revision == 1 and adapter.projection_revision == 2, "source and projection revisions advance deterministically")
_expect(not adapter.is_projection_stale(), "committed projection is fresh")
_expect(adapter.remove_route(&"route_remove_1", &"route_bridge").is_success(), "ROUTE removal and Path rebuild commit together")
_expect(path.find_path(start, goal).is_empty(), "removing the ROUTE makes the real Path graph unreachable again")
_expect(adapter.validate_invariants()[&"ok"], "basic projection crosslinks remain valid")
func _test_replace_and_layer_separation() -> void:
var source := SpatialType.new()
var path := PathType.new()
var front_start := Vector3i(0, 2, 0)
var front_mid := Vector3i(1, 2, 0)
var front_goal := Vector3i(2, 2, 0)
var back_start := Vector3i(0, 2, 1)
var back_mid := Vector3i(1, 2, 1)
var back_goal := Vector3i(2, 2, 1)
var adapter := AdapterType.new()
_expect(
adapter.configure(source, path, [back_goal, front_goal, back_start, front_start]),
"two-layer base projection configures in canonical order"
)
_expect(
adapter.place_route(&"layers_place", &"dual_layer_route", [back_mid, front_mid]).is_success(),
"one ROUTE entity can project cells on both layers"
)
_expect(path.find_path(front_start, front_goal) == [front_start, front_mid, front_goal], "FRONT route is reachable")
_expect(path.find_path(back_start, back_goal) == [back_start, back_mid, back_goal], "BACK route is reachable")
_expect(path.find_path(front_start, back_goal).is_empty(), "FRONT and BACK remain disconnected without a connector")
var replaced = adapter.replace_route(&"layers_replace", &"dual_layer_route", [front_mid])
_expect(replaced.is_success() and replaced.details[&"changed"], "ROUTE replacement commits atomically")
_expect(path.find_path(front_start, front_goal).size() == 3, "replacement retains the FRONT route")
_expect(path.find_path(back_start, back_goal).is_empty(), "replacement removes the BACK projection")
var base_route := adapter.place_route(&"base_overlap_place", &"base_overlap_route", [front_start])
_expect(base_route.is_success(), "ROUTE may overlap a base walkable cell")
_expect(adapter.remove_route(&"base_overlap_remove", &"base_overlap_route").is_success(), "base-overlap ROUTE removes cleanly")
_expect(path.has_cell(front_start), "removing a ROUTE never removes the overlapping base cell")
_expect(adapter.validate_invariants()[&"ok"], "replacement and layer projection crosslinks remain valid")
func _test_idempotency_and_defensive_queries() -> void:
var source := SpatialType.new()
var path := PathType.new()
var adapter := AdapterType.new()
var start := Vector3i(0, 0, 0)
var middle := Vector3i(1, 0, 0)
var goal := Vector3i(2, 0, 0)
adapter.configure(source, path, [goal, start])
var first = adapter.place_route(&"z_request", &"route_idem", [middle])
var after_first_adapter := adapter.create_snapshot()
var after_first_source := source.create_snapshot()
var after_first_path := path.create_snapshot()
var replay = adapter.place_route(&"z_request", &"route_idem", [middle])
_expect(first.is_success() and replay.is_success() and replay.is_replay, "same request ID and fingerprint replays")
_expect(replay.details == first.details, "idempotent replay preserves exact result details")
_expect(adapter.create_snapshot() == after_first_adapter, "replay does not advance adapter state")
_expect(source.create_snapshot() == after_first_source and path.create_snapshot() == after_first_path, "replay does not touch either authoritative service")
var conflict = adapter.place_route(&"z_request", &"route_idem", [Vector3i(1, 1, 0)])
_expect(conflict.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "same request ID with another fingerprint conflicts")
var before_noop_revision := adapter.projection_revision
var noop = adapter.replace_route(&"a_request", &"route_idem", [middle])
_expect(noop.is_success() and not noop.details[&"changed"], "canonical-equivalent replace is a successful no-op")
_expect(adapter.projection_revision == before_noop_revision, "no-op request does not advance projection revision")
_expect(adapter.get_processed_request_ids() == [&"a_request", &"z_request"], "processed request IDs are lexical")
var returned_base := adapter.get_base_walkable_cells()
var returned_route := adapter.get_projected_route_cells()
var returned_walkable := adapter.get_projected_walkable_cells()
var returned_status := adapter.get_projection_status()
returned_base.clear()
returned_route.clear()
returned_walkable.clear()
returned_status[&"source_changed"] = true
_expect(adapter.get_base_walkable_cells().size() == 2, "base-cell query is defensive")
_expect(adapter.get_projected_route_cells() == [middle], "route projection query is defensive")
_expect(adapter.get_projected_walkable_cells().size() == 3, "walkable projection query is defensive")
_expect(not adapter.get_projection_status()[&"source_changed"], "projection status query is defensive")
func _test_stale_detection_and_synchronization() -> void:
var source := SpatialType.new()
var path := PathType.new()
var adapter := AdapterType.new()
var start := Vector3i(0, 0, 0)
var middle := Vector3i(1, 0, 0)
var goal := Vector3i(2, 0, 0)
var base := [start, goal]
adapter.configure(source, path, base)
_expect(source.place(&"external_route", source.CATEGORY_ROUTE, [middle]).is_success(), "direct source mutation can occur outside the adapter boundary")
var stale_status := adapter.get_projection_status()
_expect(stale_status[&"stale"] and stale_status[&"source_changed"], "direct ROUTE mutation is detected as stale")
var blocked = adapter.place_route(&"blocked_while_stale", &"blocked_route", [Vector3i(3, 0, 0)])
_expect(blocked.code == CommandResultType.Code.TRANSACTION_CLOSED, "route mutation is blocked while projection is stale")
var synchronized = adapter.synchronize_projection(&"sync_source_1")
_expect(synchronized.is_success() and synchronized.details[&"changed"], "explicit synchronization projects the external ROUTE change")
_expect(path.find_path(start, goal) == [start, middle, goal], "source synchronization repairs real Path reachability")
_expect(adapter.source_revision == source.get_revision_for_category(source.CATEGORY_ROUTE), "synchronization records the exact source revision")
var sync_replay = adapter.synchronize_projection(&"sync_source_1")
_expect(sync_replay.is_success() and sync_replay.is_replay, "synchronization request is idempotently replayed")
path.rebuild_graph(base)
_expect(adapter.get_projection_status()[&"path_changed"], "direct Path topology drift is detected")
var repaired = adapter.synchronize_projection(&"sync_path_2")
_expect(repaired.is_success() and repaired.details[&"changed"], "synchronization repairs direct Path drift")
_expect(path.find_path(start, goal).size() == 3 and not adapter.is_projection_stale(), "Path drift repair restores a fresh route")
var projection_before_decoration := adapter.projection_revision
_expect(source.place(&"external_decor", source.CATEGORY_DECORATION, [middle]).is_success(), "non-ROUTE source category changes independently")
_expect(not adapter.is_projection_stale(), "DECORATION revision does not stale the ROUTE projection")
_expect(adapter.projection_revision == projection_before_decoration, "unrelated source mutation does not advance projection revision")
var base_source := SpatialType.new()
var base_path := PathType.new()
var base_adapter := AdapterType.new()
base_adapter.configure(base_source, base_path, base)
var base_added = base_adapter.synchronize_projection(&"base_add", [goal, middle, start])
_expect(base_added.is_success() and base_added.details[&"changed"], "synchronization atomically replaces the base walkable set")
_expect(base_path.find_path(start, goal).size() == 3, "new base walkability participates in the rebuilt graph")
_expect(base_adapter.get_base_walkable_cells() == [start, middle, goal], "updated base cells are canonical and queryable")
var base_removed = base_adapter.synchronize_projection(&"base_remove", base)
_expect(base_removed.is_success() and base_removed.details[&"changed"], "base walkability can be removed atomically")
_expect(base_path.find_path(start, goal).is_empty(), "removed base walkability disappears from the graph")
_expect(base_adapter.validate_invariants()[&"ok"], "dynamic base synchronization preserves crosslinks")
func _test_path_failure_rolls_both_services_back() -> void:
var source := SpatialType.new()
var path := ControlledPathService.new()
var adapter := AdapterType.new()
path.adapter = adapter
var start := Vector3i(0, 0, 0)
var middle := Vector3i(1, 0, 0)
var alternate := Vector3i(1, 1, 0)
var goal := Vector3i(2, 0, 0)
adapter.configure(source, path, [start, goal])
var before_adapter := adapter.create_snapshot()
var before_source := source.create_snapshot()
var before_path := path.create_snapshot()
path.fail_next_rebuild_after_mutation = true
var failed_place = adapter.place_route(&"retryable_place", &"route_atomic", [middle])
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, failed_place, "failed place")
_expect(not adapter.is_quarantined(), "exact place rollback does not quarantine")
_expect(not source.has_entity(&"route_atomic") and path.find_path(start, goal).is_empty(), "failed place leaves neither side applied")
_expect(adapter.place_route(&"retryable_place", &"route_atomic", [middle]).is_success(), "failed request ID can be retried after exact rollback")
before_adapter = adapter.create_snapshot()
before_source = source.create_snapshot()
before_path = path.create_snapshot()
path.fail_next_rebuild_after_mutation = true
var failed_replace = adapter.replace_route(&"retryable_replace", &"route_atomic", [alternate])
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, failed_replace, "failed replace")
_expect(source.get_entity_cells(&"route_atomic") == [middle], "failed replacement restores the original ROUTE cells")
_expect(adapter.replace_route(&"retryable_replace", &"route_atomic", [alternate]).is_success(), "replacement retries after exact rollback")
before_adapter = adapter.create_snapshot()
before_source = source.create_snapshot()
before_path = path.create_snapshot()
path.fail_next_rebuild_after_mutation = true
var failed_remove = adapter.remove_route(&"retryable_remove", &"route_atomic")
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, failed_remove, "failed remove")
_expect(source.has_entity(&"route_atomic"), "failed removal restores the ROUTE entity")
_expect(adapter.remove_route(&"retryable_remove", &"route_atomic").is_success(), "removal retries after exact rollback")
path.adapter = null
func _test_callback_reentry_and_tamper_defense() -> void:
var source := SpatialType.new()
var path := ControlledPathService.new()
var adapter := AdapterType.new()
path.adapter = adapter
adapter.configure(source, path, [Vector3i(0, 0, 0), Vector3i(2, 0, 0)])
path.reenter_next_rebuild = true
var outer = adapter.place_route(&"outer_path_callback", &"outer_route", [Vector3i(1, 0, 0)])
_expect(outer.is_success(), "outer transaction commits when Path callback only attempts reentry")
_expect(path.reentry_result != null and not path.reentry_result.is_success(), "Path callback reentry is rejected")
_expect(not source.has_entity(&"nested_path_route"), "reentrant Path callback cannot create a nested ROUTE")
var nested_source_result: Array = [null]
source.configure_compatibility_validator(
func(_payload: Dictionary) -> Dictionary:
nested_source_result[0] = adapter.place_route(
&"nested_source_request",
&"nested_source_route",
[Vector3i(5, 0, 0)]
)
return {&"ok": true}
)
var source_outer = adapter.place_route(&"outer_source_callback", &"second_route", [Vector3i(3, 0, 0)])
_expect(source_outer.is_success(), "outer transaction commits when Spatial validator only attempts reentry")
_expect(nested_source_result[0] != null and not nested_source_result[0].is_success(), "Spatial callback reentry is rejected")
_expect(not source.has_entity(&"nested_source_route"), "reentrant Spatial callback cannot create a nested ROUTE")
source.configure_compatibility_validator(Callable())
var before_adapter := adapter.create_snapshot()
var before_source := source.create_snapshot()
var before_path := path.create_snapshot()
path.tamper_adapter_next_rebuild = true
var tampered = adapter.replace_route(&"path_tamper_request", &"second_route", [Vector3i(4, 0, 0)])
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, tampered, "Path callback adapter tamper")
_expect(not adapter.is_quarantined(), "detected callback tamper with exact rollback stays available")
source.configure_compatibility_validator(
func(_payload: Dictionary) -> Dictionary:
return {&"ok": false, &"reason": &"WORLD_POLICY_REJECTED"}
)
before_adapter = adapter.create_snapshot()
before_source = source.create_snapshot()
before_path = path.create_snapshot()
var rejected = adapter.place_route(&"source_rejection", &"rejected_route", [Vector3i(6, 0, 0)])
_assert_exact_rollback(adapter, source, path, before_adapter, before_source, before_path, rejected, "Spatial validator rejection")
source.configure_compatibility_validator(Callable())
path.adapter = null
func _test_uncertain_rollback_quarantines_and_recovers() -> void:
var source := SpatialType.new()
var path := ControlledPathService.new()
var adapter := AdapterType.new()
path.adapter = adapter
var start := Vector3i(0, 0, 0)
var middle := Vector3i(1, 0, 0)
var goal := Vector3i(2, 0, 0)
var base := [start, goal]
adapter.configure(source, path, base)
var recovery_adapter := adapter.create_snapshot()
var recovery_source := source.create_snapshot()
var recovery_path := path.create_snapshot()
path.fail_next_rebuild_after_mutation = true
path.reject_restore = true
var failed = adapter.place_route(&"uncertain_request", &"uncertain_route", [middle])
_expect(not failed.is_success() and not failed.details[&"rollback_ok"], "uncertain rollback reports failure explicitly")
_expect(adapter.is_quarantined() and adapter.get_quarantine_reason() == adapter.QUARANTINE_TRANSACTION_ROLLBACK, "uncertain rollback quarantines the adapter")
_expect(adapter.is_projection_stale(), "quarantined projection reports stale")
var blocked = adapter.place_route(&"blocked_in_quarantine", &"blocked_route", [Vector3i(3, 0, 0)])
_expect(blocked.code == CommandResultType.Code.TRANSACTION_CLOSED, "quarantine blocks further mutations")
path.reject_restore = false
_expect(source.restore_snapshot(recovery_source), "recovery restores the Spatial source snapshot")
_restore_path_for_test(path, base, recovery_path)
_expect(path.create_snapshot() == recovery_path, "recovery restores the exact Path snapshot")
_expect(adapter.restore_snapshot(recovery_adapter), "exact adapter snapshot clears quarantine after dependencies recover")
_expect(not adapter.is_quarantined() and not adapter.is_projection_stale(), "snapshot recovery returns the adapter to service")
_expect(adapter.place_route(&"post_recovery", &"recovered_route", [middle]).is_success(), "route transaction succeeds after quarantine recovery")
path.adapter = null
func _test_strict_snapshot_round_trip_and_atomic_rejection() -> void:
var source := SpatialType.new()
var path := PathType.new()
var adapter := AdapterType.new()
var base := [Vector3i(0, 0, 0), Vector3i(3, 0, 0)]
adapter.configure(source, path, base)
adapter.place_route(&"z_place", &"route_z", [Vector3i(1, 0, 0)])
adapter.place_route(&"a_place", &"route_a", [Vector3i(2, 0, 0)])
adapter.synchronize_projection(&"b_sync_base", base)
adapter.replace_route(&"m_noop", &"route_a", [Vector3i(2, 0, 0)])
var snapshot := adapter.create_snapshot()
_expect(
snapshot[&"processed_requests"].map(func(row: Dictionary) -> String: return row[&"request_id"]) == ["a_place", "b_sync_base", "m_noop", "z_place"],
"snapshot request history is lexical"
)
var restored_source := SpatialType.new()
_expect(restored_source.restore_snapshot(source.create_snapshot()), "snapshot test restores a matching Spatial source")
var restored_path := PathType.new()
var restored := AdapterType.new()
_expect(restored.configure(restored_source, restored_path, base), "snapshot test configures matching dependencies")
_expect(restored.restore_snapshot(snapshot), "strict current adapter snapshot restores")
_expect(restored.create_snapshot() == snapshot, "adapter snapshot round trip is exact")
_expect(restored.validate_invariants()[&"ok"], "restored adapter snapshot crosslinks are valid")
var replay = restored.place_route(&"z_place", &"route_z", [Vector3i(1, 0, 0)])
_expect(replay.is_success() and replay.is_replay, "restored idempotency history replays exactly")
var before := restored.create_snapshot()
var source_before := restored_source.create_snapshot()
var path_before := restored_path.create_snapshot()
var version_type := snapshot.duplicate(true)
version_type[&"schema_version"] = "1"
_atomic_snapshot_reject(restored, restored_source, restored_path, version_type, before, source_before, path_before, "string schema version")
var unknown := snapshot.duplicate(true)
unknown[&"unknown"] = true
_atomic_snapshot_reject(restored, restored_source, restored_path, unknown, before, source_before, path_before, "unknown root field")
var source_revision_type := snapshot.duplicate(true)
source_revision_type[&"source_revision"] = float(source_revision_type[&"source_revision"])
_atomic_snapshot_reject(restored, restored_source, restored_path, source_revision_type, before, source_before, path_before, "floating source revision")
var projection_bound := snapshot.duplicate(true)
projection_bound[&"projection_revision"] = 0
_atomic_snapshot_reject(restored, restored_source, restored_path, projection_bound, before, source_before, path_before, "zero projection revision")
var projection_history_gap := snapshot.duplicate(true)
projection_history_gap[&"projection_revision"] = int(projection_history_gap[&"projection_revision"]) + 1
_atomic_snapshot_reject(restored, restored_source, restored_path, projection_history_gap, before, source_before, path_before, "projection revision without request evidence")
var unordered_base := snapshot.duplicate(true)
unordered_base[&"base_walkable_cells"].reverse()
_atomic_snapshot_reject(restored, restored_source, restored_path, unordered_base, before, source_before, path_before, "unordered base cells")
var duplicate_route := snapshot.duplicate(true)
duplicate_route[&"projected_route_cells"].append(duplicate_route[&"projected_route_cells"][0])
_atomic_snapshot_reject(restored, restored_source, restored_path, duplicate_route, before, source_before, path_before, "duplicate projected route cell")
var broken_union := snapshot.duplicate(true)
broken_union[&"projected_walkable_cells"].remove_at(1)
_atomic_snapshot_reject(restored, restored_source, restored_path, broken_union, before, source_before, path_before, "broken projected union")
var quarantine_reason := snapshot.duplicate(true)
quarantine_reason[&"quarantine_reason"] = "UNKNOWN_REASON"
_atomic_snapshot_reject(restored, restored_source, restored_path, quarantine_reason, before, source_before, path_before, "orphan quarantine reason")
var request_order := snapshot.duplicate(true)
request_order[&"processed_requests"].reverse()
_atomic_snapshot_reject(restored, restored_source, restored_path, request_order, before, source_before, path_before, "unordered request history")
var request_extra := snapshot.duplicate(true)
request_extra[&"processed_requests"][0][&"extra"] = true
_atomic_snapshot_reject(restored, restored_source, restored_path, request_extra, before, source_before, path_before, "unknown request field")
var fingerprint := snapshot.duplicate(true)
fingerprint[&"processed_requests"][0][&"fingerprint"] = "tampered"
_atomic_snapshot_reject(restored, restored_source, restored_path, fingerprint, before, source_before, path_before, "request fingerprint tamper")
var sync_input := snapshot.duplicate(true)
sync_input[&"processed_requests"][1][&"input_cells"] = [Vector3i(9, 0, 0)]
_atomic_snapshot_reject(restored, restored_source, restored_path, sync_input, before, source_before, path_before, "synchronization base fingerprint tamper")
var detail_revision := snapshot.duplicate(true)
detail_revision[&"processed_requests"][0][&"details"][&"projection_revision"] = int(snapshot[&"projection_revision"]) + 1
_atomic_snapshot_reject(restored, restored_source, restored_path, detail_revision, before, source_before, path_before, "request result revision tamper")
var detail_cells := snapshot.duplicate(true)
detail_cells[&"processed_requests"][0][&"details"][&"cells"] = [Vector3i(8, 0, 0)]
_atomic_snapshot_reject(restored, restored_source, restored_path, detail_cells, before, source_before, path_before, "request result cell tamper")
var deep := snapshot.duplicate(true)
var deep_value: Variant = []
for _index: int in range(adapter.MAX_SNAPSHOT_DEPTH + 2):
deep_value = [deep_value]
deep[&"processed_requests"] = deep_value
_atomic_snapshot_reject(restored, restored_source, restored_path, deep, before, source_before, path_before, "excessive snapshot depth")
restored_path.rebuild_graph(base)
var stale_before := restored.create_snapshot()
_expect(not restored.restore_snapshot(snapshot), "valid metadata snapshot rejects mismatched live Path topology")
_expect(restored.create_snapshot() == stale_before, "live-crosslink rejection is adapter-atomic")
_expect(restored.synchronize_projection(&"repair_after_restore_reject").is_success(), "synchronization repairs live mismatch after restore rejection")
func _test_strict_input_and_route_only_boundary() -> void:
var invalid_adapter := AdapterType.new()
_expect(not invalid_adapter.configure(SpatialType.new(), PathType.new(), Vector3i.ZERO), "non-Array base cells are rejected")
_expect(not invalid_adapter.configure(SpatialType.new(), PathType.new(), [Vector3i.ZERO, Vector3i.ZERO]), "duplicate base cells are rejected")
_expect(not invalid_adapter.configure(SpatialType.new(), PathType.new(), [Vector3i(0, 0, 2)]), "invalid base layer is rejected")
var source := SpatialType.new()
var path := PathType.new()
var adapter := AdapterType.new()
adapter.configure(source, path, [Vector3i.ZERO])
_expect(adapter.is_bound_to(source, path), "adapter reports its exact authoritative service binding")
_expect(not adapter.is_bound_to(SpatialType.new(), path), "binding check rejects another equal Spatial instance")
_expect(not adapter.is_bound_to(source, PathType.new()), "binding check rejects another equal Path instance")
var before := adapter.create_snapshot()
_expect(not adapter.place_route(1, &"route", [Vector3i(1, 0, 0)]).is_success(), "numeric request ID is rejected")
_expect(not adapter.place_route(&"bad_entity", 1, [Vector3i(1, 0, 0)]).is_success(), "numeric entity ID is rejected")
_expect(not adapter.place_route(&"bad_cells", &"route", Vector3i(1, 0, 0)).is_success(), "non-Array route cells are rejected")
_expect(not adapter.place_route(&"empty_cells", &"route", []).is_success(), "empty route placement is rejected")
_expect(not adapter.place_route(&"duplicate_cells", &"route", [Vector3i(1, 0, 0), Vector3i(1, 0, 0)]).is_success(), "duplicate route cells are rejected")
_expect(not adapter.place_route(&"invalid_layer", &"route", [Vector3i(1, 0, 2)]).is_success(), "invalid route layer is rejected")
_expect(adapter.create_snapshot() == before, "malformed route requests preserve adapter state")
_expect(source.place(&"decor_only", source.CATEGORY_DECORATION, [Vector3i.ZERO]).is_success(), "DECORATION entity exists beside adapter boundary")
var source_before := source.create_snapshot()
var path_before := path.create_snapshot()
var wrong_category = adapter.remove_route(&"remove_decor", &"decor_only")
_expect(not wrong_category.is_success(), "adapter refuses to remove a non-ROUTE entity")
_expect(source.create_snapshot() == source_before and path.create_snapshot() == path_before, "route-only rejection leaves both services untouched")
_expect(not adapter.configure(source, path, [Vector3i.ZERO]), "configured adapter rejects reconfiguration")
func _assert_exact_rollback(
adapter: RouteSubslotPathAdapter,
source: SpatialSubslotService,
path: PathService,
adapter_before: Dictionary,
source_before: Dictionary,
path_before: Dictionary,
result: CommandResult,
label: String
) -> void:
_expect(not result.is_success() and bool(result.details.get(&"rollback_ok", false)), "%s reports exact rollback" % label)
_expect(adapter.create_snapshot() == adapter_before, "%s restores exact adapter snapshot" % label)
_expect(source.create_snapshot() == source_before, "%s restores exact Spatial snapshot" % label)
_expect(path.create_snapshot() == path_before, "%s restores exact Path snapshot" % label)
func _atomic_snapshot_reject(
adapter: RouteSubslotPathAdapter,
source: SpatialSubslotService,
path: PathService,
candidate: Dictionary,
adapter_before: Dictionary,
source_before: Dictionary,
path_before: Dictionary,
label: String
) -> void:
_expect(not adapter.restore_snapshot(candidate), "%s is rejected" % label)
_expect(adapter.create_snapshot() == adapter_before, "%s rejection is adapter-atomic" % label)
_expect(source.create_snapshot() == source_before and path.create_snapshot() == path_before, "%s rejection leaves dependencies untouched" % label)
func _restore_path_for_test(path: PathService, cells: Array, snapshot: Dictionary) -> void:
path.rebuild_graph(cells)
path.restore_snapshot(snapshot)
path.rebuild_graph(cells)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bigl6r8i1b5cl
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://b4465rs47daij
+188
View File
@@ -0,0 +1,188 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: runtime world and construction integration")
_reset_runtime()
_test_world_bootstrap()
_test_rank_expansion()
_test_build_to_completion()
_test_overlap_is_atomic()
_test_mid_build_snapshot()
_test_cancel_and_demolish()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime() -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 8080)
_clock.set_paused(true)
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_world_bootstrap() -> void:
var bounds: Dictionary = _runtime.world_service.get_bounds()
_expect(bounds == {&"min_x": -12, &"max_x": 12, &"min_y": 0, &"max_y": 6}, "Rank 1 world uses canonical map bounds")
_expect(_runtime.world_service.is_walkable(Vector3i(-12, 0, WorldService.LAYER_FRONT)), "starter FRONT boundary corridor is walkable")
_expect(_runtime.world_service.is_support_anchor(Vector3i(0, 0, WorldService.LAYER_FRONT)), "starter boundary corridor is structurally anchored")
_expect(_runtime.path_service.has_connector(&"starter.staff_door"), "starter staff door explicitly connects FRONT and BACK")
func _test_rank_expansion() -> void:
var reputation = _runtime.submit_command(
&"update_progression_metrics",
{&"metrics": {&"reputation": 25}},
&"player", &"", 100, -1, &"s1.rank.reputation"
)
var capital = _runtime.submit_command(
&"change_resources",
{&"deltas": {&"gold": 5_000}},
&"player", &"", 100, -1, &"s1.rank.capital"
)
_expect(reputation.is_success() and capital.is_success(), "Rank 2 eligibility facts are recorded before promotion")
var result = _runtime.submit_command(
&"set_world_tree_rank",
{&"rank": 2},
&"player", &"", 100, -1, &"s1.rank.two"
)
_expect(result.is_success() and _state.world_tree_rank == 2, "rank command expands progression exactly once")
_expect(_runtime.world_service.get_bounds() == {&"min_x": -18, &"max_x": 18, &"min_y": -4, &"max_y": 10}, "Rank 2 uses canonical expanded bounds")
_expect(_runtime.world_service.is_walkable(Vector3i(18, 0, WorldService.LAYER_FRONT)), "new boundary corridor cells become walkable")
_expect(not _runtime.world_service.is_excavated(Vector3i(0, -1, WorldService.LAYER_FRONT)), "new underground cells remain unexcavated")
var normalize_capital = _runtime.submit_command(
&"change_resources",
{&"deltas": {&"gold": -5_000}},
&"player", &"", 100, -1, &"s1.rank.capital.normalize"
)
_expect(normalize_capital.is_success() and _state.get_resource(&"gold") == 5_000, "Rank fixture restores the construction budget")
var rejected = _runtime.submit_command(
&"set_world_tree_rank", {&"rank": 1}, &"player", &"", 100, -1, &"s1.rank.shrink"
)
_expect(not rejected.is_success(), "world rank and bounds cannot shrink")
func _test_build_to_completion() -> void:
var result = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"reception", &"origin": Vector3i(-6, 0, WorldService.LAYER_FRONT), &"rotation": 0},
&"player", &"", 50, -1, &"s1.build.reception"
)
var build_label := "valid build command creates a construction job"
if not result.is_success():
build_label += " (%s: %s; %s)" % [CommandResult.Code.keys()[result.code], result.message, result.details]
_expect(result.is_success(), build_label)
var job_id := StringName(result.details.get(&"job_id", &""))
var job = _runtime.construction_service.get_job(job_id)
_expect(job != null and job.state == ConstructionJob.State.PLANNED, "new facility begins as PLANNED")
_expect(_state.get_resource(&"gold") == 4500 and _state.get_resource(&"materials") == 42, "construction cost is paid once")
_expect(not job_id.is_empty() and _runtime.world_service.get_occupant(Vector3i(-6, 0, 0)) == job_id, "construction job reserves its footprint")
_expect(_runtime.latest_digest.length() == 64, "build command leaves a serializable runtime snapshot")
if job == null:
return
_clock.advance_minutes(119)
_expect(job.state == ConstructionJob.State.BUILDING and job.progress < ConstructionJob.MAX_PROGRESS, "job remains BUILDING before its final minute")
_clock.advance_one_minute()
_expect(job.state == ConstructionJob.State.COMPLETED, "job completes at its configured build time")
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "completion creates one facility instance")
var instance_id := StringName(facilities[0][&"instance_id"])
_expect(_runtime.world_service.get_occupant(Vector3i(-6, 0, 0)) == instance_id, "completion transfers world occupancy to the facility")
var saw_completion := false
for event in _runtime.domain_events.recent_events():
if event.event_type == &"FACILITY_COMPLETED" and instance_id in event.entity_ids:
saw_completion = true
_expect(saw_completion, "facility completion is published as a domain event")
func _test_overlap_is_atomic() -> void:
var before_resources: Dictionary = _state.create_snapshot()
var before_job_count: int = _runtime.construction_service.get_jobs().size()
var result = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"reception", &"origin": [-6, 0, 0], &"rotation": 0},
&"player", &"", 50, -1, &"s1.build.overlap"
)
_expect(not result.is_success(), "overlapping construction command is rejected")
_expect(_state.create_snapshot() == before_resources, "rejected placement spends no resource")
_expect(_runtime.construction_service.get_jobs().size() == before_job_count, "rejected placement creates no job")
func _test_mid_build_snapshot() -> void:
var result = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"warehouse", &"origin": Vector3i(2, 0, WorldService.LAYER_BACK), &"rotation": 0},
&"player", &"", 40, -1, &"s1.build.warehouse"
)
_expect(result.is_success(), "BACK-layer warehouse construction starts")
var job_id := StringName(result.details[&"job_id"])
_clock.advance_minutes(90)
var snapshot: Dictionary = _runtime.create_snapshot()
var expected_digest := SimulationDigest.compute(snapshot)
var expected_progress: int = _runtime.construction_service.get_job(job_id).progress
_clock.advance_minutes(90)
_expect(_runtime.construction_service.get_job(job_id).state == ConstructionJob.State.COMPLETED, "warehouse can complete after the snapshot")
_expect(_runtime.restore_snapshot(snapshot), "mid-build world and construction snapshot restores")
_expect(_runtime.latest_digest == expected_digest, "mid-build restore reproduces the exact digest")
_expect(_runtime.construction_service.get_job(job_id).progress == expected_progress, "mid-build integer progress restores exactly")
_clock.advance_minutes(90)
_expect(_runtime.construction_service.get_job(job_id).state == ConstructionJob.State.COMPLETED, "restored build completes once")
func _test_cancel_and_demolish() -> void:
var materials_before_cancel: int = _state.get_resource(&"materials")
var start = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"cafeteria", &"origin": Vector3i(7, 0, WorldService.LAYER_FRONT), &"rotation": 90},
&"player", &"", 30, -1, &"s1.build.cancel_target"
)
_expect(start.is_success(), "cancellable construction starts")
var job_id := StringName(start.details[&"job_id"])
var cancel = _runtime.submit_command(
&"cancel_construction",
{&"job_id": job_id},
&"player", &"", 30, -1, &"s1.cancel.target"
)
_expect(cancel.is_success(), "zero-progress construction cancels")
_expect(_state.get_resource(&"materials") == materials_before_cancel, "zero-progress cancellation returns all materials")
_expect(_runtime.world_service.get_entity_cells(job_id).is_empty(), "cancellation releases reserved world cells")
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
var reception_id := StringName(facilities[0][&"instance_id"])
var materials_before_demolition: int = _state.get_resource(&"materials")
var demolition = _runtime.submit_command(
&"demolish_facility",
{&"instance_id": reception_id},
&"player", &"", 30, -1, &"s1.demolish.reception"
)
_expect(demolition.is_success(), "completed facility can be demolished through a command")
_expect(_state.get_resource(&"materials") == materials_before_demolition + 2, "demolition returns 25 percent of reception materials")
_expect(_runtime.world_service.get_entity_cells(reception_id).is_empty(), "demolition releases facility occupancy")
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bonrrpg25e8s
+312
View File
@@ -0,0 +1,312 @@
extends SceneTree
const SpatialSubslotServiceType = preload("res://scripts/world/spatial_subslot_service.gd")
const WorldServiceType = preload("res://scripts/world/world_service.gd")
const DigestType = preload("res://scripts/core/simulation_digest.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: Spatial subslots")
_test_three_independent_slots_and_world_coexistence()
_test_revision_dirty_and_replacement()
_test_strict_input_limits()
_test_compatibility_callback_atomicity_and_reentry()
_test_snapshot_round_trip_and_corruption_rejection()
_test_deterministic_insertion_order()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_three_independent_slots_and_world_coexistence() -> void:
var world := WorldServiceType.new()
world.reset({&"min_x": -3, &"max_x": 3, &"min_y": -3, &"max_y": 3})
var shared := Vector3i(0, 0, 0)
_expect(world.occupy(&"major_facility", [shared], &"facility_large"), "major World occupant registers first")
var service := SpatialSubslotServiceType.new()
var route_cells := [Vector3i(1, 0, 0), shared, Vector3i(0, -1, 1)]
_expect(service.place(&"route_main", service.CATEGORY_ROUTE, route_cells).is_success(), "multi-cell ROUTE shares a primary World cell")
_expect(world.get_occupant(shared) == &"major_facility", "subslot placement never mutates primary World occupancy")
_expect(service.place(&"decor_banner", service.CATEGORY_DECORATION, [shared]).is_success(), "DECORATION shares the ROUTE cell")
_expect(service.place(&"utility_pipe", service.CATEGORY_UTILITY, [shared]).is_success(), "UTILITY shares ROUTE and DECORATION")
_expect(
service.get_cell_slots(shared) == {
&"ROUTE": &"route_main",
&"DECORATION": &"decor_banner",
&"UTILITY": &"utility_pipe",
},
"one layer cell exposes exactly three independent category slots"
)
_expect(
service.get_entity_cells(&"route_main") == [
Vector3i(0, -1, 1), shared, Vector3i(1, 0, 0),
],
"one entity owns multiple cells in canonical y/x/layer order"
)
_expect(
service.get_route_cells() == [Vector3i(0, -1, 1), shared, Vector3i(1, 0, 0)],
"route query is ready for deterministic Path graph input"
)
var before_overlap := service.create_snapshot()
_expect(not service.place(&"route_overlap", service.CATEGORY_ROUTE, [shared]).is_success(), "second ROUTE in one cell is rejected")
_expect(service.create_snapshot() == before_overlap, "same-category overlap rejection is atomic")
_expect(not service.place(&"route_main", service.CATEGORY_UTILITY, [Vector3i(2, 0, 0)]).is_success(), "one entity ID cannot own a second category")
var returned_cells := service.get_entity_cells(&"route_main")
returned_cells.clear()
var returned_entities := service.get_entities()
returned_entities[0][&"cells"].clear()
var returned_slots := service.get_cell_slots(shared)
returned_slots.clear()
_expect(service.get_entity_cells(&"route_main").size() == 3, "entity cell query is defensive")
_expect(service.get_entities().size() == 3, "entity records are defensive")
_expect(service.get_cell_slots(shared).size() == 3, "cell slot query is defensive")
_expect(service.validate_invariants()[&"ok"], "three-way coexistence preserves all crosslinks")
func _test_revision_dirty_and_replacement() -> void:
var service := SpatialSubslotServiceType.new()
var a := Vector3i(1, 1, 0)
var b := Vector3i(-1, 0, 1)
var c := Vector3i(2, -1, 0)
_expect(service.place(&"route", service.CATEGORY_ROUTE, [a, b]).is_success(), "first mutation succeeds")
_expect(service.revision == 1 and service.get_revision_for_category(service.CATEGORY_ROUTE) == 1, "ROUTE mutation advances global and category revisions once")
_expect(service.get_revision_for_category(service.CATEGORY_DECORATION) == 0, "unmodified category revision remains zero")
_expect(service.place(&"decor", service.CATEGORY_DECORATION, [a]).is_success(), "second category mutation succeeds")
_expect(service.revision == 2 and service.get_revision_for_category(service.CATEGORY_DECORATION) == 2, "DECORATION records its exact last revision")
_expect(service.get_dirty_cells() == [b, a], "dirty cells are canonical and deduplicated")
var consumed := service.consume_dirty_cells()
consumed.clear()
_expect(service.get_dirty_cells().is_empty() and service.revision == 2, "dirty consumption is defensive and does not advance revision")
var replacement := service.replace_entity_cells(&"route", [c, a])
_expect(replacement.is_success() and replacement.details[&"changed"], "entity cell set can be atomically replaced")
_expect(service.revision == 3 and service.get_revision_for_category(service.CATEGORY_ROUTE) == 3, "replacement advances only its category revision")
_expect(service.get_dirty_cells() == [c, b, a], "replacement dirties removed, retained and added cells once")
var no_op_revision := service.revision
_expect(service.replace_entity_cells(&"route", [a, c]).is_success(), "canonical-equivalent replacement is a successful no-op")
_expect(service.revision == no_op_revision, "replacement no-op does not change revision")
_expect(service.remove(&"decor").is_success(), "entity removal succeeds")
_expect(service.revision == 4 and service.get_revision_for_category(service.CATEGORY_DECORATION) == 4, "removal advances exact category revision")
_expect(service.get_slot_entity(a, service.CATEGORY_ROUTE) == &"route", "removal preserves another category in the same cell")
_expect(service.get_slot_entity(a, service.CATEGORY_DECORATION).is_empty(), "removal clears only the owned category slot")
_expect(not service.remove(&"decor").is_success(), "double removal is rejected")
_expect(service.validate_invariants()[&"ok"], "replacement and removal preserve strict crosslinks")
func _test_strict_input_limits() -> void:
var service := SpatialSubslotServiceType.new()
var valid_snapshot := service.create_snapshot()
_expect(not service.place(12, service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "integer entity ID is rejected")
_expect(not service.place(&"", service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "empty entity ID is rejected")
_expect(not service.place(StringName("x".repeat(service.MAX_ENTITY_ID_LENGTH + 1)), service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "oversized entity ID is rejected")
_expect(not service.place(&"bad id", service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "unstable entity ID is rejected")
_expect(not service.place(&"lower_category", &"route", [Vector3i.ZERO]).is_success(), "non-canonical category is rejected")
_expect(not service.place(&"numeric_category", 0, [Vector3i.ZERO]).is_success(), "numeric category is rejected rather than coerced")
_expect(not service.place(&"not_array", service.CATEGORY_ROUTE, Vector3i.ZERO).is_success(), "non-Array cells are rejected")
_expect(not service.place(&"empty_cells", service.CATEGORY_ROUTE, []).is_success(), "empty cell set is rejected")
_expect(not service.place(&"duplicate_cells", service.CATEGORY_ROUTE, [Vector3i.ZERO, Vector3i.ZERO]).is_success(), "duplicate cells are rejected")
_expect(not service.place(&"wrong_cell_type", service.CATEGORY_ROUTE, [Vector2i.ZERO]).is_success(), "non-Vector3i cell is rejected")
_expect(not service.place(&"wrong_layer", service.CATEGORY_ROUTE, [Vector3i(0, 0, 2)]).is_success(), "unknown layer is rejected")
_expect(not service.place(&"deep_coordinate", service.CATEGORY_ROUTE, [Vector3i(0, service.MAX_COORDINATE_ABS + 1, 0)]).is_success(), "coordinate depth limit is enforced")
var too_many_cells: Array[Vector3i] = []
for x: int in range(service.MAX_CELLS_PER_ENTITY + 1):
too_many_cells.append(Vector3i(x, 0, 0))
_expect(not service.place(&"too_many_cells", service.CATEGORY_ROUTE, too_many_cells).is_success(), "per-entity cell capacity is enforced")
_expect(service.create_snapshot() == valid_snapshot and service.revision == 0, "all malformed input failures preserve the empty state")
_expect(service.get_entity_ids(&"route").is_empty(), "invalid category query fails closed")
_expect(service.get_revision_for_category(&"route") == -1, "invalid category revision query fails closed")
func _test_compatibility_callback_atomicity_and_reentry() -> void:
var service := SpatialSubslotServiceType.new()
var callback_payloads: Array[Dictionary] = []
service.configure_compatibility_validator(
func(payload: Dictionary) -> Dictionary:
callback_payloads.append(payload.duplicate(true))
payload[&"after_cells"].clear()
return {&"ok": true, &"details": {&"pass_through": true}}
)
_expect(service.place(&"pass_route", service.CATEGORY_ROUTE, [Vector3i.ZERO]).is_success(), "pass-through validator can approve placement")
_expect(callback_payloads[0][&"operation"] == &"PLACE", "validator receives explicit operation context")
_expect(callback_payloads[0][&"after_cells"] == [Vector3i.ZERO], "validator payload captured by caller retains exact candidate")
_expect(service.get_entity_cells(&"pass_route") == [Vector3i.ZERO], "callback payload mutation cannot alter committed cells")
var callback_report := service.get_last_callback_report()
callback_report[&"details"].clear()
_expect(service.get_last_callback_report()[&"details"] == {&"pass_through": true}, "callback report is a defensive deep copy")
service.configure_compatibility_validator(
func(_payload: Dictionary) -> Dictionary:
return {&"ok": false, &"reason": &"PRIMARY_BLOCKS_CATEGORY"}
)
var before_rejection := service.create_snapshot()
var rejected = service.replace_entity_cells(&"pass_route", [Vector3i(1, 0, 0)])
_expect(not rejected.is_success() and rejected.details[&"rollback_ok"], "callback rejection reports successful pre-commit rollback")
_expect(service.create_snapshot() == before_rejection, "callback failure preserves snapshot and revision exactly")
service.configure_compatibility_validator(
func(_payload: Dictionary) -> Variant:
return true
)
_expect(not service.place(&"invalid_response", service.CATEGORY_UTILITY, [Vector3i(2, 0, 0)]).is_success(), "non-Dictionary callback response is rejected")
_expect(service.create_snapshot() == before_rejection, "invalid callback response is atomic")
var nested_result: Array = [null]
var configure_during_callback: Array = [true]
service.configure_compatibility_validator(
func(_payload: Dictionary) -> Dictionary:
nested_result[0] = service.place(&"nested", service.CATEGORY_DECORATION, [Vector3i(3, 0, 0)])
configure_during_callback[0] = service.configure_compatibility_validator(Callable())
return {&"ok": true}
)
_expect(service.place(&"outer", service.CATEGORY_DECORATION, [Vector3i(2, 0, 0)]).is_success(), "outer callback mutation commits")
_expect(nested_result[0] != null and not nested_result[0].is_success(), "callback reentry is rejected")
_expect(not configure_during_callback[0], "callback cannot replace its own boundary during mutation")
_expect(service.has_entity(&"outer") and not service.has_entity(&"nested"), "reentry rejection commits only the outer entity")
service.configure_compatibility_validator(
func(_payload: Dictionary) -> Dictionary:
service._entity_cells[&"tampered"] = [Vector3i(9, 9, 0)]
return {&"ok": true}
)
var before_tamper := service.create_snapshot()
var tamper_result = service.place(&"tamper_probe", service.CATEGORY_UTILITY, [Vector3i(4, 0, 0)])
_expect(not tamper_result.is_success() and tamper_result.details[&"rollback_ok"], "direct callback tamper is detected and rolled back")
_expect(service.create_snapshot() == before_tamper and not service.has_entity(&"tampered"), "callback tamper rollback restores every internal crosslink")
_expect(service.get_last_callback_report()[&"tampered"], "callback report records tamper detection")
service.configure_compatibility_validator(
func(_payload: Dictionary) -> Dictionary:
service._compatibility_validator = Callable()
return {&"ok": true}
)
var before_boundary_tamper := service.create_snapshot()
var boundary_tamper_result = service.place(&"boundary_tamper", service.CATEGORY_UTILITY, [Vector3i(5, 0, 0)])
_expect(not boundary_tamper_result.is_success() and boundary_tamper_result.details[&"rollback_ok"], "callback cannot replace its validator boundary directly")
_expect(service.create_snapshot() == before_boundary_tamper, "validator-boundary tamper rollback is atomic")
service.configure_compatibility_validator(Callable())
func _test_snapshot_round_trip_and_corruption_rejection() -> void:
var source := SpatialSubslotServiceType.new()
source.place(&"z_route", source.CATEGORY_ROUTE, [Vector3i(2, 0, 0), Vector3i(1, -1, 1)])
source.place(&"a_decor", source.CATEGORY_DECORATION, [Vector3i(2, 0, 0)])
source.place(&"m_utility", source.CATEGORY_UTILITY, [Vector3i(-1, 2, 1)])
source.consume_dirty_cells()
source.replace_entity_cells(&"z_route", [Vector3i(3, 0, 0), Vector3i(1, -1, 1)])
var snapshot := source.create_snapshot()
_expect(snapshot[&"entities"].map(func(row: Dictionary) -> String: return row[&"entity_id"]) == ["a_decor", "m_utility", "z_route"], "snapshot entities are lexical")
var restored := SpatialSubslotServiceType.new()
_expect(restored.restore_snapshot(snapshot), "strict current snapshot restores")
_expect(restored.create_snapshot() == snapshot, "snapshot round trip is exact")
_expect(DigestType.compute(restored.create_snapshot()) == DigestType.compute(snapshot), "snapshot digest round trip is deterministic")
_expect(restored.validate_invariants()[&"ok"], "restored entity/category/cell graph is valid")
var before := restored.create_snapshot()
var version_type := snapshot.duplicate(true)
version_type[&"schema_version"] = "1"
_atomic_reject(restored, version_type, before, "string schema version")
var unknown_field := snapshot.duplicate(true)
unknown_field[&"unknown"] = true
_atomic_reject(restored, unknown_field, before, "unknown top-level field")
var revision_type := snapshot.duplicate(true)
revision_type[&"revision"] = 4.0
_atomic_reject(restored, revision_type, before, "floating revision")
var revision_bound := snapshot.duplicate(true)
revision_bound[&"revision"] = source.MAX_REVISION + 1
_atomic_reject(restored, revision_bound, before, "oversized revision")
var category_keys := snapshot.duplicate(true)
category_keys[&"category_revisions"].erase(&"UTILITY")
_atomic_reject(restored, category_keys, before, "missing category revision")
var category_revision := snapshot.duplicate(true)
category_revision[&"category_revisions"][&"ROUTE"] = int(category_revision[&"revision"]) + 1
_atomic_reject(restored, category_revision, before, "category revision above global revision")
var missing_latest_category := snapshot.duplicate(true)
missing_latest_category[&"category_revisions"][&"ROUTE"] = int(missing_latest_category[&"revision"]) - 1
_atomic_reject(restored, missing_latest_category, before, "no category at global revision")
var entity_order := snapshot.duplicate(true)
entity_order[&"entities"].reverse()
_atomic_reject(restored, entity_order, before, "unordered entities")
var entity_extra := snapshot.duplicate(true)
entity_extra[&"entities"][0][&"extra"] = 1
_atomic_reject(restored, entity_extra, before, "unknown entity field")
var entity_id_type := snapshot.duplicate(true)
entity_id_type[&"entities"][0][&"entity_id"] = 1
_atomic_reject(restored, entity_id_type, before, "non-string entity ID")
var invalid_category := snapshot.duplicate(true)
invalid_category[&"entities"][0][&"category"] = "DECOR"
_atomic_reject(restored, invalid_category, before, "unknown category")
var unsorted_cells := snapshot.duplicate(true)
unsorted_cells[&"entities"][2][&"cells"].reverse()
_atomic_reject(restored, unsorted_cells, before, "unordered entity cells")
var duplicate_category_slot := snapshot.duplicate(true)
duplicate_category_slot[&"entities"].insert(2, {
&"entity_id": "n_route",
&"category": "ROUTE",
&"cells": [Vector3i(3, 0, 0)],
})
_atomic_reject(restored, duplicate_category_slot, before, "duplicate category slot")
var invalid_cell := snapshot.duplicate(true)
invalid_cell[&"entities"][0][&"cells"] = [Vector3i(0, 0, 2)]
_atomic_reject(restored, invalid_cell, before, "invalid entity layer")
var dirty_order := snapshot.duplicate(true)
dirty_order[&"dirty_cells"].reverse()
_atomic_reject(restored, dirty_order, before, "unordered dirty cells")
var dirty_duplicate := snapshot.duplicate(true)
dirty_duplicate[&"dirty_cells"].append(dirty_duplicate[&"dirty_cells"][0])
_atomic_reject(restored, dirty_duplicate, before, "duplicate dirty cell")
var depth := snapshot.duplicate(true)
var deep_value: Variant = []
for _index: int in range(source.MAX_SNAPSHOT_DEPTH + 2):
deep_value = [deep_value]
depth[&"entities"] = deep_value
_atomic_reject(restored, depth, before, "excessive snapshot depth")
var entity_capacity := snapshot.duplicate(true)
var oversized_entities: Array = []
oversized_entities.resize(source.MAX_ENTITIES + 1)
entity_capacity[&"entities"] = oversized_entities
_atomic_reject(restored, entity_capacity, before, "entity collection capacity")
var internal_before := restored.create_snapshot()
restored._slots[Vector3i(99, 99, 0)] = {restored.CATEGORY_ROUTE: &"ghost"}
_expect(not restored.validate_invariants()[&"ok"], "live invariant audit detects an orphan cell-side slot")
_expect(restored.restore_snapshot(internal_before), "valid snapshot repairs audited private corruption")
func _test_deterministic_insertion_order() -> void:
var first := SpatialSubslotServiceType.new()
var second := SpatialSubslotServiceType.new()
first.place(&"z", first.CATEGORY_ROUTE, [Vector3i(2, 0, 1), Vector3i(-2, 0, 0)])
first.place(&"a", first.CATEGORY_ROUTE, [Vector3i(0, 1, 0)])
second.place(&"a", second.CATEGORY_ROUTE, [Vector3i(0, 1, 0)])
second.place(&"z", second.CATEGORY_ROUTE, [Vector3i(-2, 0, 0), Vector3i(2, 0, 1)])
_expect(first.create_snapshot() == second.create_snapshot(), "snapshot ignores Dictionary and input-cell insertion order")
_expect(first.get_route_cells() == second.get_route_cells(), "route projection order is deterministic")
func _atomic_reject(
service: SpatialSubslotService,
candidate: Dictionary,
before: Dictionary,
label: String
) -> void:
_expect(not service.restore_snapshot(candidate), "%s is rejected" % label)
_expect(service.create_snapshot() == before, "%s rejection is atomic" % label)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://dnk1clco7jj33
+199
View File
@@ -0,0 +1,199 @@
extends SceneTree
const WorldServiceType = preload("res://scripts/world/world_service.gd")
const DigestType = preload("res://scripts/core/simulation_digest.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: World Service")
_test_bounds_zones_and_footprints()
_test_occupancy_validation_and_release()
_test_walkability_excavation_and_revisions()
_test_connected_support_validation()
_test_snapshot_round_trip_and_atomic_restore()
_test_snapshot_order_is_deterministic()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_bounds_zones_and_footprints() -> void:
var world = WorldServiceType.new()
_expect(world.reset(_bounds(-2, 2, -2, 2)), "dictionary bounds are accepted")
_expect(world.is_in_bounds(Vector3i(-2, -2, 0)), "FRONT minimum corner is in bounds")
_expect(world.is_in_bounds(Vector3i(2, 2, 1)), "BACK maximum corner is in bounds")
_expect(not world.is_in_bounds(Vector3i(0, 0, 2)), "unknown layer is out of bounds")
_expect(not world.is_in_bounds(Vector3i(3, 0, 0)), "x outside bounds is rejected")
_expect(world.get_zone(Vector3i(0, 1, 0)) == world.ZONE_ABOVE_GROUND, "positive y is above ground")
_expect(world.get_zone(0) == world.ZONE_BOUNDARY, "zero y is boundary")
_expect(world.get_zone(Vector2i(0, -1)) == world.ZONE_UNDERGROUND, "negative y is underground")
var footprint := world.cells_for_footprint(Vector3i(-1, -1, 1), Vector2i(2, 2))
_expect(
footprint == [
Vector3i(-1, -1, 1), Vector3i(0, -1, 1),
Vector3i(-1, 0, 1), Vector3i(0, 0, 1),
],
"rectangular footprint has stable row-major ordering"
)
_expect(world.cells_for_footprint(Vector3i.ZERO, Vector2i.ZERO).is_empty(), "empty footprint is rejected")
var before := world.get_bounds()
_expect(not world.reset(Rect2i(0, 0, 0, 2)), "invalid reset is rejected")
_expect(world.get_bounds() == before, "failed reset preserves the current world")
func _test_occupancy_validation_and_release() -> void:
var world = WorldServiceType.new()
world.reset(_bounds(-3, 3, -2, 2))
var room_cells := world.cells_for_footprint(Vector3i(0, 0, 0), Vector2i(2, 1))
_expect(world.occupy(&"room-b", room_cells, &"facility_small"), "valid entity occupies its footprint")
_expect(world.get_occupant(Vector3i(0, 0, 0)) == &"room-b", "cell points back to occupant")
_expect(world.get_occupant_kind(Vector3i(1, 0, 0)) == &"facility_small", "occupant kind is retained")
_expect(world.get_entity_cells(&"room-b") == room_cells, "entity points to canonical cells")
var returned_cells := world.get_entity_cells(&"room-b")
returned_cells.clear()
_expect(world.get_entity_cells(&"room-b") == room_cells, "returned cell arrays cannot mutate world state")
var overlap := world.validate_occupancy([Vector3i(1, 0, 0)], &"room-a")
_expect(not overlap[&"ok"] and overlap[&"errors"].has(world.ERROR_OCCUPIED), "overlapping occupation reports OCCUPIED")
var revision_before_failure: int = world.revision
_expect(not world.occupy(&"room-a", [Vector3i(1, 0, 0)], &"facility"), "overlapping occupy is atomic")
_expect(world.revision == revision_before_failure, "failed occupy does not change revision")
_expect(not world.occupy(&"room-b", [Vector3i(2, 0, 0)], &"facility"), "same entity cannot occupy twice")
var malformed := world.validate_occupancy([
Vector3i(0, 1, 0),
Vector3i(0, 1, 0),
Vector3i(0, 1, 1),
Vector3i(9, 1, 0),
Vector2i.ZERO,
])
_expect(malformed[&"errors"].has(world.ERROR_DUPLICATE_CELL), "duplicate cell is reported")
_expect(malformed[&"errors"].has(world.ERROR_LAYER_MISMATCH), "layer crossing is reported")
_expect(malformed[&"errors"].has(world.ERROR_OUT_OF_BOUNDS), "out-of-bounds cell is reported")
_expect(malformed[&"errors"].has(world.ERROR_INVALID_CELL_TYPE), "non-Vector3i cell is reported")
_expect(world.release(&"room-b"), "release succeeds for existing entity")
_expect(world.get_occupant(Vector3i(0, 0, 0)).is_empty(), "release clears cell-side occupation")
_expect(world.get_entity_cells(&"room-b").is_empty(), "release clears entity-side occupation")
_expect(not world.release(&"room-b"), "double release is rejected")
func _test_walkability_excavation_and_revisions() -> void:
var world = WorldServiceType.new()
world.reset(_bounds(-1, 1, -2, 2))
var path_cell := Vector3i(0, 1, 0)
var cave_front := Vector3i(0, -1, 0)
var cave_back := Vector3i(0, -1, 1)
_expect(not world.is_walkable(path_cell), "walkability defaults to false")
_expect(world.set_walkable(path_cell, true), "walkability can be enabled")
_expect(world.is_walkable(path_cell), "enabled walkability is readable")
_expect(world.dirty_revision == world.revision, "dirty revision exposes the current world revision")
var walk_revision: int = world.revision
_expect(world.set_walkable(path_cell, true), "same walkability is a successful no-op")
_expect(world.revision == walk_revision, "walkability no-op does not dirty world")
_expect(world.is_excavated(Vector3i(0, 0, 0)), "boundary is usable without excavation")
_expect(not world.is_excavated(cave_front), "underground defaults to unexcavated")
_expect(world.set_excavated(cave_front, true), "underground spatial cell can be excavated")
_expect(world.is_excavated(cave_front) and world.is_excavated(cave_back), "excavation is shared by FRONT and BACK")
_expect(world.excavation_revision == world.revision, "excavation tracks its dirty revision")
_expect(world.get_dirty_cells().has(cave_front) and world.get_dirty_cells().has(cave_back), "both layers become dirty")
var dirty := world.consume_dirty_cells()
_expect(not dirty.is_empty() and world.get_dirty_cells().is_empty(), "dirty cells can be consumed deterministically")
_expect(not world.set_walkable(Vector3i(99, 0, 0), true), "out-of-bounds walkability mutation is rejected")
func _test_connected_support_validation() -> void:
var world = WorldServiceType.new()
world.reset(_bounds(-5, 5, 0, 2))
_expect(world.set_support(Vector3i(0, 0, 0), 3), "explicit support capacity is stored")
_expect(world.occupy(&"existing-room", [Vector3i(1, 0, 0)], &"facility_small"), "existing structural load is stored")
var supported := world.validate_support([Vector3i(2, 0, 0), Vector3i(3, 0, 0)], 1)
_expect(supported[&"ok"], "component capacity covers existing and proposed load")
_expect(supported[&"required_weight"] == 3, "support report includes existing structural load")
_expect(supported[&"available_capacity"] == 3, "support report exposes finite capacity")
var overloaded := world.validate_support([
Vector3i(2, 0, 0), Vector3i(3, 0, 0), Vector3i(4, 0, 0),
], 1)
_expect(not overloaded[&"ok"] and overloaded[&"errors"].has(world.ERROR_UNSUPPORTED), "overloaded component is rejected")
_expect(overloaded[&"unsupported_cells"].size() == 3, "all proposed cells in failed component are identified")
_expect(world.set_support(Vector3i(-2, 0, 1), 0, true), "infinite structural anchor is stored")
var anchored := world.validate_support([Vector3i(-1, 0, 1), Vector3i(0, 0, 1)], 99)
_expect(anchored[&"ok"] and anchored[&"available_capacity"] == -1, "connected anchor supports any finite weight")
_expect(not world.validate_support([Vector3i(0, 0, 0)], 0)[&"ok"], "non-positive proposed weight is rejected")
func _test_snapshot_round_trip_and_atomic_restore() -> void:
var source = WorldServiceType.new()
source.reset(_bounds(-3, 3, -3, 3))
source.occupy(&"warehouse", [Vector3i(1, 0, 1), Vector3i(0, 0, 1)], &"facility_medium")
source.set_walkable(Vector3i(-1, 0, 0), true)
source.set_excavated(Vector3i(2, -2, 0), true)
source.set_excavated(Vector3i(-2, 1, 0), false)
source.set_support(Vector3i(0, 0, 0), 5, false)
source.set_support(Vector3i(-1, 0, 1), 0, true)
var snapshot := source.create_snapshot()
var restored = WorldServiceType.new()
restored.reset(Rect2i(0, 0, 1, 1))
_expect(restored.restore_snapshot(snapshot), "valid world snapshot restores")
_expect(restored.create_snapshot() == snapshot, "world snapshot round trip is exact")
_expect(DigestType.compute(restored.create_snapshot()) == DigestType.compute(snapshot), "round trip preserves deterministic digest")
var corrupt: Dictionary = snapshot.duplicate(true)
corrupt[&"occupancies"].append({
&"entity_id": &"overlap",
&"kind": &"facility",
&"cells": [Vector3i(0, 0, 1)],
})
var before_failed_restore := restored.create_snapshot()
_expect(not restored.restore_snapshot(corrupt), "overlapping snapshot is rejected")
_expect(restored.create_snapshot() == before_failed_restore, "failed restore is fully atomic")
func _test_snapshot_order_is_deterministic() -> void:
var first = WorldServiceType.new()
var second = WorldServiceType.new()
first.reset(_bounds(-2, 2, -1, 1))
second.reset(_bounds(-2, 2, -1, 1))
first.occupy(&"z-entity", [Vector3i(1, 0, 0)], &"decoration")
first.occupy(&"a-entity", [Vector3i(-1, 0, 0)], &"decoration")
second.occupy(&"a-entity", [Vector3i(-1, 0, 0)], &"decoration")
second.occupy(&"z-entity", [Vector3i(1, 0, 0)], &"decoration")
first.set_walkable(Vector3i(1, 1, 1), true)
first.set_walkable(Vector3i(-1, -1, 0), true)
second.set_walkable(Vector3i(-1, -1, 0), true)
second.set_walkable(Vector3i(1, 1, 1), true)
_expect(first.create_snapshot() == second.create_snapshot(), "snapshot ignores Dictionary insertion order")
_expect(DigestType.compute(first.create_snapshot()) == DigestType.compute(second.create_snapshot()), "equivalent worlds have identical digest")
func _bounds(min_x: int, max_x: int, min_y: int, max_y: int) -> Dictionary:
return {
&"min_x": min_x,
&"max_x": max_x,
&"min_y": min_y,
&"max_y": max_y,
}
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://nj6v4aby5clu
+192
View File
@@ -0,0 +1,192 @@
extends SceneTree
const LedgerEntryType = preload("res://scripts/economy/ledger_entry.gd")
const EconomyServiceType = preload("res://scripts/economy/economy_service.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: Economy Service")
_test_initialization_and_insufficient_balance()
_test_idempotency_posts_once()
_test_reverse_and_partial_refund()
_test_compound_post_is_atomic()
_test_daily_aggregation_and_reconciliation()
_test_snapshot_round_trip_is_atomic()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_initialization_and_insufficient_balance() -> void:
var economy := _economy()
var initialized = economy.initialize_account(&"PLAYER_CASH", 1_000, 360, &"init-player")
_expect(initialized.is_success(), "initial Gold is posted through SYSTEM_SOURCE")
_expect(economy.get_balance(&"PLAYER_CASH") == 1_000, "initialization debits player cash")
_expect(economy.get_balance(EconomyServiceType.SYSTEM_SOURCE) == -1_000, "SYSTEM_SOURCE is the explicit counter-account")
var entry = economy.get_transaction_entries(initialized.details[&"transaction_id"])[0]
_expect(entry.amount == 1_000 and entry.category == &"INITIALIZATION", "initialization creates a positive integer ledger entry")
var before := economy.create_snapshot()
var rejected = economy.transfer(
&"PLAYER_CASH", &"GUEST_WALLET:guest_1", 1_001, 361, &"TEST", &"", &"too-much"
)
_expect(not rejected.is_success(), "an account without credit cannot overspend")
_expect(rejected.code == CommandResultType.Code.VALIDATION_FAILED, "insufficient Gold is a validation failure")
_expect(economy.create_snapshot() == before, "rejected transfer changes neither ledger nor balances")
_expect(not economy.register_account(&"PLAYER_CASH"), "an account definition cannot be replaced to bypass its policy")
func _test_idempotency_posts_once() -> void:
var economy := _economy()
economy.initialize_account(&"PLAYER_CASH", 500, 360, &"init")
var first = economy.transfer(
&"PLAYER_CASH", &"GUEST_WALLET:guest_1", 125, 400, &"GIFT", &"player", &"gift-once"
)
var entry_count := economy.get_entries().size()
var duplicate = economy.transfer(
&"PLAYER_CASH", &"GUEST_WALLET:guest_1", 125, 400, &"GIFT", &"player", &"gift-once"
)
_expect(first.is_success(), "first idempotent command posts")
_expect(duplicate.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "duplicate idempotency key is rejected explicitly")
_expect(economy.get_entries().size() == entry_count, "duplicate command appends no second entry")
_expect(economy.get_balance(&"PLAYER_CASH") == 375, "sender is credited exactly once")
_expect(economy.get_balance(&"GUEST_WALLET:guest_1") == 125, "receiver is debited exactly once")
_expect(
economy.get_transaction_id_for_idempotency_key(&"gift-once") == first.details[&"transaction_id"],
"idempotency index resolves the original transaction"
)
func _test_reverse_and_partial_refund() -> void:
var economy := _economy()
economy.initialize_account(&"GUEST_WALLET:guest_1", 500, 360, &"init-guest")
var payment = economy.transfer(
&"GUEST_WALLET:guest_1", &"FACILITY_REVENUE:inn", 200, 500, &"SERVICE_SALE", &"inn", &"sale-1"
)
var payment_id: StringName = payment.details[&"transaction_id"]
var reversed = economy.reverse(payment_id, 510, &"reverse-sale")
_expect(reversed.is_success(), "an existing transaction reverses with a new entry")
_expect(economy.get_balance(&"GUEST_WALLET:guest_1") == 500, "reversal restores the payer balance")
_expect(economy.get_balance(&"FACILITY_REVENUE:inn") == 0, "reversal restores the receiver balance")
var reverse_entry = economy.get_transaction_entries(reversed.details[&"transaction_id"])[0]
_expect(reverse_entry.related_transaction_id == payment_id, "reversal retains the original transaction reference")
_expect(reverse_entry.debit_account == &"GUEST_WALLET:guest_1" and reverse_entry.credit_account == &"FACILITY_REVENUE:inn", "reversal swaps debit and credit")
_expect(not economy.reverse(payment_id, 511, &"reverse-again").is_success(), "a transaction cannot be fully reversed twice")
var second_payment = economy.transfer(
&"GUEST_WALLET:guest_1", &"FACILITY_REVENUE:inn", 120, 520, &"SERVICE_SALE", &"inn", &"sale-2"
)
var second_id: StringName = second_payment.details[&"transaction_id"]
_expect(economy.refund(second_id, 40, 530, &"refund-1").is_success(), "a one-line transaction supports a partial refund")
_expect(economy.refund(second_id, 80, 540, &"refund-2").is_success(), "remaining refundable Gold can be returned")
_expect(economy.get_refunded_amount(second_id) == 120, "refund total is derived from related entries")
_expect(not economy.refund(second_id, 1, 541, &"refund-over").is_success(), "refunds cannot exceed the source transaction")
func _test_compound_post_is_atomic() -> void:
var economy := _economy()
economy.initialize_account(&"PLAYER_CASH", 400, 360, &"init-player")
economy.initialize_account(&"GUEST_WALLET:guest_1", 50, 360, &"init-guest")
var before := economy.create_snapshot()
var entries: Array = [
_entry(600, &"STAFF_WALLET:staff_1", &"PLAYER_CASH", 100, &"PAYROLL"),
_entry(600, &"STAFF_WALLET:staff_1", &"GUEST_WALLET:guest_1", 51, &"PAYROLL"),
]
var rejected = economy.post(entries, &"compound-fail")
_expect(not rejected.is_success(), "one invalid line rejects a compound transaction")
_expect(economy.create_snapshot() == before, "compound failure rolls back every line and index")
entries[1].amount = 50
var posted = economy.post(entries, &"compound-ok")
_expect(posted.is_success(), "a fully valid compound transaction posts")
_expect(economy.get_transaction_entries(posted.details[&"transaction_id"]).size() == 2, "compound lines share one transaction ID")
_expect(economy.get_balance(&"STAFF_WALLET:staff_1") == 150, "compound debit deltas commit together")
_expect(economy.get_balance(&"PLAYER_CASH") == 300 and economy.get_balance(&"GUEST_WALLET:guest_1") == 0, "compound credits commit together")
func _test_daily_aggregation_and_reconciliation() -> void:
var economy := _economy()
economy.initialize_account(&"PLAYER_CASH", 1_000, 360, &"init")
economy.transfer(&"PLAYER_CASH", &"GUEST_WALLET:guest_1", 75, 700, &"GIFT", &"player", &"gift")
economy.transfer(&"PLAYER_CASH", &"STAFF_WALLET:staff_1", 125, 1_500, &"WAGE", &"staff_1", &"wage")
var day_one := economy.get_daily_summary(1)
_expect(day_one[&"transaction_count"] == 2 and day_one[&"balanced"], "day close counts only transactions inside its minute range")
_expect(day_one[&"category_totals"][&"INITIALIZATION"] == 1_000, "day close aggregates category amounts")
_expect(day_one[&"account_totals"][&"PLAYER_CASH"][&"net"] == 925, "day close aggregates per-account debit, credit and net")
_expect(economy.validate_ledger()[&"ok"], "ledger and derived balance cache reconcile")
economy._balance_cache[&"PLAYER_CASH"] = 999_999
var mismatch := economy.validate_ledger()
_expect(not mismatch[&"ok"] and mismatch[&"errors"][-1][&"code"] == &"BALANCE_CACHE_MISMATCH", "reconciliation detects a damaged balance cache")
_expect(economy.rebuild_balance_cache(), "balance cache can be rebuilt only from valid ledger entries")
_expect(economy.get_balance(&"PLAYER_CASH") == 800, "rebuild never invents a compensating ledger entry")
_expect(economy.validate_ledger()[&"ok"], "rebuilt balance cache reconciles exactly")
func _test_snapshot_round_trip_is_atomic() -> void:
var source := _economy()
source.set_current_minute(777)
source.initialize_account(&"PLAYER_CASH", 900, 360, &"init")
source.transfer(&"PLAYER_CASH", &"GUEST_WALLET:guest_1", 120, 650, &"GIFT", &"player", &"gift")
var snapshot := source.create_snapshot()
var restored := EconomyServiceType.new()
_expect(restored.restore_snapshot(snapshot), "valid economy snapshot restores")
_expect(restored.create_snapshot() == snapshot, "economy snapshot round trip is exact")
_expect(restored.validate_ledger()[&"ok"], "restored ledger and balances reconcile")
var before_invalid := restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"entries"][0][&"amount"] = 0
_expect(not restored.restore_snapshot(corrupt), "snapshot containing a zero-amount entry is rejected")
_expect(restored.create_snapshot() == before_invalid, "failed snapshot restore is fully atomic")
var duplicate_account := snapshot.duplicate(true)
duplicate_account[&"accounts"].append(duplicate_account[&"accounts"][0].duplicate(true))
_expect(not restored.restore_snapshot(duplicate_account), "snapshot with duplicate account definitions is rejected")
_expect(restored.create_snapshot() == before_invalid, "second rejected restore also preserves live state")
func _economy() -> EconomyService:
return EconomyServiceType.new({
&"PLAYER_CASH": {&"allow_negative": false},
&"GUEST_WALLET:guest_1": {&"allow_negative": false},
&"FACILITY_REVENUE:inn": {&"allow_negative": false},
&"STAFF_WALLET:staff_1": {&"allow_negative": false},
&"STAFF_PAYABLE:staff_1": {&"allow_negative": true},
})
func _entry(
absolute_minute: int,
debit_account: StringName,
credit_account: StringName,
amount: int,
category: StringName
) -> LedgerEntry:
return LedgerEntryType.new().configure(
&"",
absolute_minute,
debit_account,
credit_account,
amount,
category
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://dskbexctiolv5
+312
View File
@@ -0,0 +1,312 @@
extends SceneTree
const FacilityInstanceType = preload("res://scripts/facility/facility_instance.gd")
const ServiceInstanceType = preload("res://scripts/facility/service_instance.gd")
const FacilityServiceType = preload("res://scripts/facility/facility_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: Facility Service")
_test_state_priority_and_opening_hours()
_test_policy_revision_and_request_snapshot()
_test_deterministic_queue_and_departure()
_test_capacity_progress_and_boundaries()
_test_interruption_and_recovery()
_test_determinism()
_test_snapshot_round_trip_is_atomic()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_state_priority_and_opening_hours() -> void:
var facility := _facility(&"priority_room", 1, 4, 1, 2)
facility.set_operational_conditions({&"active_staff_ids": []}, 0)
_expect(facility.state == FacilityInstanceType.State.NO_STAFF, "staff shortage initially blocks opening")
facility.set_operational_conditions({
&"durability": 0,
&"access_available": false,
&"power_available": false,
&"operator_open": false,
&"active_staff_ids": [],
&"degraded_reasons": [&"DIRTY"],
}, 600)
_expect(facility.state == FacilityInstanceType.State.BROKEN, "BROKEN wins over every other state")
_expect(facility.state_reasons.size() == 6, "all simultaneous state reasons are retained")
facility.set_operational_conditions({
&"durability": 5_000,
&"access_available": false,
&"power_available": false,
&"operator_open": false,
&"active_staff_ids": [],
&"degraded_reasons": [],
}, 600)
_expect(facility.state == FacilityInstanceType.State.NO_ACCESS, "NO_ACCESS wins after repair")
facility.set_operational_conditions({&"access_available": true}, 600)
_expect(facility.state == FacilityInstanceType.State.NO_POWER, "NO_POWER wins over staffing and closure")
facility.set_operational_conditions({&"power_available": true}, 600)
_expect(facility.state == FacilityInstanceType.State.NO_STAFF, "NO_STAFF wins over manual closure")
facility.set_operational_conditions({&"active_staff_ids": [&"staff_a"]}, 600)
_expect(facility.state == FacilityInstanceType.State.CLOSED, "manual closure follows operational failures")
facility.set_operational_conditions({&"operator_open": true, &"durability": 2_999}, 600)
_expect(facility.state == FacilityInstanceType.State.DEGRADED, "low durability produces DEGRADED")
facility.set_operational_conditions({&"durability": 5_000}, 600)
_expect(facility.state == FacilityInstanceType.State.OPEN, "healthy staffed facility opens")
_expect(facility.set_opening_hours(600, 1_200), "valid opening hours are accepted")
facility.evaluate_state(500)
_expect(facility.state == FacilityInstanceType.State.CLOSED, "schedule closes facility before opening")
facility.evaluate_state(600)
_expect(facility.state == FacilityInstanceType.State.OPEN, "opening minute is inclusive")
facility.evaluate_state(1_200)
_expect(facility.state == FacilityInstanceType.State.CLOSED, "closing minute is exclusive")
_expect(facility.set_opening_hours(1_200, 0), "midnight close creates a valid overnight interval")
_expect(facility.is_within_opening_hours(1_300), "midnight-close schedule remains open late at night")
func _test_policy_revision_and_request_snapshot() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"policy_room", 1, 3, 0, 1)
_expect(service.register_instance(facility).is_success(), "facility instance registers")
_expect(not service.register_instance(facility).is_success(), "duplicate facility instance ID is rejected")
var changed = service.set_policy(facility.instance_id, {&"price_mode": &"standard"})
_expect(changed.is_success() and facility.policy_revision == 1, "immediate policy patch increments revision")
var request := service.create_request(
&"policy_request",
facility.instance_id,
[&"guest_a"],
&"meal",
100,
{&"quoted_price": 120}
)
_expect(service.enqueue_request(request, 100).is_success(), "policy-quoted request enters queue")
service.set_policy(facility.instance_id, {&"price_mode": &"premium"})
_expect(request.policy_snapshot[&"price_mode"] == &"standard", "queued request retains original policy snapshot")
_expect(request.policy_revision == 1 and facility.policy_revision == 2, "request retains original policy revision")
var scheduled = service.set_policy(facility.instance_id, {&"risk_mode": true}, 120)
_expect(scheduled.is_success() and facility.policy_pending_revision == 3, "future policy receives pending revision")
service.advance_one_minute(119)
_expect(not facility.policy_current.has(&"risk_mode"), "pending policy does not apply early")
service.advance_one_minute(120)
_expect(facility.policy_current.get(&"risk_mode", false), "pending policy applies at exact minute")
_expect(facility.policy_revision == 3, "scheduled application increments current revision once")
func _test_deterministic_queue_and_departure() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"queue_room", 1, 3, 0, 2)
service.register_instance(facility)
var request_b := _request(service, facility, &"request_b", 10, 0, 2)
var request_a := _request(service, facility, &"request_a", 10, 0, 2)
var request_high := _request(service, facility, &"request_high", 11, 5, 2)
_expect(service.enqueue_request(request_b, 10).is_success(), "first request enters queue")
_expect(service.enqueue_request(request_a, 10).is_success(), "same-minute request enters queue")
_expect(service.enqueue_request(request_high, 11).is_success(), "later high-priority request enters queue")
_expect(
_queue_ids(service, facility.instance_id) == [&"request_high", &"request_a", &"request_b"],
"queue sorts by priority, arrival minute, then request ID"
)
_expect(request_high.expected_wait == 0, "high-priority request wait estimate excludes requests behind it")
_expect(not service.enqueue_request(request_a, 12).is_success(), "duplicate request ID is rejected permanently")
var overflow := _request(service, facility, &"request_overflow", 12, 0, 2)
_expect(not service.enqueue_request(overflow, 12).is_success(), "queue capacity rejects overflow")
_expect(service.leave_queue(facility.instance_id, &"request_a").is_success(), "guest can leave queue")
_expect(_queue_ids(service, facility.instance_id) == [&"request_high", &"request_b"], "queue departure removes entry immediately")
_expect(not service.enqueue_request(request_a, 13).is_success(), "departed request ID cannot be reused")
func _test_capacity_progress_and_boundaries() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"capacity_room", 2, 4, 0, 2)
service.register_instance(facility)
var starts: Array[StringName] = []
var completions: Array[StringName] = []
service.configure_callbacks(
func(request: ServiceRequest, _facility_instance: FacilityInstance, _minute: int) -> bool:
starts.append(request.request_id)
return true,
func(service_instance: ServiceInstance, _request_value: ServiceRequest, _facility_instance: FacilityInstance, _minute: int) -> bool:
completions.append(service_instance.service_id)
return true
)
for request_id: StringName in [&"request_c", &"request_a", &"request_b"]:
service.enqueue_request(_request(service, facility, request_id, 100, 0, 2), 100)
var filled := service.fill_available_slots(facility.instance_id, 100)
_expect(filled.size() == 2 and filled[0].is_success() and filled[1].is_success(), "capacity opens exactly two slots")
_expect(starts == [&"request_a", &"request_b"], "slot starts follow deterministic request-ID tie break")
_expect(facility.active_services.size() == 2, "two services occupy both slots")
_expect(not service.start_next_service(facility.instance_id, 100).is_success(), "full facility cannot start a third service")
service.advance_one_minute(101)
_expect(completions.is_empty(), "two-minute service does not complete after one tick")
service.advance_one_minute(102)
_expect(completions.size() == 2, "both occupied slots complete independently")
_expect(facility.active_services.size() == 1, "completion fills one freed slot from queue")
_expect(starts == [&"request_a", &"request_b", &"request_c"], "next queued request is promoted after completion")
var rejecting := FacilityServiceType.new()
var rejection_room := _facility(&"rejection_room", 1, 2, 0, 1)
rejecting.register_instance(rejection_room)
rejecting.configure_callbacks(
func(_request_value: ServiceRequest, _instance: FacilityInstance, _minute: int) -> bool:
return true,
func(_service_value: ServiceInstance, _request_value: ServiceRequest, _instance: FacilityInstance, _minute: int) -> String:
return "ledger transaction rejected"
)
rejecting.enqueue_request(_request(rejecting, rejection_room, &"atomic_request", 0, 0, 1), 0)
var started = rejecting.start_next_service(rejection_room.instance_id, 0)
var service_id: StringName = started.details[&"service_id"]
var results := rejecting.advance_one_minute(1)
_expect(results.any(func(result: CommandResult) -> bool: return not result.is_success()), "completion callback failure is surfaced")
_expect(rejection_room.active_services.has(service_id), "failed completion transaction leaves service active")
_expect(rejecting.get_request(&"atomic_request") != null, "failed completion transaction retains request")
func _test_interruption_and_recovery() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"interrupt_room", 1, 2, 0, 3)
service.register_instance(facility)
service.enqueue_request(_request(service, facility, &"interrupt_request", 0, 0, 3), 0)
var started = service.start_next_service(facility.instance_id, 0)
var service_id: StringName = started.details[&"service_id"]
service.set_operational_status(facility.instance_id, {&"power_available": false}, 1)
service.advance_one_minute(1)
var active := service.get_service(service_id)
_expect(active.state == ServiceInstanceType.State.PAUSED, "power loss safely pauses active service")
_expect(active.elapsed_minutes == 0, "paused service makes no progress")
service.set_operational_status(facility.instance_id, {&"power_available": true}, 2)
service.advance_one_minute(2)
_expect(active.state == ServiceInstanceType.State.ACTIVE and active.elapsed_minutes == 1, "power restoration resumes service")
service.interrupt_service(service_id, FacilityServiceType.INTERRUPTION_PLAYER_CLOSED, 2)
_expect(active.state == ServiceInstanceType.State.ACTIVE, "normal player closure lets active service finish")
service.set_operational_status(facility.instance_id, {&"durability": 0}, 3)
service.advance_one_minute(3)
_expect(active.state == ServiceInstanceType.State.FAILED, "facility breakage immediately fails active service")
_expect(facility.active_services.is_empty(), "failed service frees its capacity slot")
var grace_service := FacilityServiceType.new()
var grace_room := _facility(&"grace_room", 1, 1, 0, 3)
grace_service.register_instance(grace_room)
grace_service.enqueue_request(_request(grace_service, grace_room, &"grace_request", 10, 0, 3), 10)
var grace_started = grace_service.start_next_service(grace_room.instance_id, 10)
var grace_id: StringName = grace_started.details[&"service_id"]
grace_service.interrupt_service(grace_id, FacilityServiceType.INTERRUPTION_STAFF_UNAVAILABLE, 10)
grace_service.advance_one_minute(14)
_expect(grace_service.get_service(grace_id).state == ServiceInstanceType.State.WAITING_FOR_STAFF, "staff replacement waits through minute four")
grace_service.advance_one_minute(15)
_expect(grace_service.get_service(grace_id).state == ServiceInstanceType.State.FAILED, "staff replacement fails at five-minute deadline")
func _test_determinism() -> void:
var first := _deterministic_run()
var second := _deterministic_run()
_expect(first == second, "identical commands produce byte-equivalent facility snapshots")
func _test_snapshot_round_trip_is_atomic() -> void:
var service := FacilityServiceType.new()
var facility := _facility(&"save_room", 1, 3, 0, 4)
service.register_instance(facility)
service.set_policy(facility.instance_id, {&"mode": &"quiet"})
service.set_policy(facility.instance_id, {&"mode": &"busy"}, 500)
service.enqueue_request(_request(service, facility, &"active_request", 400, 2, 4), 400)
service.enqueue_request(_request(service, facility, &"queued_request", 401, 0, 4), 401)
service.start_next_service(facility.instance_id, 401)
service.advance_one_minute(402)
var snapshot := service.create_snapshot()
var restored := FacilityServiceType.new()
_expect(restored.restore_snapshot(snapshot), "valid facility snapshot restores")
_expect(restored.create_snapshot() == snapshot, "snapshot round trip preserves full facility state")
var before_invalid := restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"requests"].append(corrupt[&"requests"][0].duplicate(true))
_expect(not restored.restore_snapshot(corrupt), "duplicate request in snapshot is rejected")
_expect(restored.create_snapshot() == before_invalid, "rejected restore leaves live state untouched")
var corrupt_reference := snapshot.duplicate(true)
corrupt_reference[&"instances"][0][&"queue_entries"] = ["missing_request"]
_expect(not restored.restore_snapshot(corrupt_reference), "orphan queue reference is rejected")
_expect(restored.create_snapshot() == before_invalid, "orphan rejection is also atomic")
func _deterministic_run() -> Dictionary:
var service := FacilityServiceType.new()
var facility := _facility(&"deterministic_room", 2, 6, 0, 2)
service.register_instance(facility)
for request_data: Array in [
[&"request_d", 0],
[&"request_b", 1],
[&"request_c", 1],
[&"request_a", 0],
]:
var request_id: StringName = request_data[0]
var priority: int = request_data[1]
service.enqueue_request(_request(service, facility, request_id, 50, priority, 2), 50)
service.fill_available_slots(facility.instance_id, 50)
service.advance_one_minute(51)
service.advance_one_minute(52)
service.advance_one_minute(53)
return service.create_snapshot()
func _facility(
instance_id: StringName,
capacity: int,
queue_capacity: int,
staff_min: int,
service_minutes: int
) -> FacilityInstance:
return FacilityInstanceType.new().configure(
instance_id,
&"test_facility",
{
&"capacity": capacity,
&"queue_capacity": queue_capacity,
&"staff_min": staff_min,
&"staff_max": maxi(staff_min, 2),
&"active_staff_ids": [&"staff_a"] if staff_min > 0 else [],
&"base_service_minutes": service_minutes,
&"base_quality": 75,
&"base_price": 100,
}
)
func _request(
service: FacilityService,
facility: FacilityInstance,
request_id: StringName,
created_at: int,
priority: int,
service_minutes: int
) -> ServiceRequest:
return service.create_request(
request_id,
facility.instance_id,
[StringName("guest_%s" % request_id)],
&"test_service",
created_at,
{&"priority": priority, &"service_minutes": service_minutes}
)
func _queue_ids(service: FacilityService, instance_id: StringName) -> Array[StringName]:
var ids: Array[StringName] = []
for request: ServiceRequest in service.get_queue(instance_id):
ids.append(request.request_id)
return ids
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://bgrxu5w7jm0an
+591
View File
@@ -0,0 +1,591 @@
extends SceneTree
const InventoryServiceScript = preload("res://scripts/inventory/inventory_service.gd")
const LogisticsRequestScript = preload("res://scripts/inventory/logistics_request.gd")
const LogisticsDispatcherScript = preload("res://scripts/inventory/logistics_dispatcher.gd")
const InventoryReservationScript = preload("res://scripts/inventory/inventory_reservation.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: Inventory and Logistics")
_test_reservation_limits_and_idempotency()
_test_transfer_never_duplicates_stock()
_test_expiration_rules()
_test_orphan_repair_report()
_test_dispatch_priority_capacity_and_tie_break()
_test_dispatch_missing_carrier_preserves_reserved_load()
_test_atomic_transfer_batches_match_sequential_operations()
_test_idempotency_conflicts_are_preflighted()
_test_request_transitions()
_test_snapshot_round_trip_and_atomic_restore()
_test_strict_snapshot_migration_and_id_tombstones()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_reservation_limits_and_idempotency() -> void:
var service = InventoryServiceScript.new()
var added: Dictionary = service.add_stack(&"warehouse", &"meal", 10, 90, -1, 0, 2)
_expect(added[&"ok"], "stock can be added to an owner inventory")
var first: Dictionary = service.reserve_item(
&"warehouse", &"meal", 7, &"cafeteria", &"service", 10, 60, &"reserve-a"
)
_expect(first[&"ok"] and service.available_quantity(&"warehouse", &"meal", 10) == 3, "reservation reduces available but not physical quantity")
var replay: Dictionary = service.reserve_item(
&"warehouse", &"meal", 7, &"cafeteria", &"service", 10, 60, &"reserve-a"
)
_expect(replay[&"ok"] and replay[&"replay"] and service.get_reservations(true).size() == 1, "reservation operation id replays without creating stock claims")
var rejected: Dictionary = service.reserve_item(
&"warehouse", &"meal", 4, &"kitchen", &"service", 10, 60, &"reserve-b"
)
_expect(not rejected[&"ok"] and rejected[&"code"] == &"INSUFFICIENT_AVAILABLE", "over-reservation is rejected atomically")
var reservation_id: StringName = first[&"reservation_ids"][0]
var consumed: Dictionary = service.consume_reservation(reservation_id, &"consume-a", 20)
var quantity_after := service.total_quantity(&"warehouse", &"meal")
var consumed_again: Dictionary = service.consume_reservation(reservation_id, &"consume-b", 21)
_expect(consumed[&"ok"] and quantity_after == 3, "consumption removes exactly the reserved physical stock")
_expect(consumed_again[&"ok"] and consumed_again[&"replay"] and service.total_quantity(&"warehouse", &"meal") == quantity_after, "a consumed reservation cannot consume twice")
_expect(service.validate_invariants()[&"ok"], "reserve and consume preserve quantity minus reserved invariant")
func _test_transfer_never_duplicates_stock() -> void:
var service = InventoryServiceScript.new()
service.add_stack(&"source", &"material", 12, 75, 500, 3, 3)
var before := service.total_quantity(&"source", &"material") + service.total_quantity(&"destination", &"material")
var moved: Dictionary = service.transfer_available(
&"source", &"destination", &"material", 5, &"transfer-a", 20
)
var after := service.total_quantity(&"source", &"material") + service.total_quantity(&"destination", &"material")
_expect(moved[&"ok"] and before == after and after == 12, "owner transfer conserves global item quantity")
_expect(service.total_quantity(&"source", &"material") == 7 and service.total_quantity(&"destination", &"material") == 5, "transfer subtracts source before adding destination")
var replay: Dictionary = service.transfer_available(
&"source", &"destination", &"material", 5, &"transfer-a", 20
)
_expect(replay[&"replay"] and service.total_quantity(&"destination", &"material") == 5, "transfer replay cannot duplicate destination stock")
var reserved: Dictionary = service.reserve_item(
&"source", &"material", 4, &"delivery-1", &"logistics", 21, 100, &"pickup-reserve"
)
var pickup: Dictionary = service.transfer_reserved(
reserved[&"reservation_ids"], &"carrier-cargo", &"pickup-a", 22
)
_expect(pickup[&"ok"] and pickup[&"weight"] == 12, "reserved pickup transfers the exact reserved weight")
_expect(service.total_quantity(&"source", &"material") == 3 and service.total_quantity(&"carrier-cargo", &"material") == 4, "reserved pickup consumes reservation and creates cargo once")
_expect(service.validate_invariants()[&"ok"], "available and reserved remain valid after transfer")
func _test_expiration_rules() -> void:
var service = InventoryServiceScript.new()
var added: Dictionary = service.add_stack(&"cold-store", &"food", 8, 80, 30, 0, 1)
var reservation: Dictionary = service.reserve_item(
&"cold-store", &"food", 3, &"service-a", &"meal", 10, 50, &"expiry-reserve"
)
_expect(not service.is_stack_expired(added[&"stack_id"], 29), "stack remains valid before expires_at")
_expect(service.is_stack_expired(added[&"stack_id"], 30), "stack expires exactly at expires_at")
var report: Dictionary = service.process_expired(30)
_expect(report[&"discarded_quantity"] == 5 and service.total_quantity(&"cold-store", &"food") == 3, "expiration discards only unreserved quantity")
var consume: Dictionary = service.consume_reservation(reservation[&"reservation_ids"][0], &"expired-consume", 30)
_expect(not consume[&"ok"] and consume[&"code"] == &"STACK_EXPIRED", "reserved stack is checked again before consumption")
service.process_expired(50)
_expect(service.total_quantity(&"cold-store", &"food") == 0, "reservation expiry releases and discards remaining expired stock")
func _test_orphan_repair_report() -> void:
var service = InventoryServiceScript.new()
var added: Dictionary = service.add_stack(&"store", &"part", 5)
var reservation: Dictionary = service.reserve_item(
&"store", &"part", 2, &"repair-job", &"build", 0, -1, &"orphan-reserve"
)
var stacks: Dictionary = service._inventories[&"store"]
stacks.erase(added[&"stack_id"])
service._inventories[&"store"] = stacks
var report: Dictionary = service.audit_and_repair_reservations(5)
var repaired_reservation = service.get_reservation(reservation[&"reservation_ids"][0])
_expect(report[&"orphan_count"] == 1 and report[&"released_reservation_ids"].size() == 1, "orphan repair returns a structured repair report")
_expect(repaired_reservation.state == InventoryReservationScript.State.RELEASED, "orphan repair closes the dangling reservation")
func _test_dispatch_priority_capacity_and_tie_break() -> void:
var high = _reserved_request(&"request-high", 90, 6, 300)
var low = _reserved_request(&"request-low", 20, 6, 400)
var dispatcher = LogisticsDispatcherScript.new()
var carriers := {
&"carrier-b": {
&"position": Vector3i(1, 0, 1),
&"capacity": 10,
&"current_load": 0,
&"role": &"logistics",
&"path_minutes": {&"default": 5},
},
&"carrier-a": {
&"position": Vector3i(0, 0, 1),
&"capacity": 10,
&"current_load": 0,
&"role": &"logistics",
&"path_minutes": {&"default": 5},
},
}
var assignments: Array[Dictionary] = dispatcher.dispatch([low, high], carriers, 100)
_expect(assignments.size() == 2 and assignments[0][&"request_id"] == &"request-high", "dispatcher evaluates requests in deterministic priority order")
_expect(assignments[0][&"carrier_id"] == &"carrier-a", "equal scores use lexical carrier id as tie break")
_expect(assignments[1][&"carrier_id"] == &"carrier-b", "reserved load prevents assigning beyond one carrier capacity")
_expect(dispatcher.get_reserved_load(&"carrier-a") == 6 and dispatcher.get_reserved_load(&"carrier-b") == 6, "dispatcher records capacity reservations for later passes")
var reordered_dispatcher = LogisticsDispatcherScript.new()
var reordered_carriers := {
&"carrier-a": carriers[&"carrier-a"],
&"carrier-b": carriers[&"carrier-b"],
}
var reordered_assignments: Array[Dictionary] = reordered_dispatcher.dispatch([
_reserved_request(&"request-high", 90, 6, 300),
_reserved_request(&"request-low", 20, 6, 400),
], reordered_carriers, 100)
_expect(
reordered_assignments == assignments,
"one-pass carrier ordering remains deterministic across request and Dictionary insertion order"
)
func _test_dispatch_missing_carrier_preserves_reserved_load() -> void:
var dispatcher = LogisticsDispatcherScript.new()
var carrier := {
&"carrier-a": {
&"position": Vector3i.ZERO,
&"capacity": 10,
&"current_load": 0,
&"role": &"logistics",
&"path_minutes": 1,
},
}
var first = _reserved_request(&"request-carrier-present", 90, 6, 300)
_expect(
dispatcher.dispatch([first], carrier, 100).size() == 1,
"carrier accepts an assignment within capacity"
)
var second = _reserved_request(&"request-carrier-absent", 80, 6, 310)
_expect(
dispatcher.dispatch([second], {}, 101).is_empty()
and dispatcher.get_reserved_load(&"carrier-a") == 6,
"temporarily absent carrier keeps its accepted reserved load"
)
var absent_snapshot: Dictionary = dispatcher.create_snapshot()
var restored = LogisticsDispatcherScript.new()
_expect(
restored.restore_snapshot(absent_snapshot)
and restored.create_snapshot() == absent_snapshot,
"missing-carrier reservation snapshot restores exactly"
)
_expect(
restored.dispatch([second], carrier, 102).is_empty()
and restored.get_reserved_load(&"carrier-a") == 6,
"reappearing carrier cannot accept another six units into capacity ten"
)
_expect(
restored.release_assignment(&"carrier-a", 6) == 0
and restored.create_snapshot()[&"carrier_loads"].is_empty(),
"explicit assignment release removes the retained carrier load"
)
func _test_atomic_transfer_batches_match_sequential_operations() -> void:
var seed = InventoryServiceScript.new()
seed.add_stack(&"warehouse", &"supply", 4, 87, 600, 3, 2, &"batch-source")
var reservation_a: Dictionary = seed.reserve_item(
&"warehouse", &"supply", 2, &"request-a", &"delivery", 10, 100, &"batch-reserve-a"
)
var reservation_b: Dictionary = seed.reserve_item(
&"warehouse", &"supply", 2, &"request-b", &"delivery", 10, 100, &"batch-reserve-b"
)
var ids_a: Array[StringName] = []
var ids_b: Array[StringName] = []
ids_a.assign(reservation_a[&"reservation_ids"])
ids_b.assign(reservation_b[&"reservation_ids"])
var sequential = InventoryServiceScript.new()
var batched = InventoryServiceScript.new()
sequential.restore_snapshot(seed.create_snapshot())
batched.restore_snapshot(seed.create_snapshot())
sequential.transfer_reserved(ids_a, &"cargo-a", &"pickup-a", 20)
sequential.transfer_reserved(ids_b, &"cargo-b", &"pickup-b", 20)
var pickup_entries: Array[Dictionary] = [
{&"reservation_ids": ids_a, &"destination_inventory_id": &"cargo-a", &"operation_id": &"pickup-a"},
{&"reservation_ids": ids_b, &"destination_inventory_id": &"cargo-b", &"operation_id": &"pickup-b"},
]
var pickup_batch: Dictionary = batched.transfer_reserved_batch(pickup_entries, 20)
_expect(
pickup_batch[&"ok"] and pickup_batch[&"transfer_count"] == 2,
"reserved-stock batch applies every ordered pickup atomically"
)
_expect(
batched.create_snapshot() == sequential.create_snapshot(),
"reserved-stock batch is snapshot-equivalent to sequential public operations"
)
var before_replay := batched.create_snapshot()
var pickup_replay: Dictionary = batched.transfer_reserved_batch(pickup_entries, 20)
_expect(
pickup_replay[&"ok"]
and pickup_replay[&"results"][0][&"replay"]
and pickup_replay[&"results"][1][&"replay"]
and batched.create_snapshot() == before_replay,
"batch replay preserves per-operation idempotency without another transfer"
)
var pickup_snapshot := batched.create_snapshot()
sequential.transfer_available(&"cargo-a", &"destination", &"supply", 2, &"deliver-a", 21)
sequential.transfer_available(&"cargo-b", &"destination", &"supply", 2, &"deliver-b", 21)
var delivery_batch: Dictionary = batched.transfer_available_batch([
{
&"source_inventory_id": &"cargo-a",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"deliver-a",
},
{
&"source_inventory_id": &"cargo-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"deliver-b",
},
], 21)
_expect(
delivery_batch[&"ok"] and batched.create_snapshot() == sequential.create_snapshot(),
"available-stock batch preserves FEFO results, stack ids, and operation order"
)
var rollback = InventoryServiceScript.new()
rollback.restore_snapshot(pickup_snapshot)
var before_failure := rollback.create_snapshot()
var failed_batch: Dictionary = rollback.transfer_available_batch([
{
&"source_inventory_id": &"cargo-a",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"rollback-first",
},
{
&"source_inventory_id": &"missing-cargo",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 1,
&"operation_id": &"rollback-second",
},
], 22)
_expect(
not failed_batch[&"ok"] and failed_batch[&"code"] == &"ATOMIC_BATCH_FAILED",
"a failed line rejects the complete transfer batch"
)
_expect(
rollback.create_snapshot() == before_failure,
"failed batch rolls back stock, generated ids, and idempotency records exactly"
)
func _test_idempotency_conflicts_are_preflighted() -> void:
var sequential = InventoryServiceScript.new()
sequential.add_stack(&"source-a", &"supply", 4)
sequential.add_stack(&"source-b", &"supply", 4)
var first: Dictionary = sequential.transfer_available(
&"source-a", &"destination", &"supply", 2, &"shared-operation", 10
)
var after_first := sequential.create_snapshot()
var conflict: Dictionary = sequential.transfer_available(
&"source-b", &"destination", &"supply", 2, &"shared-operation", 10
)
_expect(first[&"ok"], "first normalized input binds its operation id")
_expect(
not conflict[&"ok"]
and conflict[&"code"] == &"IDEMPOTENCY_CONFLICT"
and sequential.create_snapshot() == after_first,
"sequential operation-id reuse with different input is rejected without side effects"
)
var action_conflict: Dictionary = sequential.reserve_item(
&"source-a", &"supply", 1, &"requester", &"delivery", 10, -1, &"shared-operation"
)
_expect(
not action_conflict[&"ok"]
and action_conflict[&"code"] == &"IDEMPOTENCY_CONFLICT"
and sequential.create_snapshot() == after_first,
"operation id is bound to its action as well as normalized input"
)
var duplicate_batch = InventoryServiceScript.new()
duplicate_batch.add_stack(&"source-a", &"supply", 4)
duplicate_batch.add_stack(&"source-b", &"supply", 4)
var before_duplicate := duplicate_batch.create_snapshot()
var duplicate_result: Dictionary = duplicate_batch.transfer_available_batch([
{
&"source_inventory_id": &"source-a",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"batch-shared",
},
{
&"source_inventory_id": &"source-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 2,
&"operation_id": &"batch-shared",
},
], 10)
_expect(
not duplicate_result[&"ok"]
and duplicate_result[&"code"] == &"IDEMPOTENCY_CONFLICT"
and duplicate_batch.create_snapshot() == before_duplicate,
"A-to-D then B-to-D duplicate operation id is rejected before the batch mutates"
)
var existing_conflict = InventoryServiceScript.new()
existing_conflict.add_stack(&"source-a", &"supply", 4)
existing_conflict.add_stack(&"source-b", &"supply", 4)
existing_conflict.transfer_available(
&"source-a", &"destination", &"supply", 1, &"already-bound", 11
)
var before_existing_conflict := existing_conflict.create_snapshot()
var existing_result: Dictionary = existing_conflict.transfer_available_batch([
{
&"source_inventory_id": &"source-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 1,
&"operation_id": &"would-be-first",
},
{
&"source_inventory_id": &"source-b",
&"destination_inventory_id": &"destination",
&"item_id": &"supply",
&"quantity": 1,
&"operation_id": &"already-bound",
},
], 11)
_expect(
not existing_result[&"ok"]
and existing_result[&"code"] == &"IDEMPOTENCY_CONFLICT"
and existing_conflict.create_snapshot() == before_existing_conflict,
"existing operation-id conflict is found before an earlier batch row can apply"
)
func _test_request_transitions() -> void:
var request = _reserved_request(&"request-state", 50, 4, 200)
_expect(request.assign(&"carrier", 20), "RESERVED request accepts a carrier assignment")
_expect(request.transition_to(LogisticsRequestScript.State.PICKING_UP, "", 21), "ASSIGNED transitions to PICKING_UP")
_expect(request.transition_to(LogisticsRequestScript.State.DELIVERING, "", 22), "PICKING_UP transitions to DELIVERING")
_expect(request.transition_to(LogisticsRequestScript.State.COMPLETED, "", 23), "DELIVERING transitions to COMPLETED")
_expect(not request.transition_to(LogisticsRequestScript.State.OPEN, "", 24), "terminal logistics request cannot reopen")
func _test_snapshot_round_trip_and_atomic_restore() -> void:
var original = InventoryServiceScript.new()
original.add_stack(&"warehouse", &"supply", 10, 88, 700, 5, 2)
original.reserve_item(&"warehouse", &"supply", 4, &"facility", &"restock", 10, 80, &"snapshot-reserve")
var snapshot: Dictionary = original.create_snapshot()
var restored = InventoryServiceScript.new()
_expect(restored.restore_snapshot(snapshot), "valid inventory snapshot restores")
_expect(restored.create_snapshot() == snapshot, "inventory snapshot round trip is exact")
var before_invalid: Dictionary = restored.create_snapshot()
var invalid: Dictionary = snapshot.duplicate(true)
invalid[&"inventories"][0][&"stacks"][0][&"reserved_quantity"] = 99
_expect(not restored.restore_snapshot(invalid), "invalid inventory snapshot is rejected")
_expect(restored.create_snapshot() == before_invalid, "failed restore leaves live inventory state untouched")
var request = _reserved_request(&"request-save", 70, 3, 500)
var request_snapshot: Dictionary = request.create_snapshot()
var restored_request = LogisticsRequestScript.from_snapshot(request_snapshot)
_expect(restored_request != null and restored_request.create_snapshot() == request_snapshot, "logistics request round trip preserves state")
var dispatcher = LogisticsDispatcherScript.new()
dispatcher.dispatch([request], {
&"carrier": {
&"capacity": 10,
&"current_load": 0,
&"role": &"porter",
&"path_minutes": 2,
&"position": Vector3i.ZERO,
},
}, 40)
var dispatcher_snapshot: Dictionary = dispatcher.create_snapshot()
var restored_dispatcher = LogisticsDispatcherScript.new()
_expect(restored_dispatcher.restore_snapshot(dispatcher_snapshot), "dispatcher snapshot restores")
_expect(restored_dispatcher.create_snapshot() == dispatcher_snapshot, "dispatcher snapshot round trip is exact")
func _test_strict_snapshot_migration_and_id_tombstones() -> void:
var base = InventoryServiceScript.new()
base.add_stack(&"warehouse", &"supply", 3, 90, 100, 5, 1)
base.reserve_item(
&"warehouse", &"supply", 1, &"requester", &"delivery", 6, 90, &"legacy-reserve"
)
var snapshot: Dictionary = base.create_snapshot()
var guard = InventoryServiceScript.new()
_expect(guard.restore_snapshot(snapshot), "strict snapshot fixture restores before corruption checks")
var guard_snapshot := guard.create_snapshot()
var negative_minute := snapshot.duplicate(true)
negative_minute[&"current_minute"] = -1
var negative_created := snapshot.duplicate(true)
negative_created[&"inventories"][0][&"stacks"][0][&"created_at"] = -1
var negative_expiry := snapshot.duplicate(true)
negative_expiry[&"inventories"][0][&"stacks"][0][&"expires_at"] = -2
var invalid_terminal := snapshot.duplicate(true)
invalid_terminal[&"reservations"][0][&"terminal_at"] = 0
_expect(
not guard.restore_snapshot(negative_minute)
and not guard.restore_snapshot(negative_created)
and not guard.restore_snapshot(negative_expiry)
and not guard.restore_snapshot(invalid_terminal),
"restore rejects negative or state-inconsistent canonical timestamps instead of clamping"
)
_expect(
guard.create_snapshot() == guard_snapshot,
"strict timestamp rejection is atomic"
)
var low_stack_sequence := snapshot.duplicate(true)
low_stack_sequence[&"next_stack_sequence"] = 1
var low_reservation_sequence := snapshot.duplicate(true)
low_reservation_sequence[&"next_reservation_sequence"] = 1
_expect(
not guard.restore_snapshot(low_stack_sequence)
and not guard.restore_snapshot(low_reservation_sequence)
and guard.create_snapshot() == guard_snapshot,
"next sequences at or below existing canonical ids are atomically rejected"
)
var tombstone_source = InventoryServiceScript.new()
var first_stack: Dictionary = tombstone_source.add_stack(&"source", &"part", 1)
var reservation: Dictionary = tombstone_source.reserve_item(
&"source", &"part", 1, &"consumer", &"use", 0, -1, &"tombstone-reserve"
)
tombstone_source.consume_reservation(
reservation[&"reservation_ids"][0], &"tombstone-consume", 1
)
var tombstone_snapshot := tombstone_source.create_snapshot()
_expect(
tombstone_source.total_quantity(&"source", &"part") == 0
and tombstone_snapshot[&"known_stack_ids"].has(String(first_stack[&"stack_id"])),
"deleted generated stack id remains in persisted known-id history"
)
var corrupt_tombstone_sequence := tombstone_snapshot.duplicate(true)
corrupt_tombstone_sequence[&"next_stack_sequence"] = 1
var tombstone_restored = InventoryServiceScript.new()
_expect(
not tombstone_restored.restore_snapshot(corrupt_tombstone_sequence),
"tombstone prevents a corrupted sequence from reusing a deleted auto id"
)
_expect(
tombstone_restored.restore_snapshot(tombstone_snapshot)
and tombstone_restored.create_snapshot() == tombstone_snapshot
and tombstone_restored.add_stack(&"source", &"part", 1)[&"stack_id"] == &"stack_00000002",
"valid tombstone snapshot round-trips exactly and allocates the next unused id"
)
var legacy_deleted = InventoryServiceScript.new()
var migrated_deleted_ok := legacy_deleted.restore_snapshot(
_as_legacy_inventory_snapshot(tombstone_snapshot)
)
_expect(
migrated_deleted_ok
and legacy_deleted.create_snapshot()[&"known_stack_ids"].has("stack_00000001"),
"version-1 counter migration reconstructs tombstones for deleted generated ids"
)
var legacy_snapshot := _as_legacy_inventory_snapshot(snapshot)
var migrated = InventoryServiceScript.new()
var legacy_restored := migrated.restore_snapshot(legacy_snapshot)
var legacy_replay: Dictionary = migrated.reserve_item(
&"warehouse", &"supply", 1, &"requester", &"delivery", 6, 90, &"legacy-reserve"
)
_expect(
legacy_restored and legacy_replay[&"ok"] and legacy_replay[&"replay"],
"version-1 snapshot migrates while preserving legacy replay behavior"
)
var rebound_conflict: Dictionary = migrated.reserve_item(
&"warehouse", &"supply", 2, &"requester", &"delivery", 6, 90, &"legacy-reserve"
)
_expect(
not rebound_conflict[&"ok"] and rebound_conflict[&"code"] == &"IDEMPOTENCY_CONFLICT",
"first legacy replay upgrades the operation id to an input-bound record"
)
var migrated_snapshot: Dictionary = migrated.create_snapshot()
var migrated_roundtrip = InventoryServiceScript.new()
_expect(
migrated_snapshot[&"version"] == 2
and migrated_roundtrip.restore_snapshot(migrated_snapshot)
and migrated_roundtrip.create_snapshot() == migrated_snapshot,
"migrated version-2 snapshot is canonical and exact on subsequent restore"
)
var ambiguous_legacy := _as_legacy_inventory_snapshot(snapshot)
ambiguous_legacy[&"idempotency_results"].append({
&"operation_key": "transfer|legacy-reserve",
&"result": {&"ok": true, &"code": &"OK", &"replay": false},
})
var ambiguous = InventoryServiceScript.new()
var ambiguous_loaded := ambiguous.restore_snapshot(ambiguous_legacy)
var ambiguous_use: Dictionary = ambiguous.transfer_available(
&"warehouse", &"destination", &"supply", 1, &"legacy-reserve", 6
)
_expect(
ambiguous_loaded
and not ambiguous_use[&"ok"]
and ambiguous_use[&"code"] == &"IDEMPOTENCY_CONFLICT",
"legacy cross-action duplicate ids load compatibly and fail closed on replay"
)
func _as_legacy_inventory_snapshot(snapshot: Dictionary) -> Dictionary:
var legacy_snapshot := snapshot.duplicate(true)
legacy_snapshot[&"version"] = 1
legacy_snapshot.erase(&"known_stack_ids")
legacy_snapshot.erase(&"known_reservation_ids")
var legacy_results: Array[Dictionary] = []
for record: Dictionary in legacy_snapshot[&"idempotency_results"]:
legacy_results.append({
&"operation_key": "%s|%s" % [record[&"action"], record[&"operation_id"]],
&"result": (record[&"result"] as Dictionary).duplicate(true),
})
legacy_snapshot[&"idempotency_results"] = legacy_results
return legacy_snapshot
func _reserved_request(
request_id: StringName,
priority: int,
weight: int,
deadline: int
):
var request = LogisticsRequestScript.new().configure(
request_id,
&"warehouse",
&"facility",
{&"supply": weight},
priority,
10,
deadline,
weight
)
var reservations: Array[StringName] = [StringName("reservation-%s" % request_id)]
var stacks: Array[StringName] = [StringName("stack-%s" % request_id)]
request.mark_reserved(reservations, stacks, 11)
return request
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://c0445hcoy7wh2
+75
View File
@@ -0,0 +1,75 @@
extends SceneTree
const StateScript = preload("res://autoload/game_state.gd")
const EconomyScript = preload("res://scripts/economy/economy_service.gd")
const AdapterScript = preload("res://scripts/economy/ledger_backed_resource_state.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: ledger-backed resource adapter")
var state = StateScript.new()
state.reset()
var economy = EconomyScript.new({
&"PLAYER_CASH": false,
&"CONSTRUCTION_EXPENSE": false,
})
economy.initialize_account(&"PLAYER_CASH", state.get_resource(&"gold"), 360, &"initial-player")
var minute := 400
var adapter = AdapterScript.new(
state,
economy,
&"CONSTRUCTION_EXPENSE",
&"CONSTRUCTION",
&"CONSTRUCTION_REFUND",
&"construction_service",
func() -> int: return minute
)
_expect(adapter.can_afford({&"gold": 500, &"materials": 8}), "combined ledger and material balances validate together")
_expect(adapter.spend({&"gold": 500, &"materials": 8}), "construction cost commits through one resource contract")
_expect(economy.get_balance(&"PLAYER_CASH") == 4500, "Gold spending changes the authoritative ledger balance")
_expect(state.get_resource(&"gold") == 4500, "GameState Gold remains a read-compatible UI mirror")
_expect(state.get_resource(&"materials") == 42, "non-Gold resources commit with the same spend")
_expect(economy.get_balance(&"CONSTRUCTION_EXPENSE") == 500, "construction uses an explicit counter-account")
var expense_entries: Array = economy.get_entries()
_expect(expense_entries[-1].absolute_minute == 400 and expense_entries[-1].category == &"CONSTRUCTION", "ledger entry captures minute and construction category")
minute = 410
_expect(adapter.apply_resource_deltas({&"gold": 125, &"materials": 2}), "demolition refund restores Gold and material atomically")
_expect(economy.get_balance(&"PLAYER_CASH") == 4625 and state.get_resource(&"gold") == 4625, "refund keeps ledger and UI mirror equal")
_expect(economy.get_balance(&"CONSTRUCTION_EXPENSE") == 375, "refund reduces its original expense counter-account")
_expect(state.get_resource(&"materials") == 44, "material refund is applied once")
var before: Dictionary = adapter.create_snapshot()
_expect(not adapter.spend({&"gold": 10_000, &"materials": 1}), "insufficient Gold rejects a mixed spend")
_expect(adapter.create_snapshot() == before, "rejected mixed spend changes neither ledger nor resources")
_expect(not adapter.apply_resource_deltas({&"gold": 1000}), "refund cannot exceed the recorded expense balance")
_expect(adapter.create_snapshot() == before, "failed refund rolls back every resource and ledger field")
adapter.spend({&"gold": 100, &"materials": 1})
_expect(adapter.restore_snapshot(before), "adapter restores resource and economy snapshots together")
_expect(adapter.create_snapshot() == before, "adapter snapshot round trip is exact")
_expect(economy.validate_ledger()[&"ok"], "restored authoritative ledger still reconciles")
state.free()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bfd08k411g8a7
+135
View File
@@ -0,0 +1,135 @@
extends SceneTree
const ManaNetworkServiceScript = preload("res://scripts/mana/mana_network_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: deterministic Mana network")
_test_graph_and_priority_allocation()
_test_brownout_threshold_and_hold()
_test_storage_flow()
_test_cadence_and_disabled_nodes()
_test_snapshot_and_invariants()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_graph_and_priority_allocation() -> void:
var service = ManaNetworkServiceScript.new()
_expect(service.register_source(&"tree", 10), "a source registers with integer output")
_expect(service.register_connector(&"trunk_bus"), "a connector registers independently from the walking graph")
_expect(service.register_consumer(&"clinic", 6, service.ConsumerPriority.ESSENTIAL), "an essential consumer registers")
_expect(service.register_consumer(&"casino", 5, service.ConsumerPriority.LUXURY), "a luxury consumer registers")
service.connect_nodes(&"tree", &"trunk_bus")
service.connect_nodes(&"clinic", &"trunk_bus")
service.connect_nodes(&"casino", &"trunk_bus")
var report: Dictionary = service.evaluate_now(0)
_expect(report[&"evaluated"] and report[&"components"].size() == 1, "connected nodes evaluate as one Mana component")
_expect(service.get_consumer_state(&"clinic") == service.ConsumerState.ACTIVE, "essential demand is supplied first")
_expect(service.get_allocated_mana(&"clinic") == 6, "essential consumer receives its full demand")
_expect(service.get_consumer_state(&"casino") == service.ConsumerState.DEGRADED, "remaining 80 percent supply creates a brownout")
_expect(service.get_allocated_mana(&"casino") == 4, "lower-priority consumer receives only the deterministic remainder")
var isolated = ManaNetworkServiceScript.new()
isolated.register_source(&"source", 4)
isolated.register_consumer(&"z_consumer", 4, isolated.ConsumerPriority.NORMAL)
isolated.register_consumer(&"a_consumer", 4, isolated.ConsumerPriority.NORMAL)
isolated.connect_nodes(&"source", &"z_consumer")
isolated.connect_nodes(&"source", &"a_consumer")
isolated.evaluate_now(0)
_expect(isolated.get_allocated_mana(&"a_consumer") == 4, "equal priorities use consumer id as a stable tie break")
_expect(isolated.get_allocated_mana(&"z_consumer") == 0, "stable tie break leaves the later consumer unpowered")
func _test_brownout_threshold_and_hold() -> void:
var service = ManaNetworkServiceScript.new()
service.register_source(&"source", 75)
service.register_consumer(&"workshop", 100)
service.connect_nodes(&"source", &"workshop")
service.evaluate_now(0)
_expect(service.get_consumer_state(&"workshop") == service.ConsumerState.DEGRADED, "exactly 75 percent supply is DEGRADED")
service.set_source_output(&"source", 74)
service.evaluate_now(10)
_expect(service.get_consumer_state(&"workshop") == service.ConsumerState.NO_POWER, "supply below 75 percent becomes NO_POWER")
_expect(service.get_node(&"workshop")[&"hold_until"] == 20, "NO_POWER starts a ten-minute minimum off hold")
service.set_source_output(&"source", 100)
service.evaluate_now(15)
_expect(service.get_allocated_mana(&"workshop") == 0, "consumer cannot oscillate on during its off hold")
service.evaluate_now(20)
_expect(service.get_consumer_state(&"workshop") == service.ConsumerState.ACTIVE, "consumer recovers at the first evaluation after its hold")
_expect(service.get_allocated_mana(&"workshop") == 100, "recovered consumer receives full demand")
func _test_storage_flow() -> void:
var service = ManaNetworkServiceScript.new()
service.register_source(&"source", 4)
service.register_storage(&"battery", 20, 10, 6)
service.register_consumer(&"lights", 10, service.ConsumerPriority.ESSENTIAL)
service.connect_nodes(&"source", &"battery")
service.connect_nodes(&"battery", &"lights")
var discharge_report: Dictionary = service.evaluate_now(0)
_expect(discharge_report[&"components"][0][&"discharged"] == 6, "storage discharge is capped by its max flow")
_expect(service.get_storage_current(&"battery") == 4, "discharged Mana is removed exactly once")
_expect(service.get_consumer_state(&"lights") == service.ConsumerState.ACTIVE, "stored Mana completes essential supply")
service.set_source_output(&"source", 20)
var charge_report: Dictionary = service.evaluate_now(10)
_expect(charge_report[&"components"][0][&"charged"] == 6, "surplus charges storage at its max flow")
_expect(service.get_storage_current(&"battery") == 10, "charging preserves storage capacity invariants")
func _test_cadence_and_disabled_nodes() -> void:
var service = ManaNetworkServiceScript.new()
service.register_source(&"source", 5)
service.register_consumer(&"pump", 5)
service.connect_nodes(&"source", &"pump")
_expect(not service.advance_one_minute(1)[&"evaluated"], "Mana allocation skips non-ten-minute ticks")
_expect(service.advance_one_minute(10)[&"evaluated"], "Mana allocation runs on a ten-minute boundary")
_expect(not service.advance_one_minute(10)[&"evaluated"], "the same boundary cannot evaluate twice")
_expect(service.set_node_enabled(&"pump", false), "a consumer can be administratively disabled")
_expect(service.get_consumer_state(&"pump") == service.ConsumerState.DISABLED, "disabled state applies immediately")
service.evaluate_now(20)
_expect(service.get_allocated_mana(&"pump") == 0, "disabled consumer receives no Mana")
_expect(service.set_node_enabled(&"pump", true), "a disabled consumer can be enabled")
service.evaluate_now(30)
_expect(service.get_consumer_state(&"pump") == service.ConsumerState.ACTIVE, "enabled consumer returns at the next evaluation")
func _test_snapshot_and_invariants() -> void:
var original = ManaNetworkServiceScript.new()
original.register_source(&"tree", 12)
original.register_connector(&"junction")
original.register_storage(&"crystal", 30, 8, 4)
original.register_consumer(&"room", 10)
original.connect_nodes(&"tree", &"junction")
original.connect_nodes(&"junction", &"crystal")
original.connect_nodes(&"junction", &"room")
original.evaluate_now(10)
var snapshot: Dictionary = original.create_snapshot()
var restored = ManaNetworkServiceScript.new()
_expect(restored.restore_snapshot(snapshot), "Mana graph and runtime state restore from a snapshot")
_expect(restored.create_snapshot() == snapshot, "restored Mana snapshot is byte-structure equivalent")
_expect(restored.node_count() == 4 and restored.edge_count() == 3, "snapshot restores all nodes and undirected edges")
_expect(restored.validate_invariants()[&"valid"], "restored allocation, storage and graph invariants hold")
_expect(restored.unregister_node(&"junction"), "removing a node also removes its incident edges")
_expect(restored.edge_count() == 0, "node removal cannot leave orphan Mana edges")
_expect(restored.validate_invariants()[&"valid"], "node removal keeps graph symmetry valid")
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://cv3qj8jpvyxd7
+247
View File
@@ -0,0 +1,247 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
var _facility_id: StringName
var _facility_inventory_id: StringName
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S2: runtime supply-to-service cycle")
_reset_runtime()
_build_operational_cafeteria()
_deliver_inputs_through_logistics()
_run_service_sale_with_round_trip()
_test_power_projection()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime() -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, 2202)
_clock.set_paused(true)
_expect(report[&"valid"], "runtime initializes all S2 definitions")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 5000, "starting Gold is initialized through the ledger")
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "initial ledger reconciles")
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _build_operational_cafeteria() -> void:
var build = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT), &"rotation": 0},
&"player", &"", 50, -1, &"s2.build.cafeteria"
)
_expect(build.is_success(), "cafeteria construction starts")
_clock.advance_minutes(180)
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "construction produces one authoritative facility")
_facility_id = StringName(facilities[0][&"instance_id"])
var operational = _runtime.facility_service.get_instance(_facility_id)
_expect(operational != null and operational.definition_id == &"cafeteria", "completed construction registers with FacilityService")
_expect(operational.state == FacilityInstance.State.NO_STAFF, "staff requirement prevents premature opening")
_expect(_runtime.economy_service.has_account(StringName("FACILITY_REVENUE:%s" % _facility_id)), "facility receives a dedicated revenue account")
_expect(not _runtime.mana_network_service.get_node(_facility_id).is_empty(), "Mana demand is registered for the completed facility")
_facility_inventory_id = operational.local_inventory_id
var staffing = _runtime.submit_command(
&"set_facility_conditions",
{&"facility_id": _facility_id, &"conditions": {&"active_staff_ids": [&"cook_1", &"server_1"]}},
&"manager", _facility_id, 20, -1, &"s2.staff.cafeteria"
)
_expect(staffing.is_success(), "two active staff can be assigned through a command")
_expect(operational.state == FacilityInstance.State.OPEN, "staffed and powered cafeteria becomes operational")
_expect(_state.get_resource(&"gold") == 4200, "construction Gold is paid once through the authoritative ledger")
_expect(_runtime.economy_service.get_balance(&"CONSTRUCTION_EXPENSE") == 800, "construction counter-account retains the capital flow")
func _deliver_inputs_through_logistics() -> void:
var stocked = _runtime.submit_command(
&"add_inventory_stack",
{
&"owner_inventory_id": &"supplier.main",
&"item_id": &"ingredient/basic_meal",
&"quantity": 3,
&"quality": 70,
&"unit_weight": 1,
},
&"supplier", &"supplier.main", 10, -1, &"s2.stock.ingredients"
)
_expect(stocked.is_success(), "supplier stock enters InventoryService")
var created = _runtime.submit_command(
&"create_logistics_request",
{
&"request_id": &"delivery_meals_1",
&"source_inventory_id": &"supplier.main",
&"destination_inventory_id": _facility_inventory_id,
&"item_requirements": {&"ingredient/basic_meal": 2},
&"priority": 80,
&"deadline": _clock.absolute_minute + 120,
},
&"manager", _facility_id, 10, -1, &"s2.logistics.create"
)
_expect(created.is_success(), "logistics request reserves supplier stock")
var logistics: LogisticsRequest = _runtime.logistics_requests[&"delivery_meals_1"]
_expect(logistics.state == LogisticsRequest.State.RESERVED and logistics.total_weight == 2, "request records reserved weight and state")
_expect(_runtime.inventory_service.available_quantity(&"supplier.main", &"ingredient/basic_meal") == 1, "reserved supplier stock cannot be reused")
var dispatched = _runtime.submit_command(
&"dispatch_logistics",
{&"carriers": {
&"porter_1": {&"capacity": 10, &"current_load": 0, &"path_minutes": 2, &"role": &"logistics"},
&"porter_2": {&"capacity": 10, &"current_load": 0, &"path_minutes": 4, &"role": &"carrier"},
}},
&"dispatcher", &"delivery_meals_1", 10, -1, &"s2.logistics.dispatch"
)
_expect(dispatched.is_success(), "dispatcher evaluates the reserved request")
_expect(logistics.assigned_carrier_id == &"porter_1", "deterministic score chooses the suitable nearest carrier")
_expect(logistics.state == LogisticsRequest.State.ASSIGNED, "dispatch advances request to ASSIGNED")
var start_pickup = _advance_delivery(&"s2.logistics.to_pickup")
_expect(start_pickup.is_success() and logistics.state == LogisticsRequest.State.PICKING_UP, "assigned carrier begins pickup")
var picked_up = _advance_delivery(&"s2.logistics.pickup")
_expect(picked_up.is_success() and logistics.state == LogisticsRequest.State.DELIVERING, "source arrival transfers reserved stock into cargo")
_expect(_runtime.inventory_service.total_quantity(&"cargo:porter_1", &"ingredient/basic_meal") == 2, "cargo owns exactly the picked quantity")
_expect(_runtime.inventory_service.total_quantity(&"supplier.main", &"ingredient/basic_meal") == 1, "pickup subtracts source stock without duplication")
var delivered = _advance_delivery(&"s2.logistics.delivery")
_expect(delivered.is_success() and logistics.state == LogisticsRequest.State.COMPLETED, "destination arrival completes logistics")
_expect(_runtime.inventory_service.total_quantity(_facility_inventory_id, &"ingredient/basic_meal") == 2, "facility inventory receives the delivered inputs")
_expect(_runtime.inventory_service.total_quantity(&"cargo:porter_1", &"ingredient/basic_meal") == 0, "completed delivery leaves no duplicated cargo")
_expect(_runtime.logistics_dispatcher.get_reserved_load(&"porter_1") == 0, "carrier capacity releases on completion")
_expect(_runtime.inventory_service.validate_invariants()[&"ok"], "delivery preserves every inventory invariant")
func _advance_delivery(idempotency_key: StringName) -> CommandResult:
return _runtime.submit_command(
&"advance_logistics_request",
{&"request_id": &"delivery_meals_1"},
&"porter_1", &"delivery_meals_1", 10, -1, idempotency_key
)
func _run_service_sale_with_round_trip() -> void:
var wallet = _runtime.submit_command(
&"register_guest_wallet",
{&"guest_id": &"guest_1", &"gold": 100},
&"guest_1", &"guest_1", 10, -1, &"s2.guest.wallet"
)
_expect(wallet.is_success(), "guest wallet is initialized through SYSTEM_SOURCE")
var requested = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"meal_request_1",
&"facility_id": _facility_id,
&"guest_ids": [&"guest_1"],
&"service_type": &"meal",
&"options": {&"service_minutes": 3},
},
&"guest_1", _facility_id, 10, -1, &"s2.service.request"
)
_expect(requested.is_success(), "service request reserves its recipe input and enters the queue")
_expect(_runtime.inventory_service.available_quantity(_facility_inventory_id, &"ingredient/basic_meal") == 1, "queued service protects one ingredient from reuse")
_clock.advance_one_minute()
var active: Array = _runtime.facility_service.get_active_services(_facility_id)
_expect(active.size() == 1, "facility queue fills an available service slot")
var service_id: StringName = active[0].service_id
_expect(active[0].price_snapshot == 35 and active[0].duration_minutes == 3, "service freezes quoted price and duration")
var mid_snapshot: Dictionary = _runtime.create_snapshot()
var mid_digest := SimulationDigest.compute(mid_snapshot)
_clock.advance_minutes(3)
var completed: ServiceInstance = _runtime.facility_service.get_service(service_id)
_expect(completed != null and completed.state == ServiceInstance.State.COMPLETED, "three active minutes complete the service")
_expect(_runtime.inventory_service.total_quantity(_facility_inventory_id, &"ingredient/basic_meal") == 1, "completion consumes exactly one reserved ingredient")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_1") == 65, "guest pays the immutable quoted price")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4235, "service cash sweep increases player Gold")
_expect(_state.get_resource(&"gold") == 4235, "UI Gold mirror follows the ledger transaction")
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "service sale remains double-entry balanced")
var saw_completion := false
for event in _runtime.domain_events.recent_events():
if event.event_type == &"SERVICE_COMPLETED" and service_id in event.entity_ids:
saw_completion = true
_expect(saw_completion, "service completion is projected as a domain event")
var final_digest: String = _runtime.latest_digest
_expect(_runtime.restore_snapshot(mid_snapshot), "mid-service snapshot restores all S2 services")
_expect(_runtime.latest_digest == mid_digest, "mid-service restore reproduces the exact digest")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_1") == 100, "restore rolls payment back to the pre-completion ledger")
_clock.advance_minutes(3)
_expect(_runtime.latest_digest == final_digest, "replaying completion reproduces inventory, ledger, events and IDs exactly")
_expect(_runtime.economy_service.get_entries().filter(func(entry: LedgerEntry) -> bool: return entry.category == &"SERVICE_SALE").size() == 1, "replayed service sale is recorded exactly once")
var no_stock = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"meal_request_too_many",
&"facility_id": _facility_id,
&"guest_ids": [&"guest_1"],
&"options": {&"service_minutes": 20},
},
&"guest_1", _facility_id, 10, -1, &"s2.service.second"
)
_expect(no_stock.is_success(), "remaining delivered input can fund one more queued service")
var before_rejected: Dictionary = _runtime.inventory_service.create_snapshot()
var rejected = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"meal_request_no_stock",
&"facility_id": _facility_id,
&"guest_ids": [&"guest_1"],
},
&"guest_1", _facility_id, 10, -1, &"s2.service.no_stock"
)
_expect(not rejected.is_success(), "service without available recipe input is rejected")
_expect(_runtime.inventory_service.create_snapshot() == before_rejected, "rejected service request leaves reservations and stock unchanged")
func _test_power_projection() -> void:
_runtime.mana_network_service.set_source_output(_runtime.STARTER_MANA_SOURCE, 0)
var minutes_to_boundary: int = 10 - (_clock.absolute_minute % 10)
_clock.advance_minutes(minutes_to_boundary)
var facility = _runtime.facility_service.get_instance(_facility_id)
_expect(_runtime.mana_network_service.get_consumer_state(_facility_id) == ManaNetworkService.ConsumerState.NO_POWER, "Mana shortage shuts the cafeteria consumer down")
_expect(facility.state == FacilityInstance.State.NO_POWER, "Mana consumer state projects into FacilityService")
var interrupted_request_id := &"meal_request_too_many"
var interrupted_services: Array = _runtime.facility_service.get_active_services(_facility_id)
_expect(
_runtime.facility_service.get_request(interrupted_request_id) != null \
and interrupted_services.size() == 1 \
and interrupted_services[0].state == ServiceInstance.State.PAUSED,
"power loss safely pauses the active service without losing its request"
)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://d2yqh462oc5t2
+778
View File
@@ -0,0 +1,778 @@
extends SceneTree
const GuestDefinitionType = preload("res://scripts/guest/guest_definition.gd")
const GuestRuntimeType = preload("res://scripts/guest/guest_runtime.gd")
const GuestServiceType = preload("res://scripts/guest/guest_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: deterministic Guest AI")
_test_definition_and_runtime_contract()
_test_need_updates_and_single_clamp()
_test_need_context_and_adult_gate()
_test_shared_need_context_fast_paths()
_test_batched_need_updates_match_individual_api()
_test_candidate_filtering()
_test_integer_utility_and_stable_tie_break()
_test_state_machine_and_loop_guard()
_test_atomic_queue_abandonment()
_test_service_interruption_is_idempotent()
_test_service_need_effect_schema()
_test_path_failure_safe_recovery()
_test_exit_persistence_boundary()
_test_snapshot_round_trip_and_atomic_rejection()
_test_v1_need_migration_and_schema_rejection()
_test_deterministic_decision_snapshot()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_definition_and_runtime_contract() -> void:
var definition := GuestDefinitionType.new().configure(&"adventurer", {
&"tags": [&"adventurer", &"general", &"general"],
&"wealth_min": 100,
&"wealth_max": 500,
&"base_patience": 45,
&"risk_tolerance": 80,
&"preference_weights": {&"meal": 8, &"quiet": -2},
&"group_size_weights": {1: 80, 2: 20},
&"legal_sensitivity": 60,
&"quality_sensitivity": 70,
&"adult_verified": false,
&"need_rates": {&"hunger": 3, &"sleep": -2, &"leisure": 1},
&"credit_limit_gold": 100,
&"minimum_quality": 40,
})
_expect(definition.is_valid(), "complete guest definition validates")
_expect(definition.tags == [&"adventurer", &"general"], "definition tags are unique and stable-sorted")
_expect(definition.get_need_rate(&"intimacy") == 0, "omitted canonical Need rates normalize to zero")
_expect(definition.need_rates.size() == 6, "definition always materializes the six canonical Need IDs")
var round_trip := GuestDefinitionType.from_snapshot(definition.create_snapshot())
_expect(round_trip != null, "definition reconstructs from its canonical snapshot")
_expect(round_trip.create_snapshot() == definition.create_snapshot(), "definition snapshot round trip is exact")
var legacy_definition := GuestDefinitionType.new().configure(&"legacy_alias", {
&"need_rates": {&"rest": 1},
})
_expect(not legacy_definition.is_valid(), "legacy Definition Need aliases are invalid outside v1 migration")
var legacy_runtime := GuestRuntimeType.new().configure(&"legacy_guest", &"adventurer", {
&"needs": {&"social": 10},
})
_expect(not legacy_runtime.is_valid(), "legacy Runtime Need aliases are invalid outside v1 migration")
var coerced_adult := GuestDefinitionType.new().configure(&"coerced_adult", {
&"adult_verified": "true",
})
_expect(not coerced_adult.is_valid(), "adult verification cannot be supplied through truthy string coercion")
var service := GuestServiceType.new()
_expect(service.register_definition(definition), "valid definition registers")
_expect(not service.register_definition(definition), "duplicate definition ID is rejected")
var guest := service.spawn_guest(&"guest_contract", &"adventurer", 10, {
&"planned_exit_at": 200,
&"current_cell": Vector3i(2, 3, 0),
})
_expect(guest != null and guest.is_valid(), "guest runtime spawns with required authoritative fields")
_expect(guest.wallet_gold == 100 and guest.credit_limit_gold == 100, "definition supplies deterministic wallet floor and credit limit")
_expect(guest.patience_remaining == 45, "definition supplies base queue patience")
_expect(service.validate_invariants()[&"valid"], "fresh GuestService invariants hold")
func _test_need_updates_and_single_clamp() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_needs", &"adventurer", 0, {
&"state": GuestRuntimeType.State.MOVING,
&"needs": {
&"hunger": 99,
&"sleep": 1,
&"hygiene": 99,
&"medical": 99,
&"leisure": 99,
&"intimacy": 99,
},
&"planned_exit_at": 500,
})
var report: Dictionary = service.update_needs(guest.guest_id, {
&"dirt": 2,
&"injury": 2,
&"illness": 2,
&"need_effects": {
&"hunger": 5,
&"sleep": -5,
&"hygiene": 3,
&"medical": 3,
&"leisure": 3,
&"intimacy": 3,
},
})
_expect(report[&"ok"], "Need update succeeds for a live guest")
_expect(report[&"deltas"]["hunger"] == 10, "base, movement and external hunger effects add before clamp")
_expect(report[&"deltas"]["sleep"] == -5, "positive and negative sleep effects combine before clamp")
_expect(guest.needs[&"hunger"] == 100 and guest.needs[&"sleep"] == 0, "Need upper and lower bounds clamp once")
_expect(guest.needs[&"hygiene"] == 100 and guest.needs[&"medical"] == 100, "dirt, injury and illness update their distinct Needs")
_expect(guest.needs[&"leisure"] == 100 and guest.needs[&"intimacy"] == 100, "leisure and intimacy effects use canonical IDs")
_expect(service.advance_one_minute(1)[&"ok"], "one-minute GuestService cadence advances")
_expect(not service.advance_one_minute(1)[&"ok"], "the same minute cannot apply Need drift twice")
func _test_need_context_and_adult_gate() -> void:
var service := _service_with_definitions()
var verified := service.spawn_guest(&"verified_context", &"verified_noble", 0, {
&"needs": {&"hunger": 0, &"sleep": 0, &"hygiene": 10, &"medical": 20, &"leisure": 0, &"intimacy": 30},
&"planned_exit_at": 500,
})
var unverified := service.spawn_guest(&"unverified_context", &"adventurer", 0, {
&"needs": {&"hunger": 0, &"sleep": 0, &"hygiene": 10, &"medical": 20, &"leisure": 0, &"intimacy": 30},
&"planned_exit_at": 500,
})
var day := service.update_needs(verified.guest_id, {
&"unstimulated": false,
&"dirt": 5,
&"injury": 4,
&"illness": 3,
&"is_night": false,
&"intimacy_day": 3,
&"intimacy_night": 7,
})
_expect(day[&"deltas"]["hygiene"] == 5, "integer dirt context raises Hygiene only")
_expect(day[&"deltas"]["medical"] == 7, "injury and illness combine into Medical")
_expect(day[&"deltas"]["intimacy"] == 3, "day context uses the adult-verified Intimacy day rate")
var night := service.update_needs(verified.guest_id, {
&"unstimulated": false,
&"is_night": true,
&"intimacy_day": 3,
&"intimacy_night": 7,
})
_expect(night[&"deltas"]["intimacy"] == 7, "night context selects the separate Intimacy night rate")
var blocked := service.update_needs(unverified.guest_id, {
&"unstimulated": false,
&"is_night": true,
&"intimacy_night": 7,
})
_expect(blocked[&"deltas"]["intimacy"] == 0, "unverified guests cannot receive contextual adult Intimacy drift")
var before_invalid := service.create_snapshot()
var wrong_type := service.update_needs(verified.guest_id, {&"injury": "4"})
_expect(not wrong_type[&"ok"], "stringified Need context integers are rejected")
_expect(service.create_snapshot() == before_invalid, "invalid context leaves live Need state untouched")
var legacy_alias := service.update_needs(verified.guest_id, {
&"need_effects": {&"rest": -5},
})
_expect(not legacy_alias[&"ok"], "legacy Need aliases are rejected outside snapshot migration")
_expect(service.create_snapshot() == before_invalid, "legacy context rejection is atomic")
func _test_batched_need_updates_match_individual_api() -> void:
var batched := _service_with_definitions()
batched.spawn_guest(&"z_guest", &"adventurer", 0, {
&"state": GuestRuntimeType.State.MOVING,
&"group_id": &"party",
&"needs": {&"hunger": 10, &"sleep": 20, &"hygiene": 30, &"medical": 40, &"leisure": 50, &"intimacy": 60},
&"planned_exit_at": 500,
})
batched.spawn_guest(&"a_guest", &"verified_noble", 0, {
&"needs": {&"hunger": 5, &"sleep": 15, &"hygiene": 25, &"medical": 35, &"leisure": 45, &"intimacy": 55},
&"planned_exit_at": 500,
})
var individual := GuestServiceType.new()
individual.restore_snapshot(batched.create_snapshot())
var default_context := {
&"unstimulated": false,
&"dirt": 1,
&"need_effects": {&"sleep": 3},
}
var z_context := {
&"solo_guest": true,
&"injury": 2,
&"need_effects": {&"hunger": -1, &"leisure": 4},
}
var expected_reports: Array[Dictionary] = [
individual.update_needs(&"a_guest", default_context),
individual.update_needs(&"z_guest", z_context),
]
individual.current_minute = 1
var batch_report := batched.advance_one_minute(1, {
&"default": default_context,
"z_guest": z_context,
})
_expect(
batch_report[&"guest_reports"] == expected_reports,
"batched Need cadence preserves individual report values and stable guest-id order"
)
_expect(
batched.create_snapshot() == individual.create_snapshot(),
"optimized batched Need mutation is snapshot-equivalent to individual updates"
)
func _test_shared_need_context_fast_paths() -> void:
var source := _service_with_definitions()
for guest_id: StringName in [&"z_guest", &"a_guest"]:
source.spawn_guest(guest_id, &"adventurer", 0, {
&"state": GuestRuntimeType.State.MOVING,
&"needs": {
&"hunger": 10,
&"sleep": 20,
&"hygiene": 30,
&"medical": 40,
&"leisure": 50,
&"intimacy": 60,
},
&"planned_exit_at": 500,
})
var start := source.create_snapshot()
var empty_fast := GuestServiceType.new()
var empty_reference := GuestServiceType.new()
empty_fast.restore_snapshot(start)
empty_reference.restore_snapshot(start)
var empty_report := empty_fast.advance_one_minute(1)
var empty_reference_report := empty_reference.advance_one_minute(1, {
&"default": {},
&"a_guest": {},
&"z_guest": {},
})
_expect(
empty_report == empty_reference_report,
"empty shared Need context preserves the preflight-path report exactly"
)
_expect(
empty_fast.create_snapshot() == empty_reference.create_snapshot(),
"empty shared Need context preserves the preflight-path snapshot exactly"
)
var common_context := {
&"unstimulated": false,
&"dirt": 2,
&"injury": 1,
&"need_effects": {&"sleep": 3, &"leisure": -2},
}
var default_fast := GuestServiceType.new()
var default_reference := GuestServiceType.new()
default_fast.restore_snapshot(start)
default_reference.restore_snapshot(start)
var default_report := default_fast.advance_one_minute(1, {
&"default": common_context,
})
var default_reference_report := default_reference.advance_one_minute(1, {
&"default": common_context,
&"a_guest": common_context,
&"z_guest": common_context,
})
_expect(
default_report == default_reference_report,
"default-only shared Need context preserves the override-path report exactly"
)
_expect(
default_fast.create_snapshot() == default_reference.create_snapshot(),
"default-only shared Need context preserves the override-path snapshot exactly"
)
var invalid_default := GuestServiceType.new()
invalid_default.restore_snapshot(start)
var before_invalid := invalid_default.create_snapshot()
var invalid_report := invalid_default.advance_one_minute(1, {
&"default": {&"injury": "1"},
})
_expect(not invalid_report[&"ok"], "invalid default-only Need context is rejected")
_expect(
invalid_default.create_snapshot() == before_invalid,
"invalid default-only Need context is atomic across the population and minute"
)
var malformed_default := GuestServiceType.new()
var malformed_reference := GuestServiceType.new()
malformed_default.restore_snapshot(start)
malformed_reference.restore_snapshot(start)
var malformed_report := malformed_default.advance_one_minute(1, {
&"default": "legacy malformed default",
})
var malformed_reference_report := malformed_reference.advance_one_minute(1)
_expect(
malformed_report == malformed_reference_report,
"non-Dictionary default retains the legacy empty-context meaning"
)
_expect(
malformed_default.create_snapshot() == malformed_reference.create_snapshot(),
"malformed-default compatibility remains snapshot-exact"
)
func _test_candidate_filtering() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_filter", &"adventurer", 0, {
&"wallet_gold": 500,
&"credit_limit_gold": 100,
&"planned_exit_at": 1_000,
})
var candidates: Array = [
_candidate(&"cannot_pay", {&"price_gold": 501}),
_candidate(&"adult_room", {&"adult_only": true}),
_candidate(&"unreachable", {&"reachable": false}),
_candidate(&"closed", {&"is_open": false}),
_candidate(&"too_slow", {&"wait_minutes": 120}),
_candidate(&"too_risky", {&"risk_level": 81}),
_candidate(&"too_illegal", {&"illegal_level": 41}),
_candidate(&"too_poor", {&"expected_quality": 39}),
_candidate(&"legacy_relief", {&"need_relief": {&"fun": 40}}),
_candidate(&"legacy_effects", {&"need_effects": {&"safety": -20}}),
_candidate(&"canonical_effects", {&"need_effects": {&"medical": -20}}),
_candidate(&"credit_ok", {&"price_gold": 550, &"credit_allowed": true}),
_candidate(&"normal_ok"),
]
var filtered := service.extract_candidates(guest.guest_id, candidates, {
&"absolute_minute": 10,
&"adult_content_enabled": true,
&"wait_hard_limit": 120,
})
_expect(_candidate_ids(filtered) == [&"canonical_effects", &"credit_ok", &"normal_ok"], "candidate extraction applies every hard filter before Utility")
var reasons := _rejection_map(service.get_last_candidate_rejections(guest.guest_id))
_expect(reasons[&"cannot_pay"] == &"CANNOT_PAY", "SYS-GST-001 rejects an unaffordable facility")
_expect(reasons[&"adult_room"] == &"ADULT_NOT_VERIFIED", "SYS-GST-002 rejects restricted content before Utility")
_expect(reasons[&"unreachable"] == &"UNREACHABLE", "unreachable facility is removed")
_expect(reasons[&"too_slow"] == &"WAIT_HARD_LIMIT", "wait equal to the hard limit is removed")
_expect(reasons[&"too_risky"] == &"RISK_REFUSED", "risk above guest tolerance is removed")
_expect(reasons[&"too_illegal"] == &"ILLEGALITY_REFUSED", "illegal level above sensitivity threshold is removed")
_expect(reasons[&"too_poor"] == &"QUALITY_TOO_LOW", "quality below guest minimum is removed")
_expect(reasons[&"legacy_relief"] == &"INVALID_NEED_MAP", "candidate Need maps cannot use legacy aliases")
_expect(reasons[&"legacy_effects"] == &"INVALID_NEED_MAP", "candidate need_effects also reject migration-only aliases")
guest.wallet_gold = 100
var reserve_filtered := service.extract_candidates(guest.guest_id, [
_candidate(&"meal_with_cash", {&"price_gold": 80}),
_candidate(&"leisure_below_reserve", {
&"price_gold": 80,
&"service_category": &"entertainment",
}),
], {&"absolute_minute": 10})
_expect(_candidate_ids(reserve_filtered) == [&"meal_with_cash"], "discretionary spending preserves the personality-based survival reserve")
guest.wallet_gold = 500
var verified := service.spawn_guest(&"guest_verified", &"verified_noble", 0, {
&"wallet_gold": 1_000,
&"planned_exit_at": 1_000,
})
var adult_candidates := service.extract_candidates(verified.guest_id, [
_candidate(&"adult_room", {&"adult_only": true}),
], {&"adult_content_enabled": true, &"absolute_minute": 1})
_expect(adult_candidates.size() == 1, "verified adult guest can retain a permitted restricted candidate")
var content_blocked := service.extract_candidates(verified.guest_id, [
_candidate(&"adult_room", {&"adult_only": true}),
], {&"adult_content_enabled": false, &"absolute_minute": 1})
_expect(content_blocked.is_empty(), "Content Filter still overrides verified adult status")
var callback_order: Array[StringName] = []
service.extract_candidates(guest.guest_id, [
_candidate(&"z_path"),
_candidate(&"a_path"),
], {
&"absolute_minute": 1,
&"reachability_callback": func(_runtime: GuestRuntime, facility: Dictionary, _context: Dictionary) -> bool:
callback_order.append(facility[&"facility_id"])
return true,
})
_expect(callback_order == [&"a_path", &"z_path"], "reachability callbacks run in stable facility-ID order")
func _test_integer_utility_and_stable_tie_break() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_utility", &"adventurer", 0, {
&"wallet_gold": 1_000,
&"planned_exit_at": 1_000,
&"state": GuestRuntimeType.State.DECIDING,
&"needs": {&"hunger": 90},
})
var shared := {
&"need_relief": 40,
&"preference": 2,
&"expected_quality": 3,
&"novelty": 0,
&"group_fit": 4,
&"price_burden": 5,
&"travel_minutes": 6,
&"wait_minutes": 7,
&"risk": 8,
&"risk_sensitivity": 2,
&"repetition_penalty": 9,
&"urgency": 90,
&"minimum_quality": 0,
}
var candidate_z := shared.duplicate(true)
candidate_z[&"facility_id"] = &"z_room"
var candidate_a := shared.duplicate(true)
candidate_a[&"facility_id"] = &"a_room"
var evaluated := service.evaluate_candidate(guest.guest_id, candidate_z)
_expect(evaluated[&"utility"] is int, "Utility result is an integer")
_expect(evaluated[&"utility"] == 264, "Utility follows the documented integer term equation exactly")
var ranked := service.rank_candidates(guest.guest_id, [candidate_z, candidate_a])
_expect(ranked[0][&"facility_id"] == &"a_room", "SYS-GST-003 resolves a full Utility tie by facility_id")
var decision := service.decide(guest.guest_id, [candidate_z, candidate_a], 1, {
&"exit_threshold": 15,
})
_expect(decision[&"ok"] and decision[&"candidate"][&"facility_id"] == &"a_room", "decision reserves the stable winning candidate")
_expect(guest.state == GuestRuntimeType.State.MOVING and guest.target_facility_id == &"a_room", "successful decision enters MOVING with one target")
func _test_state_machine_and_loop_guard() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_states", &"adventurer", 0, {
&"planned_exit_at": 500,
})
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.ENTERING, 10)[&"ok"], "SPAWNING enters ENTERING")
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.DECIDING, 10)[&"ok"], "ENTERING enters DECIDING")
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.MOVING, 10)[&"ok"], "DECIDING enters MOVING")
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.REPLANNING, 10)[&"ok"], "MOVING enters REPLANNING")
var guarded := service.transition_guest(guest.guest_id, GuestRuntimeType.State.DECIDING, 10)
_expect(not guarded[&"ok"] and guarded[&"loop_guard"], "fifth transition in one Tick activates AI loop guard")
_expect(guest.state == GuestRuntimeType.State.DECIDING, "AI loop guard stops the guest in DECIDING")
_expect(service.get_diagnostics()[-1][&"code"] == &"AI_LOOP_GUARD", "loop guard leaves a diagnostic record")
var invalid := service.transition_guest(guest.guest_id, GuestRuntimeType.State.DESPAWNING, 11)
_expect(not invalid[&"ok"], "invalid state-machine edge is rejected")
func _test_atomic_queue_abandonment() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_queue")
var enqueue_calls := [0]
var entered := service.enter_queue(
guest.guest_id,
&"meal_room",
&"request_queue",
4,
1,
func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> bool:
enqueue_calls[0] += 1
return true
)
_expect(entered[&"ok"] and enqueue_calls[0] == 1, "queue entry crosses the Callable boundary once")
_expect(guest.state == GuestRuntimeType.State.QUEUING and guest.patience_remaining == 1, "queue entry records facility, request and initial patience")
var failed_calls := [0]
var failed_leave := service.tick_queue_patience(
guest.guest_id,
5,
0,
func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> bool:
failed_calls[0] += 1
return false
)
_expect(not failed_leave[&"ok"] and failed_calls[0] == 1, "external queue removal failure is surfaced")
_expect(guest.state == GuestRuntimeType.State.QUEUING and guest.patience_remaining == 1, "failed queue removal leaves guest state completely unchanged")
_expect(guest.target_facility_id == &"meal_room" and guest.queue_request_id == &"request_queue", "failed queue removal retains both cross-service references")
var successful_calls := [0]
var abandoned := service.tick_queue_patience(
guest.guest_id,
5,
0,
func(callback_guest_id: StringName, facility_id: StringName, request_id: StringName) -> Dictionary:
successful_calls[0] += 1
return {&"ok": callback_guest_id == guest.guest_id and facility_id == &"meal_room" and request_id == &"request_queue"}
)
_expect(abandoned[&"ok"] and abandoned[&"abandoned"] and successful_calls[0] == 1, "SYS-GST-004 removes an exhausted guest through one atomic callback")
_expect(guest.state == GuestRuntimeType.State.ABANDONING and guest.patience_remaining == 0, "successful patience exhaustion enters ABANDONING")
_expect(guest.target_facility_id.is_empty() and guest.queue_request_id.is_empty(), "atomic abandonment clears facility and queue ownership together")
func _test_service_interruption_is_idempotent() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_interrupt")
_expect(service.begin_service(guest.guest_id, &"bath", &"service_1", 4)[&"ok"], "moving guest begins a facility service")
var callback_calls := [0]
var first := service.interrupt_service(
guest.guest_id,
&"service_1",
5,
func(_guest_id: StringName, _service_id: StringName) -> bool:
callback_calls[0] += 1
return true
)
var replay := service.interrupt_service(
guest.guest_id,
&"service_1",
5,
func(_guest_id: StringName, _service_id: StringName) -> bool:
callback_calls[0] += 1
return true
)
_expect(first[&"ok"] and not first[&"replayed"], "first facility stop interrupts the active service")
_expect(replay[&"ok"] and replay[&"replayed"], "duplicate facility stop is an idempotent replay")
_expect(callback_calls[0] == 1, "SYS-GST-005 invokes interruption settlement only once")
_expect(guest.state == GuestRuntimeType.State.INTERRUPTED and guest.current_service_id.is_empty(), "interruption frees guest service ownership")
func _test_service_need_effect_schema() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_effect_schema")
guest.needs[&"sleep"] = 95
guest.needs[&"hygiene"] = 5
_expect(service.begin_service(guest.guest_id, &"bath", &"service_effects", 4)[&"ok"], "canonical Need-effect fixture begins service")
var before_invalid := service.create_snapshot()
var invalid := service.complete_service(
guest.guest_id,
&"service_effects",
5,
80,
{&"rest": -50}
)
_expect(not invalid[&"ok"], "service completion rejects a legacy Need-effect alias")
_expect(service.create_snapshot() == before_invalid, "invalid service effects do not partially complete service")
var completed := service.complete_service(
guest.guest_id,
&"service_effects",
5,
120,
{&"sleep": 20, &"hygiene": -20}
)
_expect(completed[&"ok"] and completed[&"satisfaction_sample"] == 100, "canonical service effects complete with satisfaction clamp")
_expect(guest.needs[&"sleep"] == 100 and guest.needs[&"hygiene"] == 0, "service Need effects clamp each canonical axis once")
func _test_path_failure_safe_recovery() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_path")
var recovery_targets: Array = [
{&"kind": &"emergency_exit", &"cell": Vector3i(9, 0, 0)},
{&"kind": &"entrance", &"cell": Vector3i(5, 0, 0)},
{&"kind": &"guidance", &"cell": Vector3i(2, 0, 0)},
]
var reachable := func(_from: Vector3i, target: Vector3i, _guest_id: StringName, _data: Dictionary) -> bool:
return target != Vector3i(2, 0, 0)
var first := service.record_path_failure(guest.guest_id, 4, recovery_targets, reachable)
var second := service.record_path_failure(guest.guest_id, 5, recovery_targets, reachable)
_expect(first[&"failure_count"] == 1 and not first[&"recovery_started"], "first path failure enters REPLANNING")
_expect(second[&"failure_count"] == 2 and not second[&"recovery_started"], "second path failure remains recoverable")
var third := service.record_path_failure(guest.guest_id, 6, recovery_targets, reachable)
_expect(third[&"ok"] and third[&"recovery_started"], "SYS-GST-007 starts safe recovery on the third path failure")
_expect(guest.state == GuestRuntimeType.State.LEAVING, "third failure cannot leave the guest stuck in REPLANNING")
_expect(guest.recovery_kind == &"entrance" and guest.recovery_target_cell == Vector3i(5, 0, 0), "recovery tries guidance, entrance, then emergency exit in order")
_expect(service.get_diagnostics()[-1][&"code"] == &"GUEST_PATH_RECOVERY", "safe path recovery emits a diagnostic record")
var fallback_service := _service_with_definitions()
var fallback_guest := _moving_guest(fallback_service, &"guest_fallback")
fallback_service.record_path_failure(fallback_guest.guest_id, 4)
fallback_service.record_path_failure(fallback_guest.guest_id, 5)
var fallback := fallback_service.record_path_failure(fallback_guest.guest_id, 6, [], Callable())
_expect(fallback[&"recovery_kind"] == &"emergency_despawn", "no-route case uses a virtual emergency egress rather than deadlocking")
func _test_exit_persistence_boundary() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_exit", &"adventurer", 0, {
&"state": GuestRuntimeType.State.LEAVING,
&"wallet_gold": 777,
&"debt_gold": 90,
&"satisfaction_total": 180,
&"satisfaction_samples": 2,
&"unresolved_incident_ids": [&"incident_b", &"incident_a"],
&"outstanding_reservation_ids": [&"reservation_1"],
&"planned_exit_at": 500,
})
var result := service.despawn_guest(guest.guest_id, 1)
_expect(result[&"ok"] and not service.has_guest(guest.guest_id), "leaving guest runtime is released after exit")
var record := service.get_persistent_record(guest.guest_id)
_expect(record[&"debt_gold"] == 90, "SYS-GST-006 preserves unresolved debt")
_expect(record[&"unresolved_incident_ids"] == ["incident_a", "incident_b"], "SYS-GST-006 preserves unresolved incidents")
_expect(record[&"outstanding_reservation_ids"] == ["reservation_1"], "SYS-GST-006 preserves outstanding reservations")
_expect(not record.has(&"wallet_gold") and not record.has(&"needs") and not record.has(&"satisfaction_total"), "ordinary transient guest state does not survive despawn")
func _test_snapshot_round_trip_and_atomic_rejection() -> void:
var original := _service_with_definitions()
var moving := _moving_guest(original, &"guest_saved_moving")
var queued := _moving_guest(original, &"guest_saved_queue")
original.enter_queue(queued.guest_id, &"cafeteria", &"request_saved", 4, 12)
moving.path_failure_count = 1
moving.suspicion = 33
original.advance_one_minute(20)
var snapshot := original.create_snapshot()
var restored := GuestServiceType.new()
_expect(restored.restore_snapshot(snapshot), "GuestService restores a valid snapshot")
_expect(restored.create_snapshot() == snapshot, "GuestService snapshot round trip is byte-structure equivalent")
_expect(restored.validate_invariants()[&"valid"], "restored guest state satisfies every invariant")
var before_corrupt := restored.create_snapshot()
var corrupt_need := snapshot.duplicate(true)
corrupt_need[&"guests"][0][&"needs"]["hunger"] = 101
_expect(not restored.restore_snapshot(corrupt_need), "out-of-range Need rejects snapshot restore")
_expect(restored.create_snapshot() == before_corrupt, "rejected Need snapshot leaves live state untouched")
var duplicate_guest := snapshot.duplicate(true)
duplicate_guest[&"guests"].append(duplicate_guest[&"guests"][0].duplicate(true))
_expect(not restored.restore_snapshot(duplicate_guest), "duplicate guest ID rejects snapshot restore")
_expect(restored.create_snapshot() == before_corrupt, "duplicate rejection is also atomic")
var orphan_type := snapshot.duplicate(true)
orphan_type[&"guests"][0][&"guest_type_id"] = "missing_type"
_expect(not restored.restore_snapshot(orphan_type), "unknown GuestDefinition reference rejects restore")
_expect(restored.create_snapshot() == before_corrupt, "orphan-reference rejection preserves live state")
func _test_v1_need_migration_and_schema_rejection() -> void:
var source := _service_with_definitions()
source.spawn_guest(&"guest_legacy", &"verified_noble", 0, {
&"needs": {&"hunger": 11, &"sleep": 22, &"hygiene": 0, &"medical": 44, &"leisure": 55, &"intimacy": 66},
&"planned_exit_at": 500,
})
var canonical := source.create_snapshot()
var legacy := canonical.duplicate(true)
legacy[&"version"] = 1
for definition: Dictionary in legacy[&"definitions"]:
var rates: Dictionary = definition[&"need_rates"]
definition[&"need_rates"] = {
&"hunger": rates["hunger"],
&"rest": rates["sleep"],
&"fun": rates["leisure"],
&"social": rates["intimacy"],
&"safety": rates["medical"],
}
for guest_snapshot: Dictionary in legacy[&"guests"]:
var needs: Dictionary = guest_snapshot[&"needs"]
guest_snapshot[&"needs"] = {
&"hunger": needs["hunger"],
&"rest": needs["sleep"],
&"fun": needs["leisure"],
&"social": needs["intimacy"],
&"safety": needs["medical"],
}
var migrated := GuestServiceType.new()
_expect(migrated.restore_snapshot(legacy), "GuestService deterministically migrates an exact v1 five-Need snapshot")
var migrated_snapshot := migrated.create_snapshot()
var migrated_needs: Dictionary = migrated_snapshot[&"guests"][0][&"needs"]
_expect(migrated_snapshot[&"version"] == 2, "v1 migration emits only the current GuestService schema")
_expect(migrated_needs == {
"hunger": 11,
"hygiene": 0,
"intimacy": 66,
"leisure": 55,
"medical": 44,
"sleep": 22,
}, "v1 rest/fun/social/safety map to sleep/leisure/intimacy/medical with Hygiene zero")
_expect(migrated.validate_invariants()[&"valid"], "migrated six-Need graph satisfies current invariants")
var stable_before := migrated.create_snapshot()
var missing := canonical.duplicate(true)
missing[&"guests"][0][&"needs"].erase("hygiene")
_expect(not migrated.restore_snapshot(missing), "current snapshots reject a missing canonical Need")
_expect(migrated.create_snapshot() == stable_before, "missing-Need rejection is atomic")
var unknown := canonical.duplicate(true)
unknown[&"guests"][0][&"needs"]["safety"] = 1
_expect(not migrated.restore_snapshot(unknown), "current snapshots reject unknown legacy Need keys")
_expect(migrated.create_snapshot() == stable_before, "unknown-Need rejection is atomic")
var wrong_type := canonical.duplicate(true)
wrong_type[&"definitions"][0][&"need_rates"]["sleep"] = "1"
_expect(not migrated.restore_snapshot(wrong_type), "current definition Need rates reject implicit integer coercion")
_expect(migrated.create_snapshot() == stable_before, "definition type rejection is atomic")
var mixed_v1 := legacy.duplicate(true)
mixed_v1[&"guests"][0][&"needs"]["sleep"] = 22
_expect(not migrated.restore_snapshot(mixed_v1), "v1 migration rejects mixed legacy/current Need maps")
_expect(migrated.create_snapshot() == stable_before, "mixed-schema rejection preserves live state")
func _test_deterministic_decision_snapshot() -> void:
var first := _deterministic_run()
var second := _deterministic_run()
_expect(first == second, "same guest definitions and candidates produce an identical authoritative snapshot")
func _deterministic_run() -> Dictionary:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_determinism", &"adventurer", 0, {
&"wallet_gold": 1_000,
&"planned_exit_at": 1_000,
&"state": GuestRuntimeType.State.DECIDING,
&"needs": {&"hunger": 80, &"leisure": 50},
})
var candidates: Array = [
_candidate(&"z_meal", {&"need_relief": {&"hunger": 60}, &"facility_tags": [&"meal"], &"travel_minutes": 4}),
_candidate(&"a_meal", {&"need_relief": {&"hunger": 60}, &"facility_tags": [&"meal"], &"travel_minutes": 4}),
_candidate(&"theater", {&"need_relief": {&"leisure": 50}, &"facility_tags": [&"leisure"], &"travel_minutes": 3}),
]
service.decide(guest.guest_id, candidates, 1, {&"absolute_minute": 1})
service.update_needs(guest.guest_id, {&"dirt": 2})
return service.create_snapshot()
func _service_with_definitions() -> GuestService:
var service := GuestServiceType.new()
service.register_definition(GuestDefinitionType.new().configure(&"adventurer", {
&"tags": [&"adventurer", &"general"],
&"wealth_min": 100,
&"wealth_max": 500,
&"base_patience": 45,
&"risk_tolerance": 80,
&"preference_weights": {&"meal": 8, &"leisure": 3},
&"group_size_weights": {1: 100},
&"legal_sensitivity": 60,
&"quality_sensitivity": 70,
&"adult_verified": false,
&"need_rates": {&"hunger": 3, &"sleep": -2, &"leisure": 1},
&"credit_limit_gold": 100,
&"minimum_quality": 40,
}))
service.register_definition(GuestDefinitionType.new().configure(&"verified_noble", {
&"tags": [&"wealthy", &"vip"],
&"wealth_min": 1_000,
&"wealth_max": 2_000,
&"base_patience": 20,
&"risk_tolerance": 40,
&"legal_sensitivity": 20,
&"adult_verified": true,
&"minimum_quality": 40,
}))
return service
func _moving_guest(service: GuestService, guest_id: StringName) -> GuestRuntime:
var guest := service.spawn_guest(guest_id, &"adventurer", 0, {
&"wallet_gold": 500,
&"planned_exit_at": 1_000,
})
service.transition_guest(guest_id, GuestRuntimeType.State.ENTERING, 1)
service.transition_guest(guest_id, GuestRuntimeType.State.DECIDING, 2)
service.transition_guest(guest_id, GuestRuntimeType.State.MOVING, 3)
return guest
func _candidate(facility_id: StringName, patch: Dictionary = {}) -> Dictionary:
var candidate := {
&"facility_id": facility_id,
&"is_open": true,
&"reachable": true,
&"price_gold": 100,
&"wait_minutes": 10,
&"risk_level": 10,
&"illegal_level": 10,
&"expected_quality": 60,
&"need_relief": 50,
}
for raw_key: Variant in patch:
candidate[raw_key] = patch[raw_key]
return candidate
func _candidate_ids(candidates: Array[Dictionary]) -> Array[StringName]:
var result: Array[StringName] = []
for candidate: Dictionary in candidates:
result.append(StringName(candidate[&"facility_id"]))
return result
func _rejection_map(rejections: Array[Dictionary]) -> Dictionary:
var result: Dictionary = {}
for rejection: Dictionary in rejections:
result[StringName(rejection[&"facility_id"])] = StringName(rejection[&"reason"])
return result
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://b5un0csk8il6t
+240
View File
@@ -0,0 +1,240 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
var _facility_id: StringName
var _facility_inventory_id: StringName
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: integrated Guest and Staff runtime cycle")
_reset_runtime()
_build_cafeteria()
_register_staff_and_run_task()
_run_autonomous_guest_service_with_replay()
_run_long_horizon_replay()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime() -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, 3303)
_clock.set_paused(true)
_expect(report[&"valid"], "runtime initializes S3 actor services")
_expect(_runtime.guest_service.get_definitions().size() == 35, "runtime preserves three legacy archetypes and registers 32 MVP catalog archetypes")
_expect(_runtime.guest_service.current_minute == _clock.absolute_minute, "Guest cadence starts aligned to the kernel")
_expect(_runtime.staff_service.current_minute == _clock.absolute_minute, "Staff cadence starts aligned to the kernel")
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _build_cafeteria() -> void:
var build = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT)},
&"player", &"", 50, -1, &"s3.runtime.build"
)
_expect(build.is_success(), "cafeteria construction command succeeds")
_clock.advance_minutes(180)
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "construction completes before actor integration")
_facility_id = StringName(facilities[0][&"instance_id"])
var facility = _runtime.facility_service.get_instance(_facility_id)
_facility_inventory_id = facility.local_inventory_id
_expect(facility.state == FacilityInstance.State.NO_STAFF, "unassigned S3 facility starts in NO_STAFF")
func _register_staff_and_run_task() -> void:
var common_options := {
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"state": StaffRuntime.State.OFF_DUTY,
&"permitted_task_tags": [&"service", &"work"],
&"equipment_tags": [&"uniform"],
}
var cook_options: Dictionary = common_options.duplicate(true)
cook_options[&"role_tags"] = [&"cook"]
cook_options[&"skills"] = {&"cooking": 80, &"operation": 30}
var server_options: Dictionary = common_options.duplicate(true)
server_options[&"role_tags"] = [&"server"]
server_options[&"skills"] = {&"service": 75, &"operation": 20}
var cook = _runtime.submit_command(
&"register_staff",
{&"staff_id": &"cook_aya", &"role_id": &"cook", &"options": cook_options},
&"manager", &"cook_aya", 20, -1, &"s3.runtime.staff.cook"
)
var server = _runtime.submit_command(
&"register_staff",
{&"staff_id": &"server_ren", &"role_id": &"server", &"options": server_options},
&"manager", &"server_ren", 20, -1, &"s3.runtime.staff.server"
)
_expect(cook.is_success() and server.is_success(), "two StaffRuntime records register through commands")
var start_minute := posmod(_clock.absolute_minute, 1_440)
for staff_id: StringName in [&"cook_aya", &"server_ren"]:
var shift = _runtime.submit_command(
&"create_staff_shift",
{
&"staff_id": staff_id,
&"template_id": &"FLEX",
&"start_day": _clock.absolute_minute / 1_440,
&"start_minute_of_day": start_minute,
&"end_minute_of_day": start_minute + 600,
&"facility_id": _facility_id,
},
&"manager", staff_id, 20, -1, StringName("s3.runtime.shift.%s" % staff_id)
)
_expect(shift.is_success(), "Flex Shift registers for %s" % staff_id)
_clock.advance_one_minute()
var facility = _runtime.facility_service.get_instance(_facility_id)
_expect(facility.active_staff_ids == [&"cook_aya", &"server_ren"], "on-duty Staff project into FacilityService in stable ID order")
_expect(facility.state == FacilityInstance.State.OPEN, "required S3 staffing opens the cafeteria")
var tool = _runtime.submit_command(
&"add_inventory_stack",
{
&"owner_inventory_id": _facility_inventory_id,
&"item_id": &"tool/cleaning_kit",
&"quantity": 1,
},
&"manager", _facility_id, 10, -1, &"s3.runtime.tool"
)
_expect(tool.is_success(), "facility receives a reservable Staff task tool")
var task_result = _runtime.submit_command(
&"create_staff_task",
{
&"task_id": &"clean_cafeteria",
&"task_type": &"clean",
&"priority": 70,
&"options": {
&"target_entity_id": _facility_id,
&"target_cell": Vector3i(-1, 0, WorldService.LAYER_FRONT),
&"required_role_tags": [&"server"],
&"required_skill_min": {&"service": 10},
&"required_items": {&"tool/cleaning_kit": 1},
&"task_tags": [&"service"],
&"estimated_minutes": 2,
},
},
&"manager", _facility_id, 10, -1, &"s3.runtime.task"
)
_expect(task_result.is_success(), "Staff task enters the runtime dispatcher")
_clock.advance_minutes(8)
var task = _runtime.staff_service.get_task(&"clean_cafeteria")
_expect(task.state == StaffTask.State.COMPLETED, "runtime automatically dispatches, begins and completes the Staff task")
_expect(_runtime.inventory_service.available_quantity(_facility_inventory_id, &"tool/cleaning_kit") == 1, "completed task releases its atomic tool Reservation")
_expect(_runtime.staff_service.validate_invariants()[&"ok"], "integrated Staff task graph preserves bidirectional invariants")
func _run_autonomous_guest_service_with_replay() -> void:
var ingredient = _runtime.submit_command(
&"add_inventory_stack",
{
&"owner_inventory_id": _facility_inventory_id,
&"item_id": &"ingredient/basic_meal",
&"quantity": 1,
&"quality": 80,
},
&"manager", _facility_id, 10, -1, &"s3.runtime.ingredient"
)
_expect(ingredient.is_success(), "one service recipe is stocked")
var spawn = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"guest_mio",
&"guest_type_id": &"traveler",
&"gold": 150,
&"options": {
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"needs": {&"hunger": 90, &"sleep": 20, &"hygiene": 20, &"medical": 0, &"leisure": 20, &"intimacy": 0},
&"planned_exit_at": _clock.absolute_minute + 240,
},
},
&"world", &"guest_mio", 10, -1, &"s3.runtime.guest"
)
_expect(spawn.is_success(), "spawn_guest atomically creates GuestRuntime and ledger wallet")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_mio") == 150, "Guest wallet is authoritative in EconomyService")
var active_service: ServiceInstance = null
for _minute in range(10):
_clock.advance_one_minute()
var active: Array = _runtime.facility_service.get_active_services(_facility_id)
if not active.is_empty():
active_service = active[0]
break
_expect(active_service != null, "autonomous Guest reaches the facility queue and starts service")
if active_service == null:
return
var guest = _runtime.guest_service.get_guest(&"guest_mio")
_expect(guest.state == GuestRuntime.State.RECEIVING_SERVICE, "Facility service start atomically updates Guest state")
_expect(guest.current_service_id == active_service.service_id, "Guest and Facility reference the same active service")
var mid_snapshot: Dictionary = _runtime.create_snapshot()
var mid_digest := SimulationDigest.compute(mid_snapshot)
var duration := active_service.duration_minutes
_clock.advance_minutes(duration)
var completed = _runtime.facility_service.get_service(active_service.service_id)
guest = _runtime.guest_service.get_guest(&"guest_mio")
_expect(completed != null and completed.state == ServiceInstance.State.COMPLETED, "service completes after its immutable duration")
_expect(guest != null and guest.satisfaction_samples == 1, "completion records exactly one Guest satisfaction sample")
_expect(guest != null and int(guest.needs[&"hunger"]) < 40, "cafeteria completion applies its Need relief")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_mio") == 115, "Guest pays the quoted service price once")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4235, "service revenue reaches authoritative player cash")
_expect(_runtime.inventory_service.total_quantity(_facility_inventory_id, &"ingredient/basic_meal") == 0, "service consumes exactly one reserved recipe input")
var final_digest: String = _runtime.latest_digest
_expect(_runtime.restore_snapshot(mid_snapshot), "mid-service S3 snapshot restores")
_expect(_runtime.latest_digest == mid_digest, "S3 restore reproduces the exact mid-service digest")
_clock.advance_minutes(duration)
_expect(_runtime.latest_digest == final_digest, "replayed Guest, Staff, ledger and event transitions reproduce exactly")
_expect(_runtime.economy_service.get_entries().filter(
func(entry: LedgerEntry) -> bool: return entry.category == &"SERVICE_SALE"
).size() == 1, "replay records the service sale exactly once")
func _run_long_horizon_replay() -> void:
var horizon_snapshot: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(1_440)
var first_digest: String = _runtime.latest_digest
var first_snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.guest_service.validate_invariants()[&"valid"], "24-hour Guest graph has no orphan or illegal state")
_expect(_runtime.staff_service.validate_invariants()[&"ok"], "24-hour Staff graph has no orphan reservation")
_expect(_runtime.inventory_service.validate_invariants()[&"ok"], "24-hour actor simulation preserves Inventory invariants")
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "24-hour actor simulation keeps the ledger balanced")
_expect(_runtime.restore_snapshot(horizon_snapshot), "long-horizon starting point restores")
_clock.advance_minutes(1_440)
_expect(_runtime.latest_digest == first_digest, "24-hour actor simulation is deterministic after restore")
_expect(_runtime.create_snapshot() == first_snapshot, "24-hour replay reproduces the complete authoritative snapshot")
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://0sc8xvrpbuoa
+415
View File
@@ -0,0 +1,415 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: runtime actor hardening regressions")
_test_service_affordability_is_atomic()
_test_guest_moves_one_cell_per_minute()
_test_unreachable_staff_task_is_not_assigned()
_test_shift_end_interrupts_and_fails_service()
_test_corrupt_cross_domain_snapshots_are_atomic()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime(seed: int) -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
_clock.set_paused(true)
_expect(bool(report.get(&"valid", false)), "runtime initializes for hardening scenario %d" % seed)
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_service_affordability_is_atomic() -> void:
_reset_runtime(3311)
var facility: FacilityInstance = _build_cafeteria()
if facility == null:
return
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 3, &"hardening.stock.affordability"), "affordability scenario stocks three meals")
_expect(_register_wallet(&"low_funds", 20, &"hardening.wallet.low"), "low-balance wallet registers")
var available_before: int = _runtime.inventory_service.available_quantity(
facility.local_inventory_id, &"ingredient/basic_meal"
)
var reservations_before: int = _runtime.inventory_service.get_reservations(true).size()
var low_result = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"request_low_funds",
&"facility_id": facility.instance_id,
&"guest_ids": [&"low_funds"],
&"service_type": &"default",
&"options": {&"quoted_price": 35},
},
&"low_funds", facility.instance_id, 10, -1, &"hardening.service.low"
)
_expect(not low_result.is_success(), "service request exceeding the wallet balance is rejected")
_expect(_runtime.facility_service.get_request(&"request_low_funds") == null, "rejected low-balance request leaves no Facility request")
_expect(_runtime.facility_service.get_queue(facility.instance_id).is_empty(), "rejected low-balance request leaves no queue entry")
_expect(
_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == available_before,
"rejected low-balance request does not reserve stock"
)
_expect(_runtime.inventory_service.get_reservations(true).size() == reservations_before, "rejected low-balance request creates no active Reservation")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:low_funds") == 20, "rejected low-balance request leaves the wallet unchanged")
_expect(_register_wallet(&"committed_guest", 50, &"hardening.wallet.commitment"), "commitment-test wallet registers")
var first_result = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"request_commitment_a",
&"facility_id": facility.instance_id,
&"guest_ids": [&"committed_guest"],
&"service_type": &"default",
&"options": {&"quoted_price": 30},
},
&"committed_guest", facility.instance_id, 10, -1, &"hardening.service.commitment.a"
)
_expect(first_result.is_success(), "first affordable service commitment is accepted")
var available_after_first: int = _runtime.inventory_service.available_quantity(
facility.local_inventory_id, &"ingredient/basic_meal"
)
var active_after_first: int = _runtime.inventory_service.get_reservations(true).size()
var second_result = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"request_commitment_b",
&"facility_id": facility.instance_id,
&"guest_ids": [&"committed_guest"],
&"service_type": &"default",
&"options": {&"quoted_price": 25},
},
&"committed_guest", facility.instance_id, 10, -1, &"hardening.service.commitment.b"
)
_expect(not second_result.is_success(), "combined service commitments exceeding the wallet are rejected")
var queue: Array = _runtime.facility_service.get_queue(facility.instance_id)
_expect(queue.size() == 1 and queue[0].request_id == &"request_commitment_a", "over-commit rejection preserves only the first queue entry")
_expect(_runtime.facility_service.get_request(&"request_commitment_b") == null, "over-commit rejection leaves no second request")
_expect(
_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == available_after_first,
"over-commit rejection does not reserve another meal"
)
_expect(_runtime.inventory_service.get_reservations(true).size() == active_after_first, "over-commit rejection creates no additional Reservation")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:committed_guest") == 50, "queued commitments do not debit the wallet prematurely")
func _test_guest_moves_one_cell_per_minute() -> void:
_reset_runtime(3312)
var facility: FacilityInstance = _build_cafeteria()
if facility == null:
return
var opened = _runtime.submit_command(
&"set_facility_conditions",
{&"facility_id": facility.instance_id, &"conditions": {&"active_staff_ids": [&"cook_projection", &"server_projection"]}},
&"manager", facility.instance_id, 10, -1, &"hardening.guest.open"
)
_expect(opened.is_success() and facility.state == FacilityInstance.State.OPEN, "projected staffing opens the movement target")
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 1, &"hardening.stock.path"), "movement target has a service input")
var spawn = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"path_guest",
&"guest_type_id": &"traveler",
&"gold": 100,
&"options": {
&"current_cell": Vector3i(8, 0, WorldService.LAYER_FRONT),
&"needs": {&"hunger": 100, &"sleep": 0, &"hygiene": 0, &"medical": 0, &"leisure": 0, &"intimacy": 0},
&"planned_exit_at": _clock.absolute_minute + 240,
},
},
&"world", &"path_guest", 10, -1, &"hardening.guest.spawn"
)
_expect(spawn.is_success(), "distant Guest spawns for path progression")
var guest: GuestRuntime = null
for _minute in range(8):
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"path_guest")
if guest != null and guest.state == GuestRuntime.State.MOVING:
break
_expect(guest != null and guest.state == GuestRuntime.State.MOVING, "autonomous Guest selects the reachable cafeteria")
if guest == null or guest.state != GuestRuntime.State.MOVING:
return
var target_cell: Vector3i = guest.path_handle.get(&"target_cell", guest.current_cell)
var planned_path: Array = _runtime.path_service.find_path(guest.current_cell, target_cell)
_expect(planned_path.size() >= 4, "movement regression uses a path longer than one minute")
var first_cell := guest.current_cell
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"path_guest")
var second_cell := guest.current_cell
_expect(_cell_distance(first_cell, second_cell) == 1, "Guest advances exactly one graph cell in one minute")
_expect(second_cell != target_cell, "Guest does not teleport to a distant target in one minute")
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"path_guest")
_expect(_cell_distance(second_cell, guest.current_cell) == 1, "Guest continues along the path at one cell per minute")
func _test_unreachable_staff_task_is_not_assigned() -> void:
_reset_runtime(3313)
var registered = _runtime.submit_command(
&"register_staff",
{
&"staff_id": &"isolated_worker",
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.AVAILABLE,
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
&"skills": {&"operation": 50},
},
},
&"manager", &"isolated_worker", 10, -1, &"hardening.staff.unreachable.register"
)
_expect(registered.is_success(), "available Staff registers for unreachable-task test")
var created = _runtime.submit_command(
&"create_staff_task",
{
&"task_id": &"unreachable_task",
&"task_type": &"inspect",
&"priority": 95,
&"options": {
&"target_cell": Vector3i(0, 5, WorldService.LAYER_FRONT),
&"required_role_tags": [&"worker"],
&"required_skill_min": {&"operation": 10},
&"task_tags": [&"work"],
&"estimated_minutes": 5,
},
},
&"manager", &"unreachable_task", 10, -1, &"hardening.staff.unreachable.task"
)
_expect(created.is_success(), "unreachable StaffTask registers")
_clock.advance_one_minute()
var task: StaffTask = _runtime.staff_service.get_task(&"unreachable_task")
var staff: StaffRuntime = _runtime.staff_service.get_staff(&"isolated_worker")
_expect(task != null and task.state == StaffTask.State.OPEN, "unreachable StaffTask remains OPEN")
_expect(task != null and task.assigned_staff_id.is_empty(), "unreachable StaffTask has no assigned Staff")
_expect(staff != null and staff.current_task_id.is_empty(), "unreachable assignment does not reserve the Staff member")
var unassigned: Array = _runtime.staff_service.last_dispatch_report.get(&"unassigned", [])
_expect(not unassigned.is_empty() and unassigned[0].get(&"reason", &"") == &"NO_ELIGIBLE_STAFF", "dispatcher reports unreachable work as having no eligible Staff")
_expect(_runtime.inventory_service.get_reservations(true).is_empty(), "unreachable work creates no inventory Reservation")
func _test_shift_end_interrupts_and_fails_service() -> void:
_reset_runtime(3314)
var facility: FacilityInstance = _build_cafeteria()
if facility == null:
return
for staff_id: StringName in [&"short_cook", &"short_server"]:
var registered = _runtime.submit_command(
&"register_staff",
{
&"staff_id": staff_id,
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.OFF_DUTY,
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
},
},
&"manager", staff_id, 10, -1, StringName("hardening.shift.staff.%s" % staff_id)
)
_expect(registered.is_success(), "short-shift Staff %s registers" % staff_id)
var start_day: int = _clock.absolute_minute / 1_440
var start_minute: int = posmod(_clock.absolute_minute, 1_440)
var end_minute: int = start_minute + 5
for staff_id: StringName in [&"short_cook", &"short_server"]:
var shifted = _runtime.submit_command(
&"create_staff_shift",
{
&"shift_id": StringName("shift_%s" % staff_id),
&"staff_id": staff_id,
&"template_id": &"FLEX",
&"start_day": start_day,
&"start_minute_of_day": start_minute,
&"end_minute_of_day": end_minute,
&"facility_id": facility.instance_id,
},
&"manager", staff_id, 10, -1, StringName("hardening.shift.create.%s" % staff_id)
)
_expect(shifted.is_success(), "short Shift for %s registers" % staff_id)
_clock.advance_one_minute()
_expect(facility.active_staff_ids == [&"short_cook", &"short_server"], "active short-shift Staff project into the Facility")
_expect(facility.state == FacilityInstance.State.OPEN, "short-shift staffing opens the Facility")
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 1, &"hardening.shift.stock"), "interruption scenario stocks one meal")
_expect(_register_wallet(&"shift_guest", 100, &"hardening.shift.wallet"), "service payer registers for Shift interruption")
var requested = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"shift_service_request",
&"facility_id": facility.instance_id,
&"guest_ids": [&"shift_guest"],
&"service_type": &"default",
&"options": {&"quoted_price": 35},
},
&"shift_guest", facility.instance_id, 10, -1, &"hardening.shift.service"
)
_expect(requested.is_success(), "service queues while short Shift is staffed")
_clock.advance_one_minute()
var active: Array = _runtime.facility_service.get_active_services(facility.instance_id)
_expect(active.size() == 1, "service starts before the short Shift ends")
if active.is_empty():
return
var service: ServiceInstance = active[0]
var service_id: StringName = service.service_id
var shift_end_absolute: int = start_day * 1_440 + end_minute
_clock.advance_minutes(maxi(shift_end_absolute - _clock.absolute_minute, 0))
_expect(facility.active_staff_ids.is_empty(), "Shift end removes all projected active Staff")
_expect(facility.state == FacilityInstance.State.NO_STAFF, "Shift end moves the Facility to NO_STAFF")
_clock.advance_one_minute()
service = _runtime.facility_service.get_service(service_id)
_expect(service != null and service.state == ServiceInstance.State.WAITING_FOR_STAFF, "active service waits for replacement Staff after Shift end")
var grace_deadline: int = service.staff_grace_deadline if service != null else _clock.absolute_minute
_expect(grace_deadline == _clock.absolute_minute + FacilityService.STAFF_REPLACEMENT_GRACE_MINUTES, "service records the deterministic Staff replacement grace deadline")
_clock.advance_minutes(maxi(grace_deadline - _clock.absolute_minute, 0))
service = _runtime.facility_service.get_service(service_id)
_expect(service != null and service.state == ServiceInstance.State.FAILED, "service fails when no replacement arrives before grace expiry")
_expect(service != null and service.failure_reason == FacilityService.INTERRUPTION_STAFF_UNAVAILABLE, "failed service preserves the Staff-unavailable reason")
_expect(_runtime.facility_service.get_active_services(facility.instance_id).is_empty(), "failed service leaves no active Facility slot")
_expect(_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == 1, "failed service releases its meal Reservation")
func _test_corrupt_cross_domain_snapshots_are_atomic() -> void:
_reset_runtime(3315)
var registered = _runtime.submit_command(
&"register_staff",
{
&"staff_id": &"snapshot_worker",
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.AVAILABLE,
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
},
},
&"manager", &"snapshot_worker", 10, -1, &"hardening.snapshot.staff"
)
_expect(registered.is_success(), "snapshot Staff registers")
_expect(_stock(&"staff.supplies", &"tool/snapshot_kit", 1, &"hardening.snapshot.stock"), "snapshot task tool is stocked")
var created = _runtime.submit_command(
&"create_staff_task",
{
&"task_id": &"snapshot_task",
&"task_type": &"inspect",
&"priority": 90,
&"options": {
&"target_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"required_role_tags": [&"worker"],
&"required_items": {&"tool/snapshot_kit": 1},
&"task_tags": [&"work"],
&"estimated_minutes": 10,
},
},
&"manager", &"snapshot_task", 10, -1, &"hardening.snapshot.task"
)
_expect(created.is_success(), "snapshot StaffTask registers")
_clock.advance_one_minute()
var task: StaffTask = _runtime.staff_service.get_task(&"snapshot_task")
_expect(task != null and task.state == StaffTask.State.WORKING and task.reservation_ids.size() == 1, "snapshot captures a working Task with an active Reservation")
var valid_snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(valid_snapshot), "valid hardening snapshot restores before corruption checks")
var before_corrupt: Dictionary = _runtime.create_snapshot()
var before_digest: String = SimulationDigest.compute(before_corrupt)
var minute_corrupt: Dictionary = before_corrupt.duplicate(true)
var runtime_data: Dictionary = minute_corrupt[&"systems"][&"runtime"]
runtime_data[&"facilities"][&"current_minute"] = _clock.absolute_minute + 1
_expect(not _runtime.restore_snapshot(minute_corrupt), "snapshot with FacilityService minute diverging from the clock is rejected")
_expect(_runtime.create_snapshot() == before_corrupt, "minute-mismatch restore rejection is fully atomic")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before_digest, "minute-mismatch rejection preserves the live digest")
var reservation_corrupt: Dictionary = before_corrupt.duplicate(true)
var task_entries: Array = reservation_corrupt[&"systems"][&"runtime"][&"staff"][&"tasks"]
for raw_task: Dictionary in task_entries:
if StringName(raw_task.get(&"task_id", "")) == &"snapshot_task":
raw_task[&"reservation_ids"] = ["reservation_missing"]
_expect(not _runtime.restore_snapshot(reservation_corrupt), "snapshot with a dangling StaffTask Reservation reference is rejected")
_expect(_runtime.create_snapshot() == before_corrupt, "dangling-Reservation restore rejection is fully atomic")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before_digest, "dangling-Reservation rejection preserves the live digest")
func _build_cafeteria() -> FacilityInstance:
var build = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT)},
&"player", &"", 50, -1, StringName("hardening.build.%d" % _clock.absolute_minute)
)
_expect(build.is_success(), "cafeteria construction starts")
_clock.advance_minutes(180)
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "cafeteria construction completes")
if facilities.size() != 1:
return null
var facility_id := StringName(facilities[0].get(&"instance_id", ""))
var facility: FacilityInstance = _runtime.facility_service.get_instance(facility_id)
_expect(facility != null, "completed cafeteria registers with FacilityService")
return facility
func _stock(
owner_inventory_id: StringName,
item_id: StringName,
quantity: int,
idempotency_key: StringName
) -> bool:
var result = _runtime.submit_command(
&"add_inventory_stack",
{
&"owner_inventory_id": owner_inventory_id,
&"item_id": item_id,
&"quantity": quantity,
},
&"manager", owner_inventory_id, 10, -1, idempotency_key
)
return result.is_success()
func _register_wallet(guest_id: StringName, gold: int, idempotency_key: StringName) -> bool:
var result = _runtime.submit_command(
&"register_guest_wallet",
{&"guest_id": guest_id, &"gold": gold},
&"manager", guest_id, 10, -1, idempotency_key
)
return result.is_success()
func _cell_distance(a: Vector3i, b: Vector3i) -> int:
var delta := a - b
return absi(delta.x) + absi(delta.y) + absi(delta.z)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bx62i83nwxfbh
+619
View File
@@ -0,0 +1,619 @@
extends SceneTree
const POST_V2_RUNTIME_KEYS := [
&"movement_credits",
&"catalog_manifest",
&"progression",
&"hired_staff_candidate_ids",
&"operations",
&"excavation",
&"spatial_subslots",
&"route_projection",
&"facility_infrastructure",
]
const POST_V2_EVENT_TYPES := [
&"PATH_ACTOR_STUCK",
&"WORLD_TREE_RANK_ADVANCED",
&"STAFF_CANDIDATE_HIRED",
&"GUEST_AUTO_ARRIVED",
&"FACILITY_INFRASTRUCTURE_PROJECTED",
&"EXCAVATION_COMPLETED",
]
const POST_V2_COMMAND_TYPES := [
&"set_world_tree_rank",
&"update_progression_metrics",
&"complete_progression_quest",
&"hire_staff_candidate",
&"acknowledge_operations_pause",
&"start_excavation",
&"pause_excavation",
&"resume_excavation",
]
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: Runtime Path traffic integration")
_test_namespaced_lifecycle_and_density()
_test_congestion_credit_and_alternate_route()
_test_occupied_cell_graph_rebuild_is_rejected()
_test_stuck_guest_and_staff_recovery()
_test_event_recovery_path_transaction()
_test_v6_snapshot_crosslinks_and_legacy_v2()
_test_dispatcher_request_ownership_crosslinks()
_test_twenty_four_hour_projection_integrity()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime(seed: int) -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
_clock.set_paused(true)
_expect(bool(report.get(&"valid", false)), "Runtime initializes for Path traffic fixture %d" % seed)
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_namespaced_lifecycle_and_density() -> void:
_reset_runtime(8_101)
var cell := Vector3i(-4, 0, WorldService.LAYER_FRONT)
_expect(_spawn_moving_guest(&"shared", cell, Vector3i(4, 0, 0)), "Guest with shared raw id spawns")
_expect(_register_idle_staff(&"shared", cell), "Staff with the same raw id registers")
_expect(
_runtime.path_service.has_actor_projection(&"guest:shared")
and _runtime.path_service.has_actor_projection(&"staff:shared"),
"Guest and Staff use collision-free namespaced Path actor ids"
)
_expect(
_runtime.path_service.get_current_density(cell) == 2,
"spawn/register projects both actors into the authoritative cell density"
)
var guest: GuestRuntime = _runtime.guest_service.get_guest(&"shared")
_runtime.guest_service.begin_leaving(&"shared", _clock.absolute_minute, &"TEST_EXIT")
_runtime.guest_service.despawn_guest(&"shared", _clock.absolute_minute)
_expect(guest != null and not _runtime.path_service.has_actor_projection(&"guest:shared"), "Guest despawn removes only its namespaced projection")
_expect(
_runtime.path_service.has_actor_projection(&"staff:shared")
and _runtime.path_service.get_current_density(cell) == 1,
"Guest despawn preserves the colliding raw Staff id and lowers density once"
)
_expect(_runtime.get_actor_movement_credit(&"guest:shared").is_empty(), "Guest despawn removes movement credit")
_runtime.staff_service.remove_staff(&"shared", &"TEST_REMOVE", true, _clock.absolute_minute)
_expect(
not _runtime.path_service.has_actor_projection(&"staff:shared")
and _runtime.path_service.get_current_density(cell) == 0,
"Staff removal cleans projection and density without going negative"
)
var invalid_spawn: CommandResult = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"invalid_cell",
&"guest_type_id": &"traveler",
&"options": {&"current_cell": Vector3i(0, 2, 0)},
},
&"world", &"invalid_cell", 10, -1, &"path.invalid.spawn"
)
_expect(
not invalid_spawn.is_success()
and not _runtime.guest_service.has_guest(&"invalid_cell")
and not _runtime.path_service.has_actor_projection(&"guest:invalid_cell")
and _runtime.get_actor_movement_credit(&"guest:invalid_cell").is_empty()
and not _has_recent_event(&"GUEST_SPAWNED", &"invalid_cell"),
"failed spawn leaves Actor, Path, credit, and lifecycle events uncommitted"
)
func _test_congestion_credit_and_alternate_route() -> void:
_reset_runtime(8_102)
var origin := Vector3i(-8, 0, WorldService.LAYER_FRONT)
var target := Vector3i(8, 0, WorldService.LAYER_FRONT)
_runtime.path_service.configure_cell_traffic(origin, 1, 0, false)
for index in range(5):
_expect(
_spawn_moving_guest(StringName("slow_%02d" % index), origin, target),
"congestion Guest %d spawns" % index
)
_expect(_runtime.path_service.get_current_density(origin) == 5, "five same-route actors create over-capacity density")
_clock.advance_one_minute()
var after_one: Array[Vector3i] = _guest_cells_with_prefix("slow_")
_expect(after_one.all(func(cell: Vector3i) -> bool: return cell == origin), "over-capacity speed credit prevents a first-minute burst")
_expect(
int(_runtime.get_actor_movement_credit(&"guest:slow_00").get(&"credit", -1)) == 250,
"Path speed multiplier is stored as deterministic fixed-point credit"
)
_clock.advance_minutes(3)
var after_four: Array[Vector3i] = _guest_cells_with_prefix("slow_")
_expect(after_four.all(func(cell: Vector3i) -> bool: return cell != origin), "minimum speed guarantees progress by the fourth congested minute")
_expect(_runtime.path_service.get_current_density(origin) == 0, "sequential committed movement lowers source density exactly")
_reset_runtime(8_103)
for x in range(0, 3):
_runtime.world_service.set_walkable(Vector3i(x, 1, WorldService.LAYER_FRONT), true)
_expect(_runtime.rebuild_path_graph_from_world(), "parallel walkable lane rebuilds into PathService")
var congested := Vector3i(1, 0, WorldService.LAYER_FRONT)
_runtime.path_service.configure_cell_traffic(congested, 1, 0, false)
for blocker_index in range(4):
_expect(
_register_idle_staff(StringName("blocker_%d" % blocker_index), congested),
"alternate-route blocker %d registers" % blocker_index
)
_expect(
_spawn_moving_guest(&"route_picker", Vector3i(0, 0, 0), Vector3i(2, 0, 0)),
"route-selection Guest spawns"
)
var planned: Array[Vector3i] = _runtime.path_service.find_path(
Vector3i(0, 0, 0), Vector3i(2, 0, 0)
)
_expect(
planned.size() > 1 and planned[1] == Vector3i(0, 1, 0),
"congestion-aware A* selects the parallel lane before Runtime movement (%s)" % [planned]
)
_clock.advance_one_minute()
var picker: GuestRuntime = _runtime.guest_service.get_guest(&"route_picker")
_expect(
picker != null and picker.current_cell == Vector3i(0, 1, 0),
"live congestion makes Runtime choose the cheaper parallel lane (actual %s)" % (
picker.current_cell if picker != null else "missing"
)
)
_expect(
_runtime.path_service.get_actor_projected_cell(&"guest:route_picker") == picker.current_cell,
"alternate-route movement commits Actor and Path projection together"
)
func _test_occupied_cell_graph_rebuild_is_rejected() -> void:
_reset_runtime(8_110)
var occupied := Vector3i(0, 0, WorldService.LAYER_FRONT)
_expect(_register_idle_staff(&"rebuild_guard", occupied), "graph-rebuild guard fixture registers")
var path_before: Dictionary = _runtime.path_service.create_snapshot()
_expect(_runtime.world_service.set_walkable(occupied, false), "fixture removes an occupied World cell")
_expect(
not _runtime.rebuild_path_graph_from_world(),
"Runtime rejects a graph rebuild that would evict a live actor projection"
)
_expect(
_runtime.path_service.create_snapshot() == path_before
and _runtime.path_service.get_actor_projected_cell(&"staff:rebuild_guard") == occupied
and not _runtime.get_actor_movement_credit(&"staff:rebuild_guard").is_empty(),
"rejected graph rebuild preserves Path projection, progress, density, and credit"
)
_expect(_runtime.world_service.set_walkable(occupied, true), "fixture restores the occupied World cell")
_expect(_runtime.rebuild_path_graph_from_world(), "safe topology rebuild succeeds after World repair")
func _test_stuck_guest_and_staff_recovery() -> void:
_reset_runtime(8_104)
_expect(
_spawn_moving_guest(&"stuck_guest", Vector3i(0, 0, 0), Vector3i(2, 0, 0)),
"stuck Guest fixture spawns"
)
_runtime.path_service.set_cell_temporary_block(Vector3i(1, 0, 0), true)
_clock.advance_minutes(PathService.STUCK_THRESHOLD_MINUTES - 1)
var guest: GuestRuntime = _runtime.guest_service.get_guest(&"stuck_guest")
_expect(guest != null and guest.state == GuestRuntime.State.MOVING, "Guest remains in movement recovery window before 30 minutes")
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"stuck_guest")
_expect(guest != null and guest.state == GuestRuntime.State.LEAVING, "30-minute Path report enters existing Guest safe recovery")
_expect(
guest != null and guest.current_cell == Vector3i.ZERO,
"stuck recovery proposes a safe target without implicit teleport"
)
_expect(_has_recent_event(&"PATH_ACTOR_STUCK", &"guest:stuck_guest"), "Guest stuck recovery emits a structured Path DomainEvent")
_reset_runtime(8_105)
_expect(_register_idle_staff(&"stuck_staff", Vector3i(0, 0, 0)), "stuck Staff fixture registers")
var created: CommandResult = _runtime.staff_service.create_task(
&"inspect", 90, _clock.absolute_minute,
{&"target_cell": Vector3i(2, 0, 0), &"estimated_minutes": 5},
&"stuck_task"
)
var task: StaffTask = _runtime.staff_service.get_task(&"stuck_task")
var member: StaffRuntime = _runtime.staff_service.get_staff(&"stuck_staff")
var reserved: bool = created.is_success() \
and task.reserve(&"stuck_staff", [], _clock.absolute_minute) \
and member.reserve_task(&"stuck_task", _clock.absolute_minute) \
and task.begin_moving(_clock.absolute_minute)
_expect(reserved, "StaffTask enters a consistent MOVING ownership fixture")
_runtime.path_service.set_cell_temporary_block(Vector3i(1, 0, 0), true)
_clock.advance_minutes(PathService.STUCK_THRESHOLD_MINUTES)
task = _runtime.staff_service.get_task(&"stuck_task")
member = _runtime.staff_service.get_staff(&"stuck_staff")
_expect(task != null and task.state == StaffTask.State.CANCELLED, "30-minute Staff stall cancels its owned task safely")
_expect(member != null and member.current_task_id.is_empty(), "Staff stuck recovery releases task ownership")
_expect(_has_recent_event(&"PATH_ACTOR_STUCK", &"staff:stuck_staff"), "Staff stuck cancellation emits a structured Path DomainEvent")
func _test_event_recovery_path_transaction() -> void:
_reset_runtime(8_109)
_expect(
_spawn_moving_guest(&"event_mover", Vector3i(-2, 0, 0), Vector3i(2, 0, 0)),
"Event recovery movement fixture spawns"
)
var actor_before: Dictionary = _runtime.guest_service.create_snapshot()
var path_before: Dictionary = _runtime.path_service.create_snapshot()
var credit_before: Dictionary = _runtime.get_actor_movement_credit(&"guest:event_mover")
var events_before: Dictionary = _runtime.domain_events.create_snapshot()
var action: EventAction = EventAction.new().configure(
&"recovery_move",
EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY,
{&"entity_id": &"event_mover", &"cell": Vector3i(-1, 0, 0)}
)
var applied: Dictionary = _runtime._execute_event_action_boundary(
action, &"apply", {&"event_instance_id": &"event.fixture"}, null
)
var moved: GuestRuntime = _runtime.guest_service.get_guest(&"event_mover")
_expect(
bool(applied.get(&"ok", false))
and moved.current_cell == Vector3i(-1, 0, 0)
and _runtime.path_service.get_actor_projected_cell(&"guest:event_mover") == moved.current_cell
and int(_runtime.get_actor_movement_credit(&"guest:event_mover")[&"credit"]) == 0,
"explicit Event recovery commits Actor, Path, and credit together"
)
var rolled_back: Dictionary = _runtime._execute_event_action_boundary(
action, &"rollback", {}, applied.get(&"token", null)
)
_expect(
bool(rolled_back.get(&"ok", false))
and _runtime.guest_service.create_snapshot() == actor_before
and _runtime.path_service.create_snapshot() == path_before
and _runtime.get_actor_movement_credit(&"guest:event_mover") == credit_before
and _runtime.domain_events.create_snapshot() == events_before,
"Event rollback restores Actor, Path, credit, and DomainEvents atomically"
)
func _test_v6_snapshot_crosslinks_and_legacy_v2() -> void:
_reset_runtime(8_106)
var origin := Vector3i(-3, 0, 0)
_runtime.path_service.configure_cell_traffic(origin, 1, 0, false)
for index in range(2):
_spawn_moving_guest(StringName("save_%d" % index), origin, Vector3i(3, 0, 0))
_clock.advance_one_minute()
var snapshot: Dictionary = _runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
_expect(int(runtime_data[&"runtime_schema_version"]) == 6, "Runtime snapshot advertises schema v6")
_expect(not runtime_data[&"movement_credits"].is_empty(), "mid-move snapshot persists fixed-point movement credits")
var digest := SimulationDigest.compute(snapshot)
_expect(_runtime.restore_snapshot(snapshot), "valid mid-move v6 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "mid-move v6 save/load is exact")
var corruptions: Array[Dictionary] = []
var bad_actor := snapshot.duplicate(true)
bad_actor[&"systems"][&"runtime"][&"path"][&"actor_cells"][0][&"cell"] = Vector3i(0, 0, 0)
corruptions.append({&"label": "actor cell", &"snapshot": bad_actor})
var bad_density := snapshot.duplicate(true)
for cell_entry: Dictionary in bad_density[&"systems"][&"runtime"][&"path"][&"cell_traffic"]:
if cell_entry[&"cell"] == origin:
cell_entry[&"current_density"] = int(cell_entry[&"current_density"]) + 1
break
corruptions.append({&"label": "density", &"snapshot": bad_density})
var bad_progress := snapshot.duplicate(true)
bad_progress[&"systems"][&"runtime"][&"path"][&"actor_progress"][0][&"last_cell"] = Vector3i(1, 0, 0)
corruptions.append({&"label": "progress", &"snapshot": bad_progress})
var bad_credit := snapshot.duplicate(true)
bad_credit[&"systems"][&"runtime"][&"movement_credits"][0][&"credit"] = 1_000
corruptions.append({&"label": "movement credit range", &"snapshot": bad_credit})
var bad_credit_time := snapshot.duplicate(true)
bad_credit_time[&"systems"][&"runtime"][&"movement_credits"][0][&"updated_at"] = _clock.absolute_minute + 1
corruptions.append({&"label": "movement credit time", &"snapshot": bad_credit_time})
var missing_path_cell := snapshot.duplicate(true)
missing_path_cell[&"systems"][&"runtime"][&"path"][&"cell_traffic"].pop_back()
corruptions.append({&"label": "Path/world cell-set", &"snapshot": missing_path_cell})
for corruption: Dictionary in corruptions:
var live_before: Dictionary = _runtime.create_snapshot()
_expect(
not _runtime.restore_snapshot(corruption[&"snapshot"])
and _runtime.create_snapshot() == live_before,
"corrupt %s crosslink is rejected atomically" % corruption[&"label"]
)
var legacy_v2 := snapshot.duplicate(true)
var legacy_data: Dictionary = legacy_v2[&"systems"][&"runtime"]
legacy_data[&"runtime_schema_version"] = 2
_strip_post_v2_runtime_state(legacy_data)
legacy_data[&"path"][&"actor_cells"] = []
legacy_data[&"path"][&"actor_progress"] = []
for traffic: Dictionary in legacy_data[&"path"][&"cell_traffic"]:
traffic[&"current_density"] = 0
if traffic[&"cell"] == origin:
traffic[&"temporary_block"] = true
_expect(
_is_true_runtime_v2_shape(legacy_data),
"legacy fixture removes every v3-v6 aggregate and dedicated history instead of relabelling v6"
)
_expect(_runtime.restore_snapshot(legacy_v2), "legacy Runtime v2 deterministically rebuilds Path actors and credits")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(
int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 6
and migrated[&"systems"][&"runtime"][&"path"][&"actor_cells"].size() == 2
and migrated[&"systems"][&"runtime"][&"movement_credits"].size() == 2
and bool(_runtime.path_service.get_cell_traffic(origin)[&"temporary_block"]),
"legacy migration emits canonical v6 ownership while preserving blocked traffic state"
)
func _test_dispatcher_request_ownership_crosslinks() -> void:
_reset_runtime(8_107)
var stock: CommandResult = _runtime.submit_command(
&"add_inventory_stack",
{&"owner_inventory_id": &"supplier", &"item_id": &"item/ore", &"quantity": 2, &"unit_weight": 3},
&"manager", &"supplier", 10, -1, &"path.runtime.dispatch.stock"
)
_expect(stock.is_success(), "dispatcher crosslink fixture stocks weighted items")
var requested: CommandResult = _runtime.submit_command(
&"create_logistics_request",
{
&"request_id": &"ownership_request",
&"source_inventory_id": &"supplier",
&"destination_inventory_id": &"destination",
&"item_requirements": {&"item/ore": 2},
},
&"manager", &"ownership_request", 10, -1, &"path.runtime.dispatch.request"
)
_expect(requested.is_success(), "dispatcher crosslink fixture creates a reserved request")
var dispatched: CommandResult = _runtime.submit_command(
&"dispatch_logistics",
{&"carriers": {
&"carrier_a": {&"capacity": 10, &"current_load": 0, &"path_minutes": 1},
}},
&"manager", &"ownership_request", 10, -1, &"path.runtime.dispatch.assign"
)
_expect(dispatched.is_success(), "dispatcher assigns request-ID ownership")
var snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(snapshot), "active dispatcher ownership snapshot restores")
var ownership_tamper: Dictionary = snapshot.duplicate(true)
ownership_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"][&"assignment_ownership"][0][&"carrier_id"] = "carrier_b"
var live_before: Dictionary = _runtime.create_snapshot()
_expect(
not _runtime.restore_snapshot(ownership_tamper) and _runtime.create_snapshot() == live_before,
"request-level carrier tamper is rejected despite unchanged aggregate load"
)
var coherent_dispatcher_tamper: Dictionary = snapshot.duplicate(true)
var tampered_dispatcher: Dictionary = coherent_dispatcher_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"]
tampered_dispatcher[&"assignment_ownership"][0][&"carrier_id"] = "carrier_b"
tampered_dispatcher[&"carrier_loads"][0][&"carrier_id"] = "carrier_b"
tampered_dispatcher[&"assignment_history"][0][&"carrier_id"] = "carrier_b"
_expect(
not _runtime.restore_snapshot(coherent_dispatcher_tamper)
and _runtime.create_snapshot() == live_before,
"Runtime rejects internally coherent Dispatcher carrier ownership that conflicts with its request"
)
var state_tamper: Dictionary = snapshot.duplicate(true)
state_tamper[&"systems"][&"runtime"][&"logistics_requests"][0][&"state"] = "COMPLETED"
_expect(
not _runtime.restore_snapshot(state_tamper) and _runtime.create_snapshot() == live_before,
"terminal-state tamper cannot retain active Dispatcher ownership"
)
var history_tamper: Dictionary = snapshot.duplicate(true)
history_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"][&"assignment_history"][0][&"weight"] = 3
_expect(
not _runtime.restore_snapshot(history_tamper) and _runtime.create_snapshot() == live_before,
"request-level history weight tamper is rejected atomically"
)
func _test_twenty_four_hour_projection_integrity() -> void:
_reset_runtime(8_108)
_expect(_register_idle_staff(&"day_guard", Vector3i(0, 0, 0)), "24-hour Staff projection registers")
_clock.advance_minutes(1_440)
var snapshot: Dictionary = _runtime.create_snapshot()
_expect(
_runtime.path_service.has_actor_projection(&"staff:day_guard")
and _runtime.path_service.get_actor_projected_cell(&"staff:day_guard") == Vector3i.ZERO,
"24-hour idle processing leaves no Path actor orphan"
)
_expect(_runtime.restore_snapshot(snapshot), "24-hour Path/credit snapshot passes strict crosslink restore")
_expect(_runtime.create_snapshot() == snapshot, "24-hour Path traffic state restores exactly")
func _spawn_moving_guest(guest_id: StringName, cell: Vector3i, target: Vector3i) -> bool:
var result: CommandResult = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": guest_id,
&"guest_type_id": &"traveler",
&"gold": 100,
&"options": {
&"current_cell": cell,
&"state": GuestRuntime.State.MOVING,
&"state_entered_at": _clock.absolute_minute - 1,
&"planned_exit_at": _clock.absolute_minute + 2_000,
&"path_handle": {&"target_cell": target},
},
},
&"world", guest_id, 10, -1, StringName("path.spawn.%s" % guest_id)
)
return result.is_success()
func _register_idle_staff(staff_id: StringName, cell: Vector3i) -> bool:
var result: CommandResult = _runtime.submit_command(
&"register_staff",
{
&"staff_id": staff_id,
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.AVAILABLE,
&"current_cell": cell,
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
},
},
&"manager", staff_id, 10, -1, StringName("path.staff.%s" % staff_id)
)
return result.is_success()
func _guest_cells_with_prefix(prefix: String) -> Array[Vector3i]:
var cells: Array[Vector3i] = []
for guest: GuestRuntime in _runtime.guest_service.get_guests():
if String(guest.guest_id).begins_with(prefix):
cells.append(guest.current_cell)
return cells
func _has_recent_event(event_type: StringName, actor_id: StringName) -> bool:
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == event_type and actor_id in event.entity_ids:
return true
return false
func _strip_post_v2_runtime_state(runtime_data: Dictionary) -> void:
for key: StringName in POST_V2_RUNTIME_KEYS:
runtime_data.erase(key)
runtime_data.erase(String(key))
_strip_post_v2_event_history(runtime_data)
_strip_post_v2_pending_commands(runtime_data)
func _strip_post_v2_event_history(runtime_data: Dictionary) -> void:
var stream_value: Variant = runtime_data.get(&"domain_events", null)
if not stream_value is Dictionary:
return
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
var removed_event_ids: Dictionary = {}
var retained_events: Array = []
for raw_event: Variant in recent_value:
if raw_event is Dictionary and StringName(
(raw_event as Dictionary).get("event_type", "")
) in POST_V2_EVENT_TYPES:
removed_event_ids[StringName((raw_event as Dictionary).get("event_id", ""))] = true
continue
retained_events.append(raw_event)
var retained_published_ids: Array = []
for raw_event_id: Variant in published_value:
if not removed_event_ids.has(StringName(raw_event_id)):
retained_published_ids.append(raw_event_id)
stream["recent_events"] = retained_events
stream["published_ids"] = retained_published_ids
func _strip_post_v2_pending_commands(runtime_data: Dictionary) -> void:
var queue_value: Variant = runtime_data.get(&"command_queue", null)
if not queue_value is Dictionary:
return
var queue: Dictionary = queue_value
var removed_command_ids: Dictionary = {}
for queue_key: String in ["pending", "in_flight"]:
var commands_value: Variant = queue.get(queue_key, null)
if not commands_value is Array:
continue
var retained_commands: Array = []
for raw_command: Variant in commands_value:
if raw_command is Dictionary and StringName(
(raw_command as Dictionary).get("command_type", "")
) in POST_V2_COMMAND_TYPES:
removed_command_ids[int((raw_command as Dictionary).get("command_id", -1))] = true
continue
retained_commands.append(raw_command)
queue[queue_key] = retained_commands
if removed_command_ids.is_empty():
return
var accepted_value: Variant = queue.get("accepted", null)
if accepted_value is Array:
var retained_accepted: Array = []
for raw_metadata: Variant in accepted_value:
if not raw_metadata is Dictionary or not removed_command_ids.has(
int((raw_metadata as Dictionary).get("command_id", -1))
):
retained_accepted.append(raw_metadata)
queue["accepted"] = retained_accepted
var idempotency_value: Variant = queue.get("idempotency", null)
if idempotency_value is Dictionary:
var idempotency: Dictionary = idempotency_value
for key: Variant in idempotency.keys():
if removed_command_ids.has(int(idempotency[key])):
idempotency.erase(key)
func _is_true_runtime_v2_shape(runtime_data: Dictionary) -> bool:
if typeof(runtime_data.get(&"runtime_schema_version", null)) != TYPE_INT \
or int(runtime_data[&"runtime_schema_version"]) != 2:
return false
for key: StringName in POST_V2_RUNTIME_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
var recent_value: Variant = (stream_value as Dictionary).get("recent_events", null)
if not recent_value is Array:
return false
for raw_event: Variant in recent_value:
if raw_event is Dictionary and StringName(
(raw_event as Dictionary).get("event_type", "")
) in POST_V2_EVENT_TYPES:
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"]:
var commands_value: Variant = (queue_value as Dictionary).get(queue_key, null)
if not commands_value is Array:
return false
for raw_command: Variant in commands_value:
if raw_command is Dictionary and StringName(
(raw_command as Dictionary).get("command_type", "")
) in POST_V2_COMMAND_TYPES:
return false
var staff_value: Variant = runtime_data.get(&"staff", null)
if staff_value is Dictionary:
var payroll_value: Variant = (staff_value as Dictionary).get(
&"payroll_settlements", []
)
if payroll_value is Array and not payroll_value.is_empty():
return false
var economy_value: Variant = runtime_data.get(&"economy", null)
if economy_value is Dictionary:
var entries_value: Variant = (economy_value as Dictionary).get(&"entries", [])
if entries_value is Array:
for raw_entry: Variant in entries_value:
if raw_entry is Dictionary and StringName(
(raw_entry as Dictionary).get(&"category", "")
) == &"WAGE":
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)
@@ -0,0 +1 @@
uid://bgrelqd3fwfkc
+283
View File
@@ -0,0 +1,283 @@
extends SceneTree
const StaffRuntimeType = preload("res://scripts/staff/staff_runtime.gd")
const StaffTaskType = preload("res://scripts/staff/staff_task.gd")
const StaffServiceType = preload("res://scripts/staff/staff_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: Staff payroll Operations boundary")
_test_preview_and_settlement_contract()
_test_employment_effective_minute_boundary()
_test_replay_conflict_and_atomic_validation()
_test_snapshot_contract()
_test_service_owned_task_cadence()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_preview_and_settlement_contract() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"z_active", 40, 0))
service.register_staff(_staff(&"a_active", 100, 20))
service.register_staff(_staff(&"m_departed", 70, 90))
service.remove_staff(&"m_departed", &"LEFT", true, 0)
var preview := service.preview_daily_payroll(1)
_expect(preview[&"ok"], "positive integer day previews")
_expect(
preview[&"rows"].map(func(row: Dictionary) -> StringName: return row[&"staff_id"]) \
== [&"a_active", &"m_departed", &"z_active"],
"preview rows are globally lexical"
)
_expect(preview[&"rows"][0][&"prior_arrears"] == 20 and preview[&"rows"][0][&"new_wages"] == 100, "active row separates arrears from today's wage")
_expect(preview[&"rows"][1][&"new_wages"] == 0 and preview[&"rows"][1][&"total_due"] == 90, "departed row carries debt without new wage")
_expect(preview[&"new_wages_total"] == 140, "Operations receives exact new-wage expense")
_expect(preview[&"prior_arrears_total"] == 110, "Operations receives exact carried arrears")
_expect(preview[&"total_due"] == 250, "Operations receives exact total obligation")
var applied = service.apply_daily_payroll_settlement(
&"operations.payroll.1", 1, {&"a_active": 120, &"m_departed": 30, &"z_active": 0}
)
_expect(applied.is_success() and not applied.details[&"replayed"], "settlement commits")
_expect(applied.details[&"paid_total"] == 150, "settlement reports actual cash allocation independently")
_expect(applied.details[&"unpaid_after_total"] == 100, "settlement reports remaining liability")
_expect(service.get_staff(&"a_active").unpaid_wages == 0, "full active payment clears balance")
_expect(service.get_staff(&"z_active").unpaid_wages == 40, "zero active payment becomes arrears")
_expect(service.get_departed_unpaid_wages(&"m_departed") == 60, "partial departed payment preserves remainder")
var settlement_copy := service.get_payroll_settlement(&"operations.payroll.1")
settlement_copy[&"paid_total"] = 999
_expect(service.get_payroll_settlement(&"operations.payroll.1")[&"paid_total"] == 150, "settlement query returns a defensive deep copy")
_expect(service.validate_invariants()[&"ok"], "committed settlement history satisfies Staff invariants")
var day_two := service.preview_daily_payroll(2)
_expect(day_two[&"new_wages_total"] == 140 and day_two[&"prior_arrears_total"] == 100, "next day accrues once on top of exact arrears")
service.apply_daily_payroll_settlement(
&"operations.payroll.2", 2, {&"a_active": 50, &"m_departed": 60, &"z_active": 80}
)
_expect(service.get_departed_staff_ids().is_empty(), "departed liability disappears after full paydown")
_expect(service.get_staff(&"a_active").unpaid_wages == 50, "partial next-day active payment is retained")
service.remove_staff(&"a_active", &"LEFT", true, 2)
_expect(service.get_departed_unpaid_wages(&"a_active") == 50, "departure transfers active arrears")
_expect(service.preview_daily_payroll(3)[&"rows"][0][&"new_wages"] == 0, "transferred departed debt never accrues wage")
func _test_employment_effective_minute_boundary() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"before_boundary", 100, 0, 1_439))
service.register_staff(_staff(&"exact_boundary", 80, 35, 1_440))
service.register_staff(_staff(&"after_boundary", 60, 0, 1_441))
var day_one := service.preview_daily_payroll(1)
var rows_by_id: Dictionary = {}
for row: Dictionary in day_one[&"rows"]:
rows_by_id[row[&"staff_id"]] = row
_expect(day_one[&"ok"] and day_one[&"new_wages_total"] == 100, "day close accrues only hires strictly before its end")
_expect(rows_by_id[&"before_boundary"][&"new_wages"] == 100, "minute before boundary accrues on that day")
_expect(rows_by_id[&"exact_boundary"][&"new_wages"] == 0, "exact-boundary hire starts accruing next day")
_expect(rows_by_id[&"exact_boundary"][&"total_due"] == 35, "exact-boundary exclusion preserves prior arrears")
_expect(rows_by_id[&"after_boundary"][&"new_wages"] == 0, "post-boundary hire does not accrue early")
var settled = service.apply_daily_payroll_settlement(
&"effective.day.1", 1, {&"before_boundary": 100, &"exact_boundary": 10}
)
_expect(settled.is_success() and settled.details[&"new_wages_total"] == 100, "settlement uses the same effective-time boundary as preview")
_expect(service.get_staff(&"exact_boundary").unpaid_wages == 25, "settlement pays carried arrears without inventing a boundary-day wage")
var day_two := service.preview_daily_payroll(2)
_expect(day_two[&"new_wages_total"] == 240, "all three hires accrue at the following day close")
var invalid_service := StaffServiceType.new()
var string_minute := StaffRuntimeType.new().configure(&"string_minute", &"worker", {
&"daily_wage": 10,
&"employment_effective_minute": "1440",
})
var float_minute := StaffRuntimeType.new().configure(&"float_minute", &"worker", {
&"daily_wage": 10,
&"employment_effective_minute": 1_440.0,
})
var negative_minute := StaffRuntimeType.new().configure(&"negative_minute", &"worker", {
&"daily_wage": 10,
&"employment_effective_minute": -1,
})
_expect(not invalid_service.register_staff(string_minute).is_success(), "string employment effective option is rejected")
_expect(not invalid_service.register_staff(float_minute).is_success(), "floating employment effective option is rejected")
_expect(not invalid_service.register_staff(negative_minute).is_success(), "negative employment effective option is rejected")
_expect(invalid_service.get_staff_members().is_empty(), "invalid effective-time options never partially register")
func _test_replay_conflict_and_atomic_validation() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"worker", 100, 0))
var before := service.create_snapshot()
_expect(not service.preview_daily_payroll(0)[&"ok"], "zero day fails closed")
_expect(not service.preview_daily_payroll("1")[&"ok"], "string day fails closed")
_expect(not service.apply_daily_payroll_settlement(&"bad id", 1, {}).is_success(), "unstable settlement ID fails closed")
_expect(not service.apply_daily_payroll_settlement(&"not_map", 1, []).is_success(), "non-map allocation fails closed")
_expect(not service.apply_daily_payroll_settlement(&"string", 1, {&"worker": "1"}).is_success(), "string payment is not coerced")
_expect(not service.apply_daily_payroll_settlement(&"float", 1, {&"worker": 1.0}).is_success(), "float payment is not coerced")
_expect(not service.apply_daily_payroll_settlement(&"negative", 1, {&"worker": -1}).is_success(), "negative payment fails closed")
_expect(not service.apply_daily_payroll_settlement(&"over", 1, {&"worker": 101}).is_success(), "overpayment fails closed")
_expect(not service.apply_daily_payroll_settlement(&"unknown", 1, {&"ghost": 0}).is_success(), "unknown Staff ID fails closed")
_expect(service.create_snapshot() == before, "all rejected allocations preserve state atomically")
var first = service.apply_daily_payroll_settlement(&"stable.1", 1, {&"worker": 25})
var committed := service.create_snapshot()
_expect(first.is_success() and service.get_staff(&"worker").unpaid_wages == 75, "valid allocation commits once")
var replay = service.apply_daily_payroll_settlement(&"stable.1", 1, {&"worker": 25})
_expect(replay.is_success() and replay.details[&"replayed"], "same ID and fingerprint replays")
_expect(service.create_snapshot() == committed, "replay never applies wages twice")
_expect(not service.apply_daily_payroll_settlement(&"stable.1", 1, {&"worker": 24}).is_success(), "same ID rejects conflicting payment")
_expect(not service.apply_daily_payroll_settlement(&"stable.1", 2, {&"worker": 25}).is_success(), "same ID rejects conflicting day")
_expect(not service.apply_daily_payroll_settlement(&"different.1", 1, {}).is_success(), "same day rejects a second ID")
_expect(service.create_snapshot() == committed, "all settlement conflicts preserve committed state")
func _test_snapshot_contract() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"a", 100, 20))
service.register_staff(_staff(&"b", 50, 0))
service.apply_daily_payroll_settlement(&"snapshot.1", 1, {&"a": 30, &"b": 0})
service.apply_daily_payroll_settlement(&"snapshot.2", 2, {&"a": 50, &"b": 100})
var snapshot := service.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "v3 snapshot restores")
_expect(restored.create_snapshot() == snapshot, "v3 snapshot round trip is exact")
var before := restored.create_snapshot()
var fingerprint := snapshot.duplicate(true)
fingerprint[&"payroll_settlements"][0][&"fingerprint"] = "corrupt"
_expect(not restored.restore_snapshot(fingerprint), "derived fingerprint tamper is rejected")
_expect(restored.create_snapshot() == before, "fingerprint rejection is atomic")
var total := snapshot.duplicate(true)
total[&"payroll_settlements"][0][&"paid_total"] += 1
_expect(not restored.restore_snapshot(total), "derived total tamper is rejected")
_expect(restored.create_snapshot() == before, "total rejection is atomic")
var order := snapshot.duplicate(true)
order[&"payroll_settlements"].reverse()
_expect(not restored.restore_snapshot(order), "non-canonical settlement order is rejected")
_expect(restored.create_snapshot() == before, "order rejection is atomic")
var duplicate_day := snapshot.duplicate(true)
duplicate_day[&"payroll_settlements"][1][&"day"] = 1
_expect(not restored.restore_snapshot(duplicate_day), "duplicate settled day is rejected")
_expect(restored.create_snapshot() == before, "day uniqueness rejection is atomic")
var negative := snapshot.duplicate(true)
negative[&"payroll_settlements"][0][&"rows"][0][&"payment"] = -1
_expect(not restored.restore_snapshot(negative), "negative historical amount is rejected")
_expect(restored.create_snapshot() == before, "negative history rejection is atomic")
var unknown_field := snapshot.duplicate(true)
unknown_field[&"payroll_settlements"][0][&"extra"] = 1
_expect(not restored.restore_snapshot(unknown_field), "unknown current history field is rejected")
_expect(restored.create_snapshot() == before, "unknown field rejection is atomic")
var effective_type := snapshot.duplicate(true)
effective_type[&"staff"][0][&"employment_effective_minute"] = 1_440.0
_expect(not restored.restore_snapshot(effective_type), "floating employment effective minute is rejected")
_expect(restored.create_snapshot() == before, "effective-minute type rejection is atomic")
var effective_negative := snapshot.duplicate(true)
effective_negative[&"staff"][0][&"employment_effective_minute"] = -1
_expect(not restored.restore_snapshot(effective_negative), "negative employment effective minute is rejected")
_expect(restored.create_snapshot() == before, "negative effective-minute rejection is atomic")
var effective_missing := snapshot.duplicate(true)
effective_missing[&"staff"][0].erase(&"employment_effective_minute")
_expect(not restored.restore_snapshot(effective_missing), "current Staff snapshot rejects a missing employment effective minute")
_expect(restored.create_snapshot() == before, "missing effective-minute rejection is atomic")
var unversioned_current_row: Dictionary = snapshot[&"staff"][0].duplicate(true)
unversioned_current_row.erase(&"snapshot_version")
_expect(StaffRuntimeType.from_snapshot(unversioned_current_row) == null, "StaffRuntime rejects an effective minute without its row version")
var erased_current_row_schema := snapshot.duplicate(true)
erased_current_row_schema[&"staff"][0].erase(&"snapshot_version")
erased_current_row_schema[&"staff"][0].erase(&"employment_effective_minute")
_expect(not restored.restore_snapshot(erased_current_row_schema), "v3 rejects a Staff row disguised as pre-effective schema")
_expect(restored.create_snapshot() == before, "row-schema erasure rejection is atomic")
var effective_version := snapshot.duplicate(true)
effective_version[&"staff"][0][&"snapshot_version"] = 2
_expect(not restored.restore_snapshot(effective_version), "unknown Staff row snapshot version is rejected")
_expect(restored.create_snapshot() == before, "Staff row version rejection is atomic")
var outer_downgrade := snapshot.duplicate(true)
outer_downgrade[&"version"] = 2
_expect(not restored.restore_snapshot(outer_downgrade), "changing only the outer version cannot downgrade a v3 Staff snapshot")
_expect(restored.create_snapshot() == before, "outer version downgrade rejection is atomic")
var pre_effective_v2 := snapshot.duplicate(true)
pre_effective_v2[&"version"] = 2
for staff_snapshot: Dictionary in pre_effective_v2[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var migrated_v2 := StaffServiceType.new()
_expect(migrated_v2.restore_snapshot(pre_effective_v2), "pre-effective-minute v2 Staff snapshot migrates")
_expect(migrated_v2.get_staff(&"a").employment_effective_minute == 0, "v2 legacy Staff employment starts at minute zero")
_expect(
migrated_v2.create_snapshot()[&"version"] == 3
and migrated_v2.create_snapshot()[&"staff"][0][&"snapshot_version"] == 1,
"v2 legacy Staff row is re-emitted in the current service and row schemas"
)
var legacy := snapshot.duplicate(true)
legacy[&"version"] = 1
legacy.erase(&"last_advanced_minute")
legacy.erase(&"payroll_settlements")
for staff_snapshot: Dictionary in legacy[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var migrated := StaffServiceType.new()
_expect(migrated.restore_snapshot(legacy), "v1 snapshot migrates")
_expect(migrated.get_staff(&"a").unpaid_wages == 140, "v1 migration preserves active unpaid wages")
_expect(migrated.get_staff(&"a").employment_effective_minute == 0, "legacy Staff employment starts deterministically at minute zero")
_expect(migrated.create_snapshot()[&"payroll_settlements"].is_empty(), "v1 migration starts with empty settlement history")
func _test_service_owned_task_cadence() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"task_worker", 0, 0)
var task := StaffTaskType.new().configure(&"task", &"work", 50, 0, {
&"required_role_tags": [&"worker"],
&"required_skill_min": {&"operation": 1},
&"task_tags": [&"work"],
&"estimated_minutes": 2,
&"target_cell": Vector3i.ZERO,
})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
service.advance_one_minute(1)
_expect(task.state == StaffTaskType.State.WORKING and task.progress_minutes == 0, "RESERVED begins without same-tick work")
_expect(not service.progress_task(task.task_id, 1, 1).is_success(), "same-minute explicit progress is blocked")
service.advance_one_minute(2)
_expect(task.progress_minutes == 1, "WORKING advances exactly one minute per tick")
_expect(not service.advance_one_minute(2)[&"ok"] and task.progress_minutes == 1, "duplicate service tick is idempotently rejected")
service.advance_one_minute(3)
_expect(task.state == StaffTaskType.State.COMPLETED, "automatic work completes exactly")
func _staff(
staff_id: StringName,
daily_wage: int,
unpaid_wages: int,
employment_effective_minute: int = 0
) -> StaffRuntime:
return StaffRuntimeType.new().configure(staff_id, &"worker", {
&"state": StaffRuntimeType.State.AVAILABLE,
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
&"skills": {&"operation": 10},
&"daily_wage": daily_wage,
&"employment_effective_minute": employment_effective_minute,
&"unpaid_wages": unpaid_wages,
})
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bnqa31rwr3wx7
+828
View File
@@ -0,0 +1,828 @@
extends SceneTree
const StaffRuntimeType = preload("res://scripts/staff/staff_runtime.gd")
const ShiftInstanceType = preload("res://scripts/staff/shift_instance.gd")
const StaffTaskType = preload("res://scripts/staff/staff_task.gd")
const TaskDispatcherType = preload("res://scripts/staff/task_dispatcher.gd")
const StaffServiceType = preload("res://scripts/staff/staff_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: Staff Shift and Task Dispatcher")
_test_night_shift_crosses_midnight()
_test_shift_state_machine_and_override()
_test_multiple_future_shifts_and_strict_overlap()
_test_deterministic_dispatch_and_pair_reservation()
_test_skill_score_and_tie_break()
_test_item_reservation_failure_is_atomic()
_test_interrupt_levels()
_test_fatigue_blocks_dangerous_tasks()
_test_missing_staff_reopens_task()
_test_task_progression()
_test_automatic_task_progression_once_per_tick()
_test_employment_and_daily_payroll_contract()
_test_retirement_preserves_unpaid_wages()
_test_daily_payroll_settlement_lifecycle()
_test_payroll_validation_and_idempotency()
_test_payroll_snapshot_migration_and_tamper()
_test_remove_staff_cleans_schedule_graph()
_test_snapshot_round_trip_and_atomic_restore()
_test_orphan_recovery_is_atomic()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_night_shift_crosses_midnight() -> void:
var shift := ShiftInstanceType.create_standard(
&"night_d2",
&"staff_night",
&"NIGHT",
2,
&"warehouse"
)
_expect(shift != null and shift.is_valid(), "standard NIGHT Shift is valid")
_expect(shift.start_day == 2 and shift.shift_instance_id == &"night_d2", "overnight Shift keeps its start-day identity")
_expect(shift.start_at == 3_960 and shift.end_at == 4_680, "NIGHT uses exact absolute-minute bounds")
_expect(shift.crosses_midnight(), "NIGHT is marked as crossing midnight")
_expect(shift.contains_minute(4_379), "same Shift contains time before calendar midnight")
_expect(shift.contains_minute(4_380), "same Shift contains time after calendar midnight")
_expect(not shift.contains_minute(4_680), "Shift end minute is exclusive")
shift.record_duty_minute(4_379)
shift.record_duty_minute(4_380)
_expect(shift.worked_minutes == 2, "work on both calendar dates aggregates into one Shift")
var restored := ShiftInstanceType.from_snapshot(shift.create_snapshot())
_expect(restored != null and restored.create_snapshot() == shift.create_snapshot(), "overnight Shift snapshot round trip is exact")
func _test_shift_state_machine_and_override() -> void:
var service := StaffServiceType.new()
var staff := _staff(
&"shift_worker",
0,
{&"state": StaffRuntimeType.State.OFF_DUTY}
)
_expect(service.register_staff(staff).is_success(), "Staff registers before scheduling")
_expect(
service.set_schedule_override(
staff.staff_id,
1,
{&"start_minute_of_day": 420, &"end_minute_of_day": 900}
).is_success(),
"one-day ScheduleOverride is stored separately"
)
_expect(
service.create_standard_shift(staff.staff_id, &"DAY", 1, &"reception", &"shift_override").is_success(),
"standard Shift instance accepts a one-day override"
)
var shift := service.get_shift(&"shift_override")
_expect(shift.start_minute_of_day == 420 and shift.end_minute_of_day == 900, "override changes only the concrete Shift times")
_expect(shift.template_id == &"DAY", "ScheduleOverride does not rewrite the base template ID")
service.advance_one_minute(1_830)
_expect(staff.state == StaffRuntimeType.State.COMMUTING, "Staff starts commuting exactly 30 minutes before Shift")
service.advance_one_minute(1_860)
_expect(staff.state == StaffRuntimeType.State.AVAILABLE, "Staff becomes available at Shift start")
_expect(staff.assigned_facility_id == &"reception", "Shift assigns the work facility")
service.advance_one_minute(2_340)
_expect(staff.state == StaffRuntimeType.State.OFF_DUTY and staff.shift_id.is_empty(), "Staff goes off duty at Shift end")
_expect(shift.actual_ended_at == 2_340, "Shift records its exact end transition")
func _test_multiple_future_shifts_and_strict_overlap() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"multi_shift_worker", 10, {&"state": StaffRuntimeType.State.OFF_DUTY})
_expect(service.register_staff(staff).is_success(), "Staff registers for multiple future Shifts")
_expect(
service.create_flex_shift(staff.staff_id, 1, 540, 600, &"workshop", &"shift_second").is_success(),
"later future Shift can be registered first"
)
_expect(
service.create_flex_shift(staff.staff_id, 1, 420, 480, &"reception", &"shift_first").is_success(),
"earlier non-overlapping Shift can be registered second"
)
_expect(staff.shift_id.is_empty(), "future Shift registration does not overwrite the active Shift pointer")
service.advance_one_minute(1_830)
_expect(
staff.shift_id == &"shift_first" and staff.state == StaffRuntimeType.State.COMMUTING,
"chronologically first Shift enters COMMUTING at its commute boundary"
)
service.advance_one_minute(1_860)
var first := service.get_shift(&"shift_first")
_expect(
staff.shift_id == first.shift_instance_id and staff.state == StaffRuntimeType.State.AVAILABLE,
"chronologically first Shift becomes ACTIVE at start"
)
_expect(first.actual_started_at == 1_860 and staff.assigned_facility_id == &"reception", "first Shift records start and facility")
service.advance_one_minute(1_920)
_expect(
first.actual_ended_at == 1_920 and staff.shift_id.is_empty() and staff.state == StaffRuntimeType.State.OFF_DUTY,
"first Shift becomes COMPLETED and releases its active pointer"
)
service.advance_one_minute(1_950)
_expect(
staff.shift_id == &"shift_second" and staff.state == StaffRuntimeType.State.COMMUTING,
"second future Shift becomes the active commute after the first completes"
)
service.advance_one_minute(1_980)
var second := service.get_shift(&"shift_second")
_expect(
staff.shift_id == second.shift_instance_id and staff.state == StaffRuntimeType.State.AVAILABLE,
"second Shift becomes ACTIVE in sequence"
)
service.advance_one_minute(2_040)
_expect(
second.actual_ended_at == 2_040 and staff.shift_id.is_empty() and staff.state == StaffRuntimeType.State.OFF_DUTY,
"second Shift becomes COMPLETED in sequence"
)
_expect(service.validate_invariants()[&"ok"], "multiple completed Shift records preserve invariants")
var overlap_service := StaffServiceType.new()
var overlap_staff := _staff(&"overlap_worker", 10, {&"state": StaffRuntimeType.State.OFF_DUTY})
overlap_service.register_staff(overlap_staff)
overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 420, 480, &"a", &"base_shift")
_expect(
not overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 400, 430, &"b", &"overlap_left").is_success(),
"overlap is rejected when the proposed Shift starts before the existing Shift"
)
_expect(
not overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 470, 500, &"b", &"overlap_right").is_success(),
"overlap is rejected when the proposed Shift starts inside the existing Shift"
)
_expect(
not overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 400, 500, &"b", &"overlap_enclosing").is_success(),
"overlap is rejected when the proposed Shift encloses the existing Shift"
)
_expect(
overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 480, 510, &"b", &"adjacent_shift").is_success(),
"exactly adjacent half-open Shift intervals are accepted"
)
func _test_deterministic_dispatch_and_pair_reservation() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"staff_b", 10))
service.register_staff(_staff(&"staff_a", 10))
service.register_task(_task(&"task_b", 50, 100))
service.register_task(_task(&"task_a", 50, 100))
var report: Dictionary = service.dispatch_now(100)
_expect(report[&"assigned"].size() == 2, "dispatcher fills two eligible Staff reservations")
_expect(report[&"assigned"][0][&"task_id"] == &"task_a", "Task tie breaks lexically after priority and deadline")
_expect(report[&"assigned"][0][&"staff_id"] == &"staff_a", "Staff score tie breaks lexically")
_expect(report[&"assigned"][1][&"task_id"] == &"task_b", "second Task remains in deterministic order")
_expect(report[&"assigned"][1][&"staff_id"] == &"staff_b", "one Staff is not assigned twice in one dispatch pass")
var task_a := service.get_task(&"task_a")
var staff_a := service.get_staff(&"staff_a")
_expect(task_a.assigned_staff_id == staff_a.staff_id, "Task points to its reserved Staff")
_expect(staff_a.current_task_id == task_a.task_id, "Staff points back to its reserved Task")
_expect(service.validate_invariants()[&"ok"], "dispatcher preserves Task/Staff bidirectional invariant")
func _test_skill_score_and_tie_break() -> void:
var dispatcher := TaskDispatcherType.new()
var task := _task(&"skill_task", 40, 0, {
&"required_skill_min": {&"operation": 5},
})
var novice := _staff(&"novice", 5)
var expert := _staff(&"expert", 30)
var novice_score := dispatcher.score_candidate(task, novice, 2)
var expert_score := dispatcher.score_candidate(task, expert, 2)
_expect(expert_score > novice_score, "higher matching Skill raises StaffTaskScore")
_expect(
dispatcher.score_candidate(task, expert, 5) < dispatcher.score_candidate(task, expert, 2),
"longer travel lowers StaffTaskScore with integer-minute penalty"
)
expert.is_overtime = true
_expect(dispatcher.score_candidate(task, expert, 2) < expert_score, "overtime applies a deterministic score penalty")
func _test_item_reservation_failure_is_atomic() -> void:
var service := StaffServiceType.new()
var reserve_calls := [0]
service.configure_callbacks(
func(_task_value: StaffTask, _minute: int) -> Dictionary:
reserve_calls[0] += 1
return {&"ok": false, &"reason": &"NO_STOCK"},
func(_ids: Array[StringName], _task_value: StaffTask, _minute: int) -> bool:
return true
)
var staff := _staff(&"stock_worker", 10)
service.register_staff(staff)
var task := _task(&"stock_task", 80, 0, {&"required_items": {&"meal": 2}})
service.register_task(task)
var report: Dictionary = service.dispatch_now(0)
_expect(reserve_calls[0] == 1 and report[&"blocked"].size() == 1, "required items are reserved before Staff mutation")
_expect(task.state == StaffTaskType.State.BLOCKED and task.assigned_staff_id.is_empty(), "stock failure sends Task to BLOCKED without half reservation")
_expect(staff.state == StaffRuntimeType.State.AVAILABLE and staff.current_task_id.is_empty(), "stock failure leaves Staff fully available")
_expect(service.validate_invariants()[&"ok"], "failed item transaction preserves Staff invariants")
func _test_interrupt_levels() -> void:
var protected_service := StaffServiceType.new()
var protected_staff := _staff(&"protected_worker", 10)
protected_service.register_staff(protected_staff)
var protected_task := _task(&"protected_task", 20, 0, {&"interrupt_level": 0})
protected_service.register_task(protected_task)
protected_service.dispatch_now(0)
protected_service.begin_task(protected_task.task_id, 0)
var replacement := _task(&"replacement", 100, 1, {&"emergency": true})
protected_service.register_task(replacement)
var protected_report: Dictionary = protected_service.dispatch_now(1)
_expect(protected_report[&"assigned"].is_empty(), "interrupt level 0 rejects even emergency replacement")
_expect(protected_task.state == StaffTaskType.State.WORKING and protected_staff.current_task_id == protected_task.task_id, "non-interruptible Task/Staff pair remains untouched")
var emergency_service := StaffServiceType.new()
var emergency_staff := _staff(&"response_worker", 10)
emergency_service.register_staff(emergency_staff)
var level_one := _task(&"level_one", 20, 0, {&"interrupt_level": 1})
emergency_service.register_task(level_one)
emergency_service.dispatch_now(0)
emergency_service.begin_task(level_one.task_id, 0)
var normal := _task(&"normal_high", 100, 1, {&"emergency": false})
var emergency := _task(&"emergency", 99, 1, {&"emergency": true})
emergency_service.register_task(normal)
emergency_service.register_task(emergency)
var emergency_report: Dictionary = emergency_service.dispatch_now(1)
_expect(emergency_report[&"assigned"].size() == 1 and emergency_report[&"assigned"][0][&"task_id"] == &"emergency", "interrupt level 1 accepts only emergency Task")
_expect(level_one.state == StaffTaskType.State.OPEN and level_one.assigned_staff_id.is_empty(), "interrupted Task returns atomically to OPEN")
_expect(emergency_staff.current_task_id == emergency.task_id and emergency.assigned_staff_id == emergency_staff.staff_id, "replacement Task and Staff reserve together")
_expect(emergency_service.validate_invariants()[&"ok"], "interrupt transaction preserves pair invariants")
func _test_fatigue_blocks_dangerous_tasks() -> void:
var service := StaffServiceType.new()
var exhausted := _staff(&"exhausted", 20, {&"fatigue": 90})
service.register_staff(exhausted)
var dangerous := _task(&"dangerous", 100, 0, {&"dangerous": true})
var safe := _task(&"rest_break", 90, 0, {&"dangerous": false, &"task_tags": [&"rest"]})
service.register_task(dangerous)
service.register_task(safe)
var report: Dictionary = service.dispatch_now(0)
_expect(dangerous.state == StaffTaskType.State.OPEN and dangerous.assigned_staff_id.is_empty(), "fatigue 90 forbids dangerous Task assignment")
_expect(report[&"assigned"].size() == 1 and safe.assigned_staff_id == exhausted.staff_id, "fatigued Staff can still receive a safe rest Task")
_expect(exhausted.current_task_id == safe.task_id, "safe Task reserves the fatigued Staff atomically")
func _test_missing_staff_reopens_task() -> void:
var service := StaffServiceType.new()
var released_ids: Array[StringName] = []
service.configure_callbacks(
func(task: StaffTask, _minute: int) -> Dictionary:
return {&"ok": true, &"reservation_ids": [StringName("reserve_%s" % task.task_id)]},
func(ids: Array[StringName], _task_value: StaffTask, _minute: int) -> bool:
for reservation_id: StringName in ids:
released_ids.append(reservation_id)
return true
)
var staff := _staff(&"vanishing_staff", 10)
var task := _task(&"orphan_task", 60, 0, {&"required_items": {&"tool": 1}})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
_expect(task.state == StaffTaskType.State.RESERVED and task.reservation_ids.size() == 1, "Task owns item Reservation after successful dispatch")
service._staff.erase(staff.staff_id)
var audit: Dictionary = service.audit_orphan_tasks(1)
_expect(audit[&"reopened_task_ids"] == [&"orphan_task"], "orphan audit reports vanished Staff Task")
_expect(task.state == StaffTaskType.State.OPEN and task.assigned_staff_id.is_empty(), "vanished Staff Task returns to OPEN")
_expect(task.reservation_ids.is_empty() and released_ids == [&"reserve_orphan_task"], "orphan recovery releases every item Reservation")
_expect(service.validate_invariants()[&"ok"], "orphan recovery restores all Staff invariants")
func _test_task_progression() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"progress_worker", 10)
var task := _task(&"progress_task", 50, 0, {&"estimated_minutes": 2})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
_expect(service.begin_task(task.task_id, 1).is_success(), "reserved Task enters WORKING with its Staff")
var first = service.progress_task(task.task_id, 1, 2)
_expect(first.is_success() and not first.details[&"completed"], "Task remains WORKING before estimated duration")
var second = service.progress_task(task.task_id, 1, 3)
_expect(second.is_success() and second.details[&"completed"], "Task completes exactly at estimated duration")
_expect(task.state == StaffTaskType.State.COMPLETED and staff.current_task_id.is_empty(), "completion releases Staff while Task becomes terminal")
_expect(staff.state == StaffRuntimeType.State.AVAILABLE, "completed Task returns on-duty Staff to AVAILABLE")
func _test_automatic_task_progression_once_per_tick() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"auto_worker", 10)
var task := _task(&"auto_task", 50, 0, {&"estimated_minutes": 2})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
var begin_tick: Dictionary = service.advance_one_minute(1)
_expect(
begin_tick[&"task_actions"].size() == 1
and begin_tick[&"task_actions"][0][&"action"] == &"BEGAN_WORK",
"automatic cadence begins a RESERVED Task after its assignment minute"
)
_expect(task.state == StaffTaskType.State.WORKING and task.progress_minutes == 0, "begin tick does not also consume a work minute")
_expect(not service.progress_task(task.task_id, 1, 1).is_success(), "explicit same-minute progress cannot double apply after automatic begin")
_expect(not service.advance_one_minute(1)[&"ok"], "the same StaffService minute cannot run twice")
_expect(task.progress_minutes == 0, "duplicate tick rejection preserves Task progress")
var progress_tick: Dictionary = service.advance_one_minute(2)
_expect(
progress_tick[&"task_actions"][0][&"action"] == &"PROGRESSED"
and task.progress_minutes == 1,
"the following tick applies exactly one work minute"
)
_expect(not service.progress_task(task.task_id, 1, 2).is_success(), "automatic progress blocks a second same-minute write")
service.advance_one_minute(3)
_expect(task.state == StaffTaskType.State.COMPLETED and task.progress_minutes == 2, "automatic cadence completes at the exact estimated duration")
_expect(staff.state == StaffRuntimeType.State.AVAILABLE and staff.current_task_id.is_empty(), "automatic completion releases Staff exactly once")
var travel_service := StaffServiceType.new()
var traveler := _staff(&"travel_worker", 10, {&"current_cell": Vector3i.ZERO})
var remote := _task(&"remote_task", 50, 0, {
&"target_cell": Vector3i(2, 0, 0),
&"estimated_minutes": 1,
})
travel_service.register_staff(traveler)
travel_service.register_task(remote)
travel_service.dispatch_now(0)
travel_service.advance_one_minute(1)
_expect(remote.state == StaffTaskType.State.MOVING and remote.progress_minutes == 0, "non-arrived Staff enters MOVING without phantom work")
traveler.current_cell = remote.target_cell
travel_service.advance_one_minute(2)
_expect(remote.state == StaffTaskType.State.WORKING and remote.progress_minutes == 0, "arrival begins work without same-tick progress")
travel_service.advance_one_minute(3)
_expect(remote.state == StaffTaskType.State.COMPLETED, "arrived Staff completes through the same deterministic cadence")
func _test_employment_and_daily_payroll_contract() -> void:
var ordinary := _staff(&"staff_z", 10)
_expect(
ordinary.daily_wage == 0
and ordinary.employment_kind == StaffRuntimeType.EmploymentKind.FREE_HIRE,
"legacy generic Staff defaults to a zero-wage FREE_HIRE employment row"
)
var service := StaffServiceType.new()
_expect(service.register_staff(ordinary).is_success(), "generic Staff remains registrable without adult-content fields")
_expect(
service.register_staff(_staff(&"staff_a", 10, {
&"daily_wage": 240,
&"employment_kind": &"ADULT_CONTRACT",
})).is_success(),
"ADULT_CONTRACT is accepted as a commercial kind without duplicating the catalog gate"
)
_expect(
service.register_staff(_staff(&"staff_m", 10, {
&"daily_wage": 75,
&"employment_kind": StaffRuntimeType.EmploymentKind.UNIQUE,
})).is_success(),
"UNIQUE employment accepts the strict enum value"
)
var expected_rows: Array[Dictionary] = [
{&"staff_id": &"staff_a", &"employment_kind": &"ADULT_CONTRACT", &"daily_wage": 240},
{&"staff_id": &"staff_m", &"employment_kind": &"UNIQUE", &"daily_wage": 75},
{&"staff_id": &"staff_z", &"employment_kind": &"FREE_HIRE", &"daily_wage": 0},
]
_expect(service.get_daily_payroll_rows() == expected_rows, "daily payroll rows have an exact shape and lexical Staff order")
_expect(service.get_daily_payroll_total() == 315, "daily payroll total is the exact sum of active Staff wages")
var invalid_service := StaffServiceType.new()
_expect(
not invalid_service.register_staff(_staff(&"string_wage", 10, {&"daily_wage": "100"})).is_success(),
"string daily wage is rejected rather than coerced"
)
_expect(
not invalid_service.register_staff(_staff(&"float_wage", 10, {&"daily_wage": 10.0})).is_success(),
"floating-point daily wage is rejected rather than truncated"
)
_expect(
not invalid_service.register_staff(_staff(&"negative_wage", 10, {&"daily_wage": -1})).is_success(),
"negative daily wage is rejected"
)
_expect(
not invalid_service.register_staff(_staff(&"unknown_kind", 10, {&"employment_kind": &"CONTRACT"})).is_success(),
"unknown employment kind is rejected"
)
_expect(invalid_service.get_staff_members().is_empty(), "invalid employment rows never partially register")
var current_snapshot := service.create_snapshot()
var exact := StaffServiceType.new()
_expect(exact.restore_snapshot(current_snapshot), "current payroll snapshot restores")
_expect(exact.create_snapshot() == current_snapshot, "current payroll snapshot round trip is exact")
var legacy_snapshot: Dictionary = current_snapshot.duplicate(true)
for staff_snapshot: Dictionary in legacy_snapshot[&"staff"]:
staff_snapshot.erase(&"daily_wage")
staff_snapshot.erase(&"employment_kind")
var migrated := StaffServiceType.new()
_expect(migrated.restore_snapshot(legacy_snapshot), "pre-payroll Staff snapshot restores through deterministic defaults")
_expect(
migrated.get_daily_payroll_total() == 0
and migrated.get_staff(&"staff_a").employment_kind == StaffRuntimeType.EmploymentKind.FREE_HIRE,
"legacy payroll migration defaults every existing Staff to zero-wage FREE_HIRE"
)
var atomic_before := exact.create_snapshot()
var negative_snapshot: Dictionary = current_snapshot.duplicate(true)
negative_snapshot[&"staff"][0][&"daily_wage"] = -1
_expect(not exact.restore_snapshot(negative_snapshot), "negative wage snapshot corruption is rejected")
_expect(exact.create_snapshot() == atomic_before, "negative wage restore failure is atomic")
var typed_snapshot: Dictionary = current_snapshot.duplicate(true)
typed_snapshot[&"staff"][0][&"daily_wage"] = "240"
_expect(not exact.restore_snapshot(typed_snapshot), "malformed wage snapshot type is rejected")
_expect(exact.create_snapshot() == atomic_before, "malformed wage restore failure is atomic")
var kind_snapshot: Dictionary = current_snapshot.duplicate(true)
kind_snapshot[&"staff"][0][&"employment_kind"] = "UNKNOWN"
_expect(not exact.restore_snapshot(kind_snapshot), "unknown employment snapshot kind is rejected")
_expect(exact.create_snapshot() == atomic_before, "unknown kind restore failure is atomic")
var unpaid_type_snapshot: Dictionary = current_snapshot.duplicate(true)
unpaid_type_snapshot[&"staff"][0][&"unpaid_wages"] = "0"
_expect(not exact.restore_snapshot(unpaid_type_snapshot), "malformed active unpaid-wage type is rejected")
_expect(exact.create_snapshot() == atomic_before, "malformed active liability restore failure is atomic")
var invalid_runtime := service.get_staff(&"staff_a")
invalid_runtime.daily_wage = -1
_expect(not service.validate_invariants()[&"ok"], "Staff invariants detect a corrupted live daily wage")
invalid_runtime.daily_wage = 240
service._departed_payroll[&"former_staff"] = -5
_expect(not service.validate_invariants()[&"ok"], "Staff invariants detect a corrupted departed wage liability")
service._departed_payroll.erase(&"former_staff")
_expect(service.validate_invariants()[&"ok"], "valid employment and payroll state satisfies all Staff invariants")
func _test_retirement_preserves_unpaid_wages() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"retiring_staff", 10, {
&"daily_wage": 125,
&"employment_kind": &"ADULT_CONTRACT",
&"unpaid_wages": 375,
})
service.register_staff(staff)
var result = service.retire_staff(staff.staff_id, 50)
_expect(result.is_success() and not service.has_staff(staff.staff_id), "retirement removes active Staff after cleanup")
_expect(service.get_departed_unpaid_wages(staff.staff_id) == 375, "retirement preserves unpaid wages by former Staff ID")
_expect(service.get_daily_payroll_rows().is_empty() and service.get_daily_payroll_total() == 0, "departed Staff is removed from future daily wage expense")
var snapshot: Dictionary = service.create_snapshot()
_expect(snapshot[&"departed_payroll"][0][&"unpaid_wages"] == 375, "departed payroll liability is included in Save snapshot")
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "departed payroll snapshot restores exactly")
var before_corruption := restored.create_snapshot()
var malformed: Dictionary = snapshot.duplicate(true)
malformed[&"departed_payroll"][0][&"unpaid_wages"] = "375"
_expect(not restored.restore_snapshot(malformed), "malformed departed payroll amount is rejected")
_expect(restored.create_snapshot() == before_corruption, "departed payroll corruption rejection is atomic")
func _test_daily_payroll_settlement_lifecycle() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"staff_z", 10, {&"daily_wage": 40}))
service.register_staff(_staff(&"staff_a", 10, {
&"daily_wage": 100,
&"employment_kind": &"ADULT_CONTRACT",
&"unpaid_wages": 50,
}))
service.register_staff(_staff(&"former", 10, {
&"daily_wage": 80,
&"unpaid_wages": 90,
}))
service.remove_staff(&"former", &"CONTRACT_ENDED", true, 0)
var preview: Dictionary = service.preview_daily_payroll(1)
var expected_rows: Array[Dictionary] = [
{
&"staff_id": &"former", &"payroll_status": &"DEPARTED", &"employment_kind": &"",
&"daily_wage": 0, &"prior_arrears": 90, &"new_wages": 0, &"total_due": 90,
},
{
&"staff_id": &"staff_a", &"payroll_status": &"ACTIVE", &"employment_kind": &"ADULT_CONTRACT",
&"daily_wage": 100, &"prior_arrears": 50, &"new_wages": 100, &"total_due": 150,
},
{
&"staff_id": &"staff_z", &"payroll_status": &"ACTIVE", &"employment_kind": &"FREE_HIRE",
&"daily_wage": 40, &"prior_arrears": 0, &"new_wages": 40, &"total_due": 40,
},
]
_expect(preview[&"ok"] and preview[&"rows"] == expected_rows, "payroll preview has an exact uniform shape and lexical Staff order")
_expect(
preview[&"new_wages_total"] == 140
and preview[&"prior_arrears_total"] == 140
and preview[&"total_due"] == 280,
"payroll preview strictly separates new wages, prior arrears and total due"
)
preview[&"rows"][0][&"total_due"] = 999
_expect(service.get_departed_unpaid_wages(&"former") == 90, "payroll preview is a defensive copy")
var first = service.apply_daily_payroll_settlement(
&"payroll.day.1",
1,
{&"staff_a": 150, &"former": 30, &"staff_z": 0}
)
_expect(first.is_success() and not first.details[&"replayed"], "first payroll settlement commits once")
_expect(
first.details[&"paid_total"] == 180
and first.details[&"unpaid_after_total"] == 100,
"full, partial and zero payments settle in one atomic batch"
)
_expect(service.get_staff(&"staff_a").unpaid_wages == 0, "fully paid active Staff has no arrears")
_expect(service.get_staff(&"staff_z").unpaid_wages == 40, "zero payment carries the new active wage forward")
_expect(service.get_departed_unpaid_wages(&"former") == 60, "partial departed payment reduces only preserved debt")
_expect(not service.preview_daily_payroll(1)[&"ok"], "settled day cannot be previewed as a second accrual")
var obligations := service.get_payroll_obligations()
_expect(
obligations.map(func(row: Dictionary) -> StringName: return row[&"staff_id"]) == [&"former", &"staff_a", &"staff_z"],
"Operations obligation query is ordered across active and departed Staff"
)
obligations[0][&"unpaid_wages"] = 999
_expect(service.get_departed_unpaid_wages(&"former") == 60, "obligation query returns defensive row copies")
var second_preview := service.preview_daily_payroll(2)
_expect(
second_preview[&"new_wages_total"] == 140
and second_preview[&"prior_arrears_total"] == 100
and second_preview[&"total_due"] == 240,
"next day accrues active wages once while retaining all arrears"
)
var second = service.apply_daily_payroll_settlement(
&"payroll.day.2",
2,
{&"former": 60, &"staff_a": 50, &"staff_z": 80}
)
_expect(second.is_success() and second.details[&"paid_total"] == 190, "next-day partial allocation commits")
_expect(service.get_departed_staff_ids().is_empty(), "full departed paydown removes the departed liability row")
_expect(
service.get_staff(&"staff_a").unpaid_wages == 50
and service.get_staff(&"staff_z").unpaid_wages == 0,
"active Staff carry only their exact post-payment balances"
)
service.remove_staff(&"staff_a", &"CONTRACT_ENDED", true, 10)
_expect(service.get_departed_unpaid_wages(&"staff_a") == 50, "removal after settlement preserves remaining active arrears")
var third_preview := service.preview_daily_payroll(3)
_expect(
third_preview[&"rows"][0][&"staff_id"] == &"staff_a"
and third_preview[&"rows"][0][&"new_wages"] == 0
and third_preview[&"rows"][0][&"total_due"] == 50,
"departed Staff never accrue another daily wage"
)
_expect(
service.apply_daily_payroll_settlement(&"payroll.day.3", 3, {&"staff_a": 50, &"staff_z": 40}).is_success(),
"later settlement can fully clear active and departed obligations"
)
_expect(service.get_departed_staff_ids().is_empty() and service.get_staff(&"staff_z").unpaid_wages == 0, "full settlement clears every liability")
func _test_payroll_validation_and_idempotency() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"paid_staff", 10, {&"daily_wage": 100}))
var pristine := service.create_snapshot()
_expect(not service.preview_daily_payroll(0)[&"ok"], "zero payroll day is rejected")
_expect(not service.preview_daily_payroll("1")[&"ok"], "string payroll day is rejected rather than coerced")
_expect(not service.apply_daily_payroll_settlement(&"bad id", 1, {}).is_success(), "unstable settlement ID is rejected")
_expect(not service.apply_daily_payroll_settlement(&"bad_payments", 1, []).is_success(), "non-Dictionary payments are rejected")
_expect(not service.apply_daily_payroll_settlement(&"typed", 1, {&"paid_staff": "100"}).is_success(), "string payment is rejected")
_expect(not service.apply_daily_payroll_settlement(&"float", 1, {&"paid_staff": 100.0}).is_success(), "floating payment is rejected")
_expect(not service.apply_daily_payroll_settlement(&"negative", 1, {&"paid_staff": -1}).is_success(), "negative payment is rejected")
_expect(not service.apply_daily_payroll_settlement(&"unknown", 1, {&"missing": 0}).is_success(), "unknown Staff payment is rejected even when zero")
_expect(not service.apply_daily_payroll_settlement(&"overpay", 1, {&"paid_staff": 101}).is_success(), "payment above exact obligation is rejected")
_expect(service.create_snapshot() == pristine, "all malformed and overpayment requests are atomic")
var committed = service.apply_daily_payroll_settlement(&"stable.day.1", 1, {&"paid_staff": 25})
_expect(committed.is_success() and service.get_staff(&"paid_staff").unpaid_wages == 75, "valid partial settlement commits exact arrears")
var committed_snapshot := service.create_snapshot()
var replay = service.apply_daily_payroll_settlement(&"stable.day.1", 1, {&"paid_staff": 25})
_expect(replay.is_success() and replay.details[&"replayed"], "same settlement ID and fingerprint replays successfully")
_expect(service.create_snapshot() == committed_snapshot, "idempotent replay performs no second wage accrual or payment")
_expect(not service.apply_daily_payroll_settlement(&"stable.day.1", 1, {&"paid_staff": 24}).is_success(), "same settlement ID rejects a conflicting payment fingerprint")
_expect(not service.apply_daily_payroll_settlement(&"stable.day.1", 2, {&"paid_staff": 25}).is_success(), "same settlement ID rejects a conflicting day")
_expect(not service.apply_daily_payroll_settlement(&"other.day.1", 1, {}).is_success(), "different settlement ID cannot accrue the same day twice")
_expect(service.create_snapshot() == committed_snapshot, "all idempotency conflicts preserve committed state")
func _test_payroll_snapshot_migration_and_tamper() -> void:
var legacy_source := StaffServiceType.new()
legacy_source.register_staff(_staff(&"active_debt", 10, {&"daily_wage": 75, &"unpaid_wages": 125}))
legacy_source.register_staff(_staff(&"departed_debt", 10, {&"daily_wage": 50, &"unpaid_wages": 200}))
legacy_source.remove_staff(&"departed_debt", &"LEFT", true, 0)
var legacy: Dictionary = legacy_source.create_snapshot()
legacy[&"version"] = 1
legacy.erase(&"last_advanced_minute")
legacy.erase(&"payroll_settlements")
for staff_snapshot: Dictionary in legacy[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var migrated := StaffServiceType.new()
_expect(migrated.restore_snapshot(legacy), "v1 StaffService snapshot migrates to empty settlement history")
_expect(
migrated.get_staff(&"active_debt").unpaid_wages == 125
and migrated.get_departed_unpaid_wages(&"departed_debt") == 200,
"v1 migration preserves active and departed unpaid wages"
)
var migrated_snapshot := migrated.create_snapshot()
_expect(migrated_snapshot[&"version"] == 3 and migrated_snapshot[&"payroll_settlements"].is_empty(), "v1 migration emits canonical v3 with empty history")
var service := StaffServiceType.new()
service.register_staff(_staff(&"staff_a", 10, {&"daily_wage": 100, &"unpaid_wages": 20}))
service.register_staff(_staff(&"staff_b", 10, {&"daily_wage": 50}))
service.apply_daily_payroll_settlement(&"snapshot.day.1", 1, {&"staff_a": 30, &"staff_b": 0})
service.apply_daily_payroll_settlement(&"snapshot.day.2", 2, {&"staff_a": 50, &"staff_b": 100})
var snapshot: Dictionary = service.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "canonical v3 payroll history restores")
_expect(restored.create_snapshot() == snapshot, "v3 payroll settlement history round trip is exact")
var v2_snapshot: Dictionary = snapshot.duplicate(true)
v2_snapshot[&"version"] = 2
for staff_snapshot: Dictionary in v2_snapshot[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var v2_migrated := StaffServiceType.new()
_expect(v2_migrated.restore_snapshot(v2_snapshot), "pre-effective-minute v2 payroll history migrates")
_expect(
v2_migrated.create_snapshot()[&"version"] == 3
and v2_migrated.get_staff(&"staff_a").employment_effective_minute == 0,
"v2 migration emits canonical v3 Staff rows at effective minute zero"
)
var before_tamper := restored.create_snapshot()
var fingerprint_tamper: Dictionary = snapshot.duplicate(true)
fingerprint_tamper[&"payroll_settlements"][0][&"fingerprint"] = "0".repeat(64)
_expect(not restored.restore_snapshot(fingerprint_tamper), "tampered payroll fingerprint is rejected")
_expect(restored.create_snapshot() == before_tamper, "fingerprint rejection is atomic")
var total_tamper: Dictionary = snapshot.duplicate(true)
total_tamper[&"payroll_settlements"][0][&"total_due"] += 1
_expect(not restored.restore_snapshot(total_tamper), "tampered payroll derived total is rejected")
_expect(restored.create_snapshot() == before_tamper, "derived total rejection is atomic")
var amount_tamper: Dictionary = snapshot.duplicate(true)
amount_tamper[&"payroll_settlements"][0][&"rows"][0][&"payment"] = -1
_expect(not restored.restore_snapshot(amount_tamper), "negative historical payment is rejected")
_expect(restored.create_snapshot() == before_tamper, "negative history rejection is atomic")
var order_tamper: Dictionary = snapshot.duplicate(true)
order_tamper[&"payroll_settlements"].reverse()
_expect(not restored.restore_snapshot(order_tamper), "unordered settlement days are rejected")
_expect(restored.create_snapshot() == before_tamper, "history order rejection is atomic")
var duplicate_day: Dictionary = snapshot.duplicate(true)
duplicate_day[&"payroll_settlements"][1][&"day"] = 1
_expect(not restored.restore_snapshot(duplicate_day), "duplicate settlement day is rejected")
_expect(restored.create_snapshot() == before_tamper, "duplicate day rejection is atomic")
var extra_field: Dictionary = snapshot.duplicate(true)
extra_field[&"payroll_settlements"][0][&"unexpected"] = true
_expect(not restored.restore_snapshot(extra_field), "unknown current payroll history field is rejected")
_expect(restored.create_snapshot() == before_tamper, "unknown field rejection is atomic")
var typed_day: Dictionary = snapshot.duplicate(true)
typed_day[&"payroll_settlements"][0][&"day"] = "1"
_expect(not restored.restore_snapshot(typed_day), "string historical day is rejected rather than coerced")
_expect(restored.create_snapshot() == before_tamper, "historical type rejection is atomic")
func _test_remove_staff_cleans_schedule_graph() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"scheduled_departure", 10, {&"state": StaffRuntimeType.State.OFF_DUTY})
service.register_staff(staff)
service.set_schedule_override(staff.staff_id, 3, {&"start_minute_of_day": 420})
service.set_schedule_override(staff.staff_id, 4, {&"assigned_facility_id": &"remote"})
service.create_standard_shift(staff.staff_id, &"DAY", 3, &"reception", &"departure_day_3")
service.create_standard_shift(staff.staff_id, &"DAY", 4, &"warehouse", &"departure_day_4")
var removal = service.remove_staff(staff.staff_id, &"CONTRACT_ENDED", true, 100)
_expect(removal.is_success() and not service.has_staff(staff.staff_id), "Staff removal succeeds with future schedule records")
_expect(service.get_shifts().is_empty(), "Staff removal deletes every related Shift instance")
_expect(
service.get_schedule_override(staff.staff_id, 3).is_empty()
and service.get_schedule_override(staff.staff_id, 4).is_empty(),
"Staff removal deletes every related ScheduleOverride"
)
_expect(
removal.details[&"removed_shift_ids"] == [&"departure_day_3", &"departure_day_4"]
and removal.details[&"removed_override_days"] == [3, 4],
"Staff removal reports deterministic schedule cleanup"
)
_expect(service.validate_invariants()[&"ok"], "Staff removal leaves no orphan schedule graph")
var snapshot := service.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "post-removal StaffService snapshot restores")
_expect(restored.create_snapshot() == snapshot, "post-removal snapshot round trip is exact")
func _test_snapshot_round_trip_and_atomic_restore() -> void:
var original := StaffServiceType.new()
var staff := _staff(&"snapshot_staff", 20)
original.register_staff(staff)
original.create_standard_shift(staff.staff_id, &"NIGHT", 0, &"warehouse", &"snapshot_shift")
original.advance_one_minute(1_080)
var task := _task(&"snapshot_task", 70, 1_080)
original.register_task(task)
original.dispatch_now(1_080)
var snapshot: Dictionary = original.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "valid StaffService snapshot restores")
_expect(restored.create_snapshot() == snapshot, "Staff, Shift, Task and Dispatcher round trip exactly")
_expect(restored.validate_invariants()[&"ok"], "restored Staff graph satisfies pair invariants")
var before_invalid: Dictionary = restored.create_snapshot()
var invalid: Dictionary = snapshot.duplicate(true)
invalid[&"tasks"][0][&"assigned_staff_id"] = "missing_staff"
_expect(not restored.restore_snapshot(invalid), "snapshot with orphan Task is rejected")
_expect(restored.create_snapshot() == before_invalid, "failed restore leaves live Staff state untouched")
func _test_orphan_recovery_is_atomic() -> void:
var release_outcome := {&"ok": false}
var release_calls: Array = []
var service := StaffServiceType.new()
service.configure_callbacks(
Callable(),
func(ids: Array[StringName], _task_value: StaffTask, _minute: int) -> Dictionary:
release_calls.append(ids.duplicate())
return {&"ok": bool(release_outcome[&"ok"])}
)
service.register_staff(_staff(&"live_staff", 10))
var live_before: Dictionary = service.create_snapshot()
var reopened_signals: Array[StringName] = []
service.task_reopened.connect(
func(task_id: StringName, _reason: StringName) -> void:
reopened_signals.append(task_id)
)
var candidate := StaffServiceType.new().create_snapshot()
var orphan_a := _task(&"orphan_a", 50, 0, {
&"required_items": {&"tool": 1},
&"state": StaffTaskType.State.RESERVED,
&"assigned_staff_id": &"missing_a",
&"reservation_ids": [&"reservation_a"],
})
var orphan_b := _task(&"orphan_b", 40, 0, {
&"required_items": {&"tool": 1},
&"state": StaffTaskType.State.RESERVED,
&"assigned_staff_id": &"missing_b",
&"reservation_ids": [&"reservation_b"],
})
candidate[&"tasks"] = [orphan_a.create_snapshot(), orphan_b.create_snapshot()]
_expect(not service.restore_snapshot_with_orphan_recovery(candidate), "failed external Reservation release rejects orphan recovery")
_expect(service.create_snapshot() == live_before and reopened_signals.is_empty(), "failed orphan recovery preserves live Staff snapshot and emits no repair signals")
_expect(release_calls == [[&"reservation_a", &"reservation_b"]], "all orphan Reservations use one external release transaction")
_expect(service.last_recovery_repair_report[&"release_failed_task_ids"] == [&"orphan_a", &"orphan_b"], "failed transaction reports every unrepaired Task exactly")
release_outcome[&"ok"] = true
release_calls.clear()
_expect(service.restore_snapshot_with_orphan_recovery(candidate), "successful external release commits the isolated repaired candidate")
_expect(release_calls == [[&"reservation_a", &"reservation_b"]], "successful recovery still performs exactly one Reservation transaction")
_expect(service.get_task(&"orphan_a").state == StaffTaskType.State.OPEN and service.get_task(&"orphan_b").state == StaffTaskType.State.OPEN, "committed recovery reopens only the two orphan Tasks")
_expect(reopened_signals == [&"orphan_a", &"orphan_b"] and service.validate_invariants()[&"ok"], "repair signals publish only after the valid candidate commits")
var committed_before: Dictionary = service.create_snapshot()
var signal_count_before := reopened_signals.size()
var unrelated_damage: Dictionary = candidate.duplicate(true)
var missing_staff_shift := ShiftInstanceType.create_standard(&"invalid_shift", &"missing_shift_staff", &"DAY", 0)
unrelated_damage[&"shifts"] = [missing_staff_shift.create_snapshot()]
release_calls.clear()
_expect(not service.restore_snapshot_with_orphan_recovery(unrelated_damage), "recovery rejects non-orphan Shift corruption mixed with repairable Tasks")
_expect(release_calls.is_empty(), "non-repairable damage is rejected before any external Reservation release")
_expect(service.create_snapshot() == committed_before and reopened_signals.size() == signal_count_before, "non-repairable recovery failure preserves live snapshot and signals atomically")
var exact := StaffServiceType.new()
_expect(exact.restore_snapshot_with_orphan_recovery(committed_before), "ordinary valid snapshot also restores through the recovery boundary")
_expect(exact.create_snapshot() == committed_before, "ordinary recovery-boundary round trip remains exact")
func _staff(
staff_id: StringName,
operation_skill: int,
options: Dictionary = {}
) -> StaffRuntime:
var configuration := {
&"state": StaffRuntimeType.State.AVAILABLE,
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work", &"rest"],
&"skills": {&"operation": operation_skill},
}
for key: Variant in options:
configuration[key] = options[key]
return StaffRuntimeType.new().configure(staff_id, &"worker", configuration)
func _task(
task_id: StringName,
priority: int,
created_at: int,
options: Dictionary = {}
) -> StaffTask:
var configuration := {
&"required_role_tags": [&"worker"],
&"required_skill_min": {&"operation": 1},
&"task_tags": [&"work"],
&"estimated_minutes": 5,
&"interrupt_level": 3,
}
for key: Variant in options:
configuration[key] = options[key]
return StaffTaskType.new().configure(task_id, &"work", priority, created_at, configuration)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://djxblic7ohru8
@@ -0,0 +1,473 @@
extends SceneTree
const PreviewServiceType = preload("res://scripts/relationship/placement_risk_preview_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S4: Placement Risk Preview Service")
_test_half_open_multiday_contact_and_replacement()
_test_deterministic_order_across_facilities()
_test_high_contact_branch_and_route_confirmation()
_test_content_filter_changes_presentation_only()
_test_determinism_purity_and_statelessness()
_test_avoidance_suggestions_reduce_contact_and_risk()
_test_strict_fail_closed_schema()
_test_collection_limits_and_derived_values()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_half_open_multiday_contact_and_replacement() -> void:
var service := PreviewServiceType.new()
var adjacent := _base_request(false)
adjacent[&"period"] = {&"start_at": 0, &"end_at": 120}
adjacent[&"actors"][0][&"existing_shifts"] = [
_shift("hero_edge", "shared", 0, 60),
]
adjacent[&"actors"][1][&"existing_shifts"] = [
_shift("rival_edge", "shared", 60, 120),
]
var adjacent_result: Dictionary = service.preview(adjacent)
_expect(adjacent_result[&"ok"], "adjacent half-open fixture previews")
_expect(adjacent_result[&"totals"][&"contact_minutes"] == 0, "adjacent half-open Shifts add no phantom contact")
_expect(adjacent_result[&"totals"][&"shield_attack_candidates"] == 0, "adjacent half-open Shifts add no attack candidate")
var one_minute := adjacent.duplicate(true)
one_minute[&"actors"][1][&"existing_shifts"][0][&"start_at"] = 59
var one_minute_result: Dictionary = service.preview(one_minute)
_expect(one_minute_result[&"ok"], "one-minute overlap fixture previews")
_expect(one_minute_result[&"totals"][&"contact_minutes"] == 1, "half-open overlap counts the exact minute")
var edge_interval: Dictionary = one_minute_result[&"relationships"][0][&"contacts_by_facility"][0][&"intervals"][0]
_expect(edge_interval[&"start_at"] == 59 and edge_interval[&"end_at"] == 60, "half-open interval bounds remain exact")
var high_result: Dictionary = service.preview(_base_request(true))
_expect(high_result[&"ok"], "multi-day proposal fixture previews")
var relationship: Dictionary = high_result[&"relationships"][0]
_expect(relationship[&"total_contact_minutes"] == 240, "same-ID proposals replace existing rival Shifts instead of appending them")
_expect(relationship[&"contacts_by_facility"].size() == 1, "shared contacts group by facility")
var intervals: Array = relationship[&"contacts_by_facility"][0][&"intervals"]
_expect(intervals.size() == 2, "separate days retain separate overlap intervals")
_expect(
intervals[0][&"start_at"] == 90
and intervals[0][&"end_at"] == 240
and intervals[0][&"minutes"] == 150,
"adjacent heroine Shifts merge without duplicate minutes"
)
_expect(intervals[0][&"heroine_shift_ids"] == ["hero_day_a", "hero_day_b"], "merged interval preserves deterministic source Shift IDs")
_expect(
intervals[1][&"start_at"] == 1_500
and intervals[1][&"end_at"] == 1_590
and intervals[1][&"minutes"] == 90,
"absolute-minute contact works across the day boundary"
)
func _test_high_contact_branch_and_route_confirmation() -> void:
var service := PreviewServiceType.new()
var low: Dictionary = service.preview(_base_request(false))
var high: Dictionary = service.preview(_base_request(true))
_expect(low[&"ok"] and high[&"ok"], "low and high contact branches both preview")
_expect(low[&"totals"][&"contact_minutes"] == 0, "separate existing facilities are a zero-contact branch")
_expect(low[&"totals"][&"shield_attack_candidates"] == 0, "low-contact branch creates no Shield attack candidates")
_expect(low[&"totals"][&"triggerable_events"] == 0, "low-contact branch creates no Event candidates")
_expect(high[&"totals"][&"shield_attack_candidates"] == 5, "only high contact creates the deterministic Shield attack sequence")
_expect(high[&"totals"][&"triggerable_events"] == 1, "only high contact unlocks the threshold Event")
var relationship: Dictionary = high[&"relationships"][0]
var triggers: Array[int] = []
for attack: Dictionary in relationship[&"shield_attack_candidates"]:
triggers.append(int(attack[&"trigger_at"]))
_expect(triggers == [119, 179, 239, 1_529, 1_589], "Shield candidates use stable 30-minute then 60-minute trigger points")
_expect(relationship[&"projected_values"][&"shield_current"] == 0, "candidate mechanics clamp projected Shield at zero")
_expect(relationship[&"projected_values"][&"rival_bond"] == 49, "post-break candidates project bounded Rival Bond gains")
var candidate: Dictionary = relationship[&"triggerable_events"][0]
_expect(candidate[&"event_id"] == "danger_event", "eligible Event is selected by mechanical thresholds")
_expect(_reason_codes(candidate[&"reasons"]) == [
"CONTACT_THRESHOLD_MET", "SHIELD_THRESHOLD_MET", "RIVAL_BOND_RANGE_MET", "SHARED_FACILITY",
], "Event candidate reports every deterministic eligibility reason")
_expect(candidate[&"mechanics"][&"route_lock_requested"] == "EXCLUSIVE_LOCKED", "candidate retains requested exclusive Route Lock")
_expect(candidate[&"mechanics"][&"route_lock_preview"] == "EXCLUSIVE_PENDING", "preview downgrades unconfirmed exclusive lock to pending")
_expect(not candidate[&"mechanics"][&"route_lock_confirmed"], "Event candidate never claims an unconfirmed Route Lock")
var warning: Dictionary = relationship[&"route_lock_warning"]
_expect(warning[&"confirmation_required"], "exclusive candidate emits a confirmation-required warning")
_expect(not warning[&"preview_confirms_lock"], "read-only preview never confirms Route Lock")
_expect(warning[&"projected_route_lock"] == "EXCLUSIVE_PENDING", "mechanical summary keeps Route Lock pending")
_expect(relationship[&"risk_level"] == "CRITICAL", "confirmation-required high-contact branch is critical risk")
var soft_request := _base_request(true)
soft_request[&"events"][1][&"route_lock"] = "SOFT"
soft_request[&"events"][1][&"route_id"] = ""
var soft_result: Dictionary = service.preview(soft_request)
_expect(soft_result[&"relationships"][0][&"route_lock_warning"][&"projected_route_lock"] == "SOFT", "non-exclusive candidate Route Lock is still projected mechanically")
_expect(not soft_result[&"relationships"][0][&"route_lock_warning"][&"confirmation_required"], "SOFT Route Lock does not invent an exclusive confirmation")
var vulnerable_low := _base_request(false)
vulnerable_low[&"relationships"][0][&"shield_current"] = 0
vulnerable_low[&"relationships"][0][&"rival_bond"] = 90
_expect(service.preview(vulnerable_low)[&"relationships"][0][&"risk_level"] == "NONE", "zero-contact placement is not mislabeled from vulnerable baseline values alone")
func _test_deterministic_order_across_facilities() -> void:
var service := PreviewServiceType.new()
var request := _base_request(false)
request[&"period"] = {&"start_at": 0, &"end_at": 200}
request[&"actors"][0][&"existing_shifts"] = [
_shift("hero_zeta", "zeta", 0, 60),
_shift("hero_alpha", "alpha", 100, 160),
]
request[&"actors"][1][&"existing_shifts"] = [
_shift("rival_zeta", "zeta", 0, 60),
_shift("rival_alpha", "alpha", 100, 160),
]
var result: Dictionary = service.preview(request)
_expect(result[&"ok"], "multi-facility ordering fixture previews")
var relationship: Dictionary = result[&"relationships"][0]
_expect(
[
relationship[&"contacts_by_facility"][0][&"facility_id"],
relationship[&"contacts_by_facility"][1][&"facility_id"],
] == ["alpha", "zeta"],
"facility summaries use stable lexical order"
)
var triggers: Array[int] = []
for attack: Dictionary in relationship[&"shield_attack_candidates"]:
triggers.append(int(attack[&"trigger_at"]))
_expect(triggers == [29, 129], "cross-facility Shield candidates use chronological mechanical order")
func _test_content_filter_changes_presentation_only() -> void:
var service := PreviewServiceType.new()
var outputs: Dictionary = {}
for mode: String in ["NONE", "FADE", "REPLACE", "DISABLE"]:
var request := _base_request(true)
request[&"content_filter"][&"mode"] = mode
outputs[mode] = service.preview(request)
_expect(outputs[mode][&"ok"], "%s Content Filter preview succeeds" % mode)
var baseline: Dictionary = outputs["NONE"][&"relationships"][0]
var expected_modes := {
"NONE": "FULL",
"FADE": "FADE",
"REPLACE": "REPLACE",
"DISABLE": "DISABLE",
}
for mode: String in ["NONE", "FADE", "REPLACE", "DISABLE"]:
var current: Dictionary = outputs[mode][&"relationships"][0]
_expect(current[&"mechanical_summary"] == baseline[&"mechanical_summary"], "%s preserves risk, cost, cooldown, lock, and projected values" % mode)
_expect(current[&"contacts_by_facility"] == baseline[&"contacts_by_facility"], "%s preserves mechanical contact intervals" % mode)
_expect(current[&"shield_attack_candidates"] == baseline[&"shield_attack_candidates"], "%s preserves Shield attack mechanics" % mode)
_expect(current[&"triggerable_events"][0][&"mechanics"] == baseline[&"triggerable_events"][0][&"mechanics"], "%s preserves Event mechanics" % mode)
_expect(current[&"triggerable_events"][0][&"reasons"] == baseline[&"triggerable_events"][0][&"reasons"], "%s preserves Event reasons" % mode)
_expect(current[&"triggerable_events"][0][&"presentation"][&"mode"] == expected_modes[mode], "%s changes only the Event presentation mode" % mode)
_expect(outputs["REPLACE"][&"relationships"][0][&"triggerable_events"][0][&"presentation"][&"display_event_id"] == "safe_notice", "REPLACE displays the validated safe replacement")
_expect(not outputs["DISABLE"][&"relationships"][0][&"triggerable_events"][0][&"presentation"][&"visible"], "DISABLE hides presentation without removing mechanics")
func _test_determinism_purity_and_statelessness() -> void:
var service := PreviewServiceType.new()
var request := _base_request(true)
var before := request.duplicate(true)
var first: Dictionary = service.preview(request)
var second: Dictionary = service.preview(request)
_expect(first == second, "identical request produces exact deterministic output")
_expect(request == before, "preview does not mutate any nested input data")
var shuffled := request.duplicate(true)
shuffled[&"actors"].reverse()
shuffled[&"events"].reverse()
for actor: Dictionary in shuffled[&"actors"]:
actor[&"existing_shifts"].reverse()
actor[&"proposed_shifts"].reverse()
_expect(service.preview(shuffled) == first, "input array order does not change canonical Preview order")
var low: Dictionary = service.preview(_base_request(false))
var third: Dictionary = service.preview(request)
_expect(low[&"totals"][&"contact_minutes"] == 0 and third == first, "interleaved preview calls retain no service state")
_expect(not service.has_method("create_snapshot") and not service.has_method("restore_snapshot"), "stateless preview exposes no snapshot lifecycle")
_expect(not service.has_method("configure_callbacks"), "pure preview exposes no callback or re-entry surface")
func _test_avoidance_suggestions_reduce_contact_and_risk() -> void:
var service := PreviewServiceType.new()
var request := _base_request(true)
var original: Dictionary = service.preview(request)
var relationship: Dictionary = original[&"relationships"][0]
var suggestions: Array = relationship[&"avoidance_suggestions"]
_expect(suggestions.size() == 2, "preview returns both facility and time separation suggestions")
_expect(suggestions[0][&"kind"] == "CHANGE_FACILITY" and suggestions[1][&"kind"] == "SEPARATE_TIME", "avoidance suggestions have deterministic kind order")
for suggestion: Dictionary in suggestions:
_expect(suggestion[&"reduction_minutes"] > 0, "%s advertises only a positive deterministic reduction" % suggestion[&"kind"])
var applied := request.duplicate(true)
_replace_actor_proposal(applied, String(suggestion[&"actor_id"]), suggestion[&"proposed_shift"])
var reduced: Dictionary = service.preview(applied)
_expect(reduced[&"ok"], "%s proposal is directly previewable" % suggestion[&"kind"])
_expect(reduced[&"totals"][&"contact_minutes"] == suggestion[&"contact_minutes_after"], "%s predicted contact matches reapplied result" % suggestion[&"kind"])
_expect(reduced[&"totals"][&"contact_minutes"] < original[&"totals"][&"contact_minutes"], "%s lowers shared-facility contact" % suggestion[&"kind"])
_expect(reduced[&"totals"][&"shield_attack_candidates"] < original[&"totals"][&"shield_attack_candidates"], "%s lowers Shield attack candidates" % suggestion[&"kind"])
_expect(_risk_rank(reduced[&"relationships"][0][&"risk_level"]) < _risk_rank(relationship[&"risk_level"]), "%s lowers the resulting risk level" % suggestion[&"kind"])
_expect(request == _base_request(true), "applying suggestions to copies leaves source proposal unchanged")
func _test_strict_fail_closed_schema() -> void:
var service := PreviewServiceType.new()
_expect_rejected(service.preview(null), "INVALID_TYPE", "non-Dictionary request is rejected")
var unknown_top := _base_request(false)
unknown_top[&"derived_risk"] = "NONE"
_expect_rejected(service.preview(unknown_top), "INVALID_SCHEMA", "unknown top-level field is rejected")
var missing_top := _base_request(false)
missing_top.erase(&"events")
_expect_rejected(service.preview(missing_top), "INVALID_SCHEMA", "missing top-level field is rejected")
var fake_version := _base_request(false)
fake_version[&"schema_version"] = "1"
_expect_rejected(service.preview(fake_version), "UNSUPPORTED_SCHEMA", "numeric-string schema version is rejected")
var fake_time := _base_request(false)
fake_time[&"period"][&"start_at"] = 0.0
_expect_rejected(service.preview(fake_time), "INVALID_TYPE", "float period bound is rejected")
var long_period := _base_request(false)
long_period[&"period"][&"end_at"] = 14 * 1_440 + 1
_expect_rejected(service.preview(long_period), "OUT_OF_RANGE", "period beyond the 14-day limit is rejected")
var actor_unknown := _base_request(false)
actor_unknown[&"actors"][0][&"derived_availability"] = true
_expect_rejected(service.preview(actor_unknown), "INVALID_SCHEMA", "unknown actor field is rejected")
var role_fake := _base_request(false)
role_fake[&"actors"][0][&"role"] = "heroine"
_expect_rejected(service.preview(role_fake), "INVALID_ENUM", "case-forged role enum is rejected")
var shift_unknown := _base_request(false)
shift_unknown[&"actors"][0][&"existing_shifts"][0][&"duration"] = 120
_expect_rejected(service.preview(shift_unknown), "INVALID_SCHEMA", "derived Shift duration is rejected")
var shift_string := _base_request(false)
shift_string[&"actors"][0][&"existing_shifts"][0][&"start_at"] = "60"
_expect_rejected(service.preview(shift_string), "INVALID_TYPE", "numeric-string Shift bound is rejected")
var double_booked := _base_request(true)
double_booked[&"actors"][1][&"proposed_shifts"][1][&"start_at"] = 200
double_booked[&"actors"][1][&"proposed_shifts"][1][&"end_at"] = 300
_expect_rejected(service.preview(double_booked), "ACTOR_DOUBLE_BOOKED", "overlapping effective Shift placement is rejected")
var filter_unknown := _base_request(false)
filter_unknown[&"content_filter"][&"visible"] = false
_expect_rejected(service.preview(filter_unknown), "INVALID_SCHEMA", "unknown Content Filter field is rejected")
var bad_filter_enum := _base_request(false)
bad_filter_enum[&"content_filter"][&"mode"] = "HIDE"
_expect_rejected(service.preview(bad_filter_enum), "INVALID_ENUM", "unknown Content Filter mode is rejected")
var event_unknown := _base_request(false)
event_unknown[&"events"][0][&"eligible"] = true
_expect_rejected(service.preview(event_unknown), "INVALID_SCHEMA", "derived Event eligibility is rejected")
var event_missing := _base_request(false)
event_missing[&"events"][0].erase(&"cooldown_minutes")
_expect_rejected(service.preview(event_missing), "INVALID_SCHEMA", "missing Event field is rejected")
var event_float := _base_request(false)
event_float[&"events"][0][&"cost_gold"] = 75.0
_expect_rejected(service.preview(event_float), "INVALID_TYPE", "float Event cost is rejected")
var event_route := _base_request(false)
event_route[&"events"][0][&"route_lock"] = "LOCKED"
_expect_rejected(service.preview(event_route), "INVALID_ENUM", "forged Route Lock enum is rejected")
var event_relationship := _base_request(false)
event_relationship[&"events"][0][&"relationship_id"] = "missing"
_expect_rejected(service.preview(event_relationship), "MISSING_RELATIONSHIP", "Event cannot reference an unknown relationship")
var duplicate_pair := _base_request(false)
var repeated_relationship: Dictionary = duplicate_pair[&"relationships"][0].duplicate(true)
repeated_relationship[&"relationship_id"] = "duplicate_pair"
duplicate_pair[&"relationships"].append(repeated_relationship)
_expect_rejected(service.preview(duplicate_pair), "DUPLICATE_PAIR", "duplicate heroine-rival pair cannot double-count Preview totals")
var missing_replacement := _base_request(true)
missing_replacement[&"content_filter"][&"mode"] = "REPLACE"
missing_replacement[&"events"][1][&"replacement_event_id"] = "missing"
_expect_rejected(service.preview(missing_replacement), "REPLACEMENT_MISSING", "REPLACE fails closed when safe replacement is absent")
var unsafe_replacement := _base_request(true)
unsafe_replacement[&"content_filter"][&"mode"] = "REPLACE"
unsafe_replacement[&"events"][0][&"content_rating"] = "RESTRICTED"
_expect_rejected(service.preview(unsafe_replacement), "REPLACEMENT_UNSAFE", "REPLACE rejects a filtered replacement chain")
func _test_collection_limits_and_derived_values() -> void:
var service := PreviewServiceType.new()
var too_many_actors := _base_request(false)
while too_many_actors[&"actors"].size() <= 64:
too_many_actors[&"actors"].append(too_many_actors[&"actors"][0].duplicate(true))
_expect_rejected(service.preview(too_many_actors), "COLLECTION_LIMIT", "actor collection upper bound is enforced before parsing")
var too_many_shifts := _base_request(false)
too_many_shifts[&"actors"][0][&"existing_shifts"] = []
for index: int in range(65):
too_many_shifts[&"actors"][0][&"existing_shifts"].append(_shift("h_%02d" % index, "solo", index * 10, index * 10 + 5))
_expect_rejected(service.preview(too_many_shifts), "COLLECTION_LIMIT", "per-list Shift upper bound is enforced")
var too_many_effective := _base_request(false)
too_many_effective[&"actors"][0][&"existing_shifts"] = []
for index: int in range(64):
too_many_effective[&"actors"][0][&"existing_shifts"].append(_shift("h_%02d" % index, "solo", index * 10, index * 10 + 5))
too_many_effective[&"actors"][0][&"proposed_shifts"] = [_shift("h_new", "solo", 700, 705)]
_expect_rejected(service.preview(too_many_effective), "COLLECTION_LIMIT", "combined effective Shift upper bound prevents overlap-pair truncation")
var too_many_events := _base_request(false)
too_many_events[&"events"] = []
for _index: int in range(257):
too_many_events[&"events"].append(_safe_event())
_expect_rejected(service.preview(too_many_events), "COLLECTION_LIMIT", "Event definition upper bound is enforced before parsing")
var forged_shield := _base_request(false)
forged_shield[&"relationships"][0][&"shield_current"] = 90
forged_shield[&"relationships"][0][&"shield_max"] = 80
_expect_rejected(service.preview(forged_shield), "INVALID_DERIVED_VALUE", "forged current Shield above Shield Max is rejected")
var duplicate_tag := _base_request(false)
duplicate_tag[&"events"][1][&"content_tags"] = ["intimacy", "intimacy"]
_expect_rejected(service.preview(duplicate_tag), "DUPLICATE_VALUE", "duplicate content tags are rejected")
func _base_request(high_contact: bool) -> Dictionary:
var rival_proposals: Array[Dictionary] = []
if high_contact:
rival_proposals = [
_shift("rival_day", "shared", 90, 240),
_shift("rival_next_day", "shared", 1_500, 1_590),
]
return {
&"schema_version": 1,
&"period": {&"start_at": 0, &"end_at": 2_880},
&"actors": [
{
&"actor_id": "heroine",
&"role": "HEROINE",
&"existing_shifts": [
_shift("hero_day_a", "shared", 60, 180),
_shift("hero_day_b", "shared", 180, 300),
_shift("hero_next_day", "shared", 1_500, 1_620),
],
&"proposed_shifts": [],
&"mechanical_effects": {
&"base_shield_attack": 0,
&"night_shield_attack_bonus": 0,
},
},
{
&"actor_id": "rival",
&"role": "RIVAL",
&"existing_shifts": [
_shift("rival_day", "archive", 90, 240),
_shift("rival_next_day", "archive", 1_500, 1_590),
],
&"proposed_shifts": rival_proposals,
&"mechanical_effects": {
&"base_shield_attack": 8,
&"night_shield_attack_bonus": 4,
},
},
],
&"relationships": [
{
&"relationship_id": "main_pair",
&"heroine_id": "heroine",
&"rival_id": "rival",
&"affection": 55,
&"rival_bond": 40,
&"shield_current": 20,
&"shield_max": 100,
&"route_lock": "NONE",
&"route_id": "",
&"target_resistance": 0,
&"monitoring_bonus": 0,
},
],
&"events": [_safe_event(), _danger_event()],
&"content_filter": {
&"mode": "NONE",
&"maximum_rating": "GENERAL",
&"disabled_tags": [],
},
}
func _safe_event() -> Dictionary:
return {
&"event_id": "safe_notice",
&"relationship_id": "main_pair",
&"priority": 1,
&"min_contact_minutes": 1_000,
&"max_shield_current": 100,
&"min_rival_bond": 0,
&"max_rival_bond": 100,
&"required_facility_id": "",
&"shield_damage": 0,
&"rival_bond_delta": 0,
&"cost_gold": 0,
&"cooldown_minutes": 0,
&"route_lock": "NONE",
&"route_id": "",
&"content_rating": "GENERAL",
&"content_tags": [],
&"filter_behavior": "NONE",
&"replacement_event_id": "",
&"display_name": "A quiet scheduling notice",
}
func _danger_event() -> Dictionary:
return {
&"event_id": "danger_event",
&"relationship_id": "main_pair",
&"priority": 100,
&"min_contact_minutes": 120,
&"max_shield_current": 0,
&"min_rival_bond": 0,
&"max_rival_bond": 100,
&"required_facility_id": "shared",
&"shield_damage": 0,
&"rival_bond_delta": 5,
&"cost_gold": 75,
&"cooldown_minutes": 720,
&"route_lock": "EXCLUSIVE_LOCKED",
&"route_id": "rival_route",
&"content_rating": "RESTRICTED",
&"content_tags": ["intimacy"],
&"filter_behavior": "NONE",
&"replacement_event_id": "safe_notice",
&"display_name": "Restricted encounter",
}
func _shift(shift_id: String, facility_id: String, start_at: int, end_at: int) -> Dictionary:
return {
&"shift_id": shift_id,
&"facility_id": facility_id,
&"start_at": start_at,
&"end_at": end_at,
}
func _replace_actor_proposal(request: Dictionary, actor_id: String, proposal: Dictionary) -> void:
for actor: Dictionary in request[&"actors"]:
if actor[&"actor_id"] != actor_id:
continue
var replaced := false
for index: int in range(actor[&"proposed_shifts"].size()):
if actor[&"proposed_shifts"][index][&"shift_id"] == proposal[&"shift_id"]:
actor[&"proposed_shifts"][index] = proposal.duplicate(true)
replaced = true
break
if not replaced:
actor[&"proposed_shifts"].append(proposal.duplicate(true))
return
func _reason_codes(reasons: Array) -> Array[String]:
var result: Array[String] = []
for reason: Dictionary in reasons:
result.append(String(reason[&"code"]))
return result
func _risk_rank(level: String) -> int:
return ["NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"].find(level)
func _expect_rejected(result: Dictionary, code: String, message: String) -> void:
_expect(not bool(result.get(&"ok", true)) and result.get(&"error_code", "") == code, message)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if condition:
return
_failures += 1
push_error("FAIL: %s" % message)
@@ -0,0 +1 @@
uid://bp0j42sq0baq4
+441
View File
@@ -0,0 +1,441 @@
extends SceneTree
const RelationshipStateType = preload("res://scripts/relationship/relationship_state.gd")
const RelationshipServiceType = preload("res://scripts/relationship/relationship_service.gd")
const EventConditionType = preload("res://scripts/events/event_condition.gd")
const EventActionType = preload("res://scripts/events/event_action.gd")
const EventChoiceType = preload("res://scripts/events/event_choice.gd")
const EventDefinitionType = preload("res://scripts/events/event_definition.gd")
const EventInstanceType = preload("res://scripts/events/event_instance.gd")
const EventServiceType = preload("res://scripts/events/event_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S4: Relationship and Event Service")
_test_relationship_pair_metrics_and_thresholds()
_test_shield_penalty_and_route_confirmation()
_test_relationship_snapshot_atomicity()
_test_condition_tree()
_test_foreground_queue_and_choice_idempotency()
_test_missing_actor_releases_lock_and_cost()
_test_content_replacement_boundary()
_test_transaction_policies()
_test_event_snapshot_round_trip_and_atomicity()
_test_event_recovery_lock_scope_and_strict_filter_parse()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_relationship_pair_metrics_and_thresholds() -> void:
var service := RelationshipServiceType.new()
var created = service.create_relationship(
&"rel_main",
&"zoe",
&"alex",
{
&"trust": 20,
&"respect": 30,
&"affection": 45,
&"fear": 5,
&"dependence": 10,
&"shield_max": 40,
&"shield_current": 35,
}
)
_expect(created.is_success(), "relationship registers")
var state := service.get_relationship(&"rel_main")
_expect(state.actor_a_id == &"alex" and state.actor_b_id == &"zoe", "actor pair normalizes lexically")
_expect(service.find_relationship(&"alex", &"zoe") == state, "normalized pair lookup is direction independent")
_expect(not service.create_relationship(&"rel_duplicate", &"alex", &"zoe").is_success(), "duplicate normalized actor pair is rejected")
_expect(state.get_metric(&"trust") == 20 and state.get_metric(&"respect") == 30, "five canonical metrics are retained")
var gated = service.apply_value_change(&"rel_main", &"affection", 10, 100)
_expect(gated.is_success() and state.get_metric(&"affection") == 49, "background change stops before gated threshold 50")
_expect(gated.details[&"gated_threshold"] == 50, "gated change exposes its required threshold Event")
var candidates := service.get_threshold_candidates()
_expect(candidates.size() == 1 and candidates[0][&"requires_unlock_event"], "threshold crossing registers a candidate without immediate playback")
var unlocked = service.apply_value_change(
&"rel_main",
&"affection",
1,
101,
{&"important": true, &"allow_threshold_crossing": true}
)
_expect(unlocked.is_success() and state.get_metric(&"affection") == 50, "explicit important Event may cross the gated threshold")
service.apply_value_change(&"rel_main", &"trust", 50, 102)
_expect(state.get_metric(&"trust") == 30, "automatic relationship drift is limited to +10 per day")
service.apply_value_change(&"rel_main", &"trust", 50, 1_500)
_expect(state.get_metric(&"trust") == 40, "daily automatic limit resets on a new day")
func _test_shield_penalty_and_route_confirmation() -> void:
var service := RelationshipServiceType.new()
service.create_relationship(
&"rel_shield",
&"owner",
&"target",
{&"shield_max": 100, &"shield_current": 100}
)
var state := service.get_relationship(&"rel_shield")
var damages: Array[int] = []
var penalties: Array[int] = []
for minute: int in range(4):
var result = service.apply_shield_attack(
&"rel_shield",
&"repeat_action",
minute,
{&"base_damage": 20, &"max_damage": 20, &"daily_limit": 4}
)
damages.append(int(result.details[&"damage"]))
penalties.append(int(result.details[&"penalty_percent"]))
_expect(penalties == [0, 25, 50, 75], "repeat Action applies 0/25/50/75 percent penalty stages")
_expect(damages == [20, 15, 10, 5], "repeat penalty changes deterministic Shield damage")
_expect(not service.apply_shield_attack(&"rel_shield", &"repeat_action", 4, {&"base_damage": 20}).is_success(), "daily Action use limit is enforced")
service.apply_shield_attack(&"rel_shield", &"fresh_action", 5, {&"base_damage": 500, &"max_damage": 500})
_expect(state.shield_current == 0, "Shield never falls below zero")
var next_day = service.apply_shield_attack(&"rel_shield", &"repeat_action", 1_440, {&"base_damage": 20, &"max_damage": 20})
_expect(next_day.is_success() and next_day.details[&"penalty_percent"] == 0, "repeat count resets at the day boundary")
var direct = service.request_route_lock(
&"rel_shield",
RelationshipStateType.RouteLock.EXCLUSIVE_LOCKED,
&"route_a"
)
_expect(not direct.is_success() and state.route_lock == RelationshipStateType.RouteLock.NONE, "exclusive route cannot lock without confirmation")
_expect(service.request_route_lock(&"rel_shield", RelationshipStateType.RouteLock.EXCLUSIVE_PENDING, &"route_a").is_success(), "exclusive route first enters pending state")
_expect(not service.confirm_route_lock(&"rel_shield", &"route_b").is_success(), "mismatched route confirmation is rejected")
_expect(service.confirm_route_lock(&"rel_shield", &"route_a").is_success(), "matching explicit confirmation locks the route")
func _test_relationship_snapshot_atomicity() -> void:
var service := RelationshipServiceType.new()
service.create_relationship(&"rel_save", &"b", &"a", {&"trust": 12, &"shield_max": 50, &"shield_current": 25})
service.apply_value_change(&"rel_save", &"trust", 5, 20)
var snapshot: Dictionary = service.create_snapshot()
var restored := RelationshipServiceType.new()
_expect(restored.restore_snapshot(snapshot), "valid Relationship snapshot restores")
_expect(restored.create_snapshot() == snapshot, "Relationship snapshot round trips exactly")
var before: Dictionary = restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"version"] = 999
_expect(not restored.restore_snapshot(corrupt), "unsupported Relationship snapshot is rejected")
_expect(restored.create_snapshot() == before, "failed Relationship restore is atomic")
func _test_condition_tree() -> void:
var condition := EventConditionType.new().configure(
EventConditionType.Operator.ALL,
[],
null,
[
EventConditionType.new().configure(EventConditionType.Operator.GTE, "relationship.trust", 50),
EventConditionType.new().configure(EventConditionType.Operator.HAS_FLAG, "flags", &"met_before"),
EventConditionType.new().configure(EventConditionType.Operator.TIME_IN_WINDOW, "absolute_minute", [1_200, 300]),
EventConditionType.new().configure(EventConditionType.Operator.ENTITY_EXISTS, "actor_id"),
]
)
var context := {
&"relationship": {&"trust": 60},
&"flags": {&"met_before": true},
&"absolute_minute": 1_300,
&"actor_id": &"actor_a",
}
var callbacks := {&"entity_exists": func(actor_id: Variant) -> bool: return actor_id == &"actor_a"}
_expect(condition.is_valid() and condition.evaluate(context, callbacks), "nested data Condition evaluates comparisons, flag, overnight time and entity callback")
var clone := EventConditionType.from_variant(condition.create_snapshot())
_expect(clone != null and clone.evaluate(context, callbacks), "Condition snapshot preserves deterministic evaluation")
context[&"relationship"][&"trust"] = 49
_expect(not clone.evaluate(context, callbacks), "ALL condition fails when one child changes")
func _test_foreground_queue_and_choice_idempotency() -> void:
var actor_live := {&"actor_a": true, &"actor_b": true}
var applied := {&"count": 0}
var service := EventServiceType.new().configure_callbacks(
func(actor_id: StringName) -> bool: return bool(actor_live.get(actor_id, false)),
func(action: EventAction, phase: StringName, _context: Dictionary, token: Variant) -> Dictionary:
if phase == &"validate":
return {&"ok": true}
if phase == &"apply":
var old_value := int(applied[&"count"])
applied[&"count"] = old_value + int(action.payload.get(&"delta", 0))
return {&"ok": true, &"token": old_value}
if phase == &"rollback":
applied[&"count"] = int(token)
return {&"ok": true}
)
service.register_definition(_choice_definition(&"low", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [_delta_action(&"low_add", 1)]))
service.register_definition(_choice_definition(&"high", 50, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [_delta_action(&"high_add", 3)]))
service.enqueue_candidate(&"low", [&"actor_b"], 0, {&"event_instance_id": &"event_low"})
service.enqueue_candidate(&"high", [&"actor_a"], 0, {&"event_instance_id": &"event_high"})
_expect(_instance_ids(service.get_queued_instances()) == [&"event_high", &"event_low"], "Event queue sorts priority descending then deterministic instance ID")
var started = service.start_next_event(0)
_expect(started.is_success() and started.details[&"event_instance_id"] == &"event_high", "highest priority foreground Event starts first")
_expect(service.get_actor_locks() == {&"actor_a": &"event_high"}, "foreground Event locks all actors by stable ID")
_expect(not service.start_next_event(0).is_success(), "only one foreground Event may run at a time")
var choice_token: StringName = started.details[&"choice_instance_id"]
var first = service.submit_choice(&"event_high", choice_token, &"accept", 0)
var second = service.submit_choice(&"event_high", choice_token, &"accept", 0)
_expect(first.is_success() and second.is_success() and second.is_replay, "duplicate Choice submission returns cached replay")
_expect(applied[&"count"] == 3, "duplicate Choice never applies Actions twice")
_expect(service.get_actor_locks().is_empty(), "completed Event releases actor locks")
_expect(service.start_next_event(1).is_success(), "next foreground Event starts after lock release")
func _test_missing_actor_releases_lock_and_cost() -> void:
var actor_live := {&"actor_a": true}
var released: Array = []
var committed: Array = []
var service := EventServiceType.new().configure_callbacks(
func(actor_id: StringName) -> bool: return bool(actor_live.get(actor_id, false)),
func(_action: EventAction, _phase: StringName, _context: Dictionary, _token: Variant) -> Dictionary:
return {&"ok": true},
func(cost: Dictionary, _context: Dictionary) -> Dictionary:
return {&"ok": true, &"token": {&"reserved_gold": int(cost.get(&"gold", 0))}},
func(token: Variant, _context: Dictionary) -> bool:
released.append(token)
return true,
func(token: Variant, _context: Dictionary) -> bool:
committed.append(token)
return true
)
var definition := _choice_definition(&"missing_actor", 20, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [_delta_action(&"noop", 0)], {&"gold": 25})
service.register_definition(definition)
service.enqueue_candidate(&"missing_actor", [&"actor_a"], 10, {&"event_instance_id": &"event_missing"})
var started = service.start_next_event(10)
_expect(started.is_success(), "missing-actor fixture starts while actor exists")
_expect(service.reserve_choice_cost(&"event_missing", &"accept", 10).is_success(), "Choice cost reserves while UI is waiting")
actor_live[&"actor_a"] = false
var audit := service.audit_actor_availability(11)
_expect(audit.size() == 1 and service.get_instance(&"event_missing").state == EventInstanceType.State.CANCELLED, "actor disappearance cancels Event by default policy")
_expect(service.get_actor_locks().is_empty(), "actor disappearance always releases foreground lock")
_expect(released.size() == 1 and committed.is_empty(), "actor disappearance releases reserved Cost without committing it")
func _test_content_replacement_boundary() -> void:
var service := EventServiceType.new()
service.register_definition(EventDefinitionType.new().configure(&"safe_business", {
&"content_rating": &"GENERAL",
&"content_tags": [&"business_conflict"],
}))
service.register_definition(EventDefinitionType.new().configure(&"restricted_with_replacement", {
&"content_rating": &"RESTRICTED",
&"content_tags": [&"restricted_route"],
&"filter_behavior": &"REPLACE",
&"replacement_event_id": &"safe_business",
}))
service.register_definition(EventDefinitionType.new().configure(&"restricted_missing", {
&"content_rating": &"RESTRICTED",
&"content_tags": [&"restricted_route"],
&"filter_behavior": &"REPLACE",
&"replacement_event_id": &"missing_safe_event",
}))
service.configure_content_filter(
EventDefinitionType.FilterBehavior.REPLACE,
EventDefinitionType.ContentRating.GENERAL,
[&"restricted_route"]
)
var replaced := service.enqueue_candidate(&"restricted_with_replacement", [], 0, {&"event_instance_id": &"event_replaced"})
_expect(replaced.is_success() and replaced.details[&"resolved_event_id"] == &"safe_business", "restricted Event resolves only to configured safe replacement ID")
var disabled := service.enqueue_candidate(&"restricted_missing", [], 0)
_expect(not disabled.is_success() and disabled.details[&"reason"] == &"REPLACEMENT_MISSING", "missing replacement safely disables candidate")
_expect(not service.validate_definitions()[&"ok"], "definition audit reports missing replacement boundary")
func _test_transaction_policies() -> void:
var rollback_state := {&"value": 0}
var rollback_service: Variant = _transaction_service(rollback_state)
rollback_service.register_definition(_choice_definition(
&"rollback_event",
10,
EventDefinitionType.TransactionPolicy.ROLLBACK_ALL,
[_delta_action(&"add_first", 5), _failing_action(&"fail_second")]
))
var rollback_result = _run_choice(rollback_service, &"rollback_event", &"rollback_instance")
_expect(not rollback_result.is_success() and rollback_state[&"value"] == 0, "ROLLBACK_ALL reverses prior Action after later failure")
var skip_state := {&"value": 0}
var skip_service: Variant = _transaction_service(skip_state)
skip_service.register_definition(_choice_definition(
&"skip_event",
10,
EventDefinitionType.TransactionPolicy.SKIP_FAILED,
[_delta_action(&"add_one", 2), _failing_action(&"skip_me"), _delta_action(&"add_two", 3)]
))
var skip_result = _run_choice(skip_service, &"skip_event", &"skip_instance")
_expect(skip_result.is_success() and skip_state[&"value"] == 5, "SKIP_FAILED keeps successful Actions and skips only failed Action")
_expect(skip_result.details[&"skipped_action_ids"] == [&"skip_me"], "SKIP_FAILED reports deterministic skipped Action IDs")
var branch_state := {&"value": 0}
var branch_service: Variant = _transaction_service(branch_state)
var branch_definition := _choice_definition(
&"branch_event",
10,
EventDefinitionType.TransactionPolicy.BRANCH_FAILURE,
[_delta_action(&"primary_add", 9), _failing_action(&"primary_fail")]
)
branch_definition.failure_actions = [_delta_action(&"failure_branch_add", 4)]
branch_service.register_definition(branch_definition)
var branch_result = _run_choice(branch_service, &"branch_event", &"branch_instance")
_expect(branch_result.is_success() and branch_state[&"value"] == 4, "BRANCH_FAILURE rolls back primary Actions then applies failure branch")
_expect(branch_result.details[&"branched_failure"], "BRANCH_FAILURE result marks selected failure branch")
func _test_event_snapshot_round_trip_and_atomicity() -> void:
var service := EventServiceType.new()
service.register_definition(_choice_definition(&"save_event", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, []))
service.enqueue_candidate(&"save_event", [&"actor_a"], 100, {&"event_instance_id": &"save_instance"})
service.start_next_event(100)
var snapshot: Dictionary = service.create_snapshot()
var restored := EventServiceType.new()
restored.register_definition(_choice_definition(&"save_event", 10, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, []))
_expect(restored.restore_snapshot(snapshot), "Event queue, Choice token and Actor Locks restore")
_expect(restored.create_snapshot() == snapshot, "active foreground Event snapshot round trips exactly")
_expect(restored.validate_invariants()[&"ok"], "restored EventService satisfies Actor Lock invariants")
var before: Dictionary = restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"actor_locks"] = {&"wrong_actor": &"save_instance"}
_expect(not restored.restore_snapshot(corrupt), "orphaned Actor Lock snapshot is rejected")
_expect(restored.create_snapshot() == before, "failed Event restore is atomic")
func _test_event_recovery_lock_scope_and_strict_filter_parse() -> void:
var definition := _choice_definition(
&"recover_event",
10,
EventDefinitionType.TransactionPolicy.ROLLBACK_ALL,
[]
)
var source := EventServiceType.new()
source.register_definition(definition)
source.enqueue_candidate(&"recover_event", [&"actor_a"], 10, {&"event_instance_id": &"recover_instance"})
source.start_next_event(10)
var terminal_snapshot: Dictionary = source.create_snapshot()
terminal_snapshot[&"instances"][0][&"state"] = "COMPLETED"
terminal_snapshot[&"active_deduplication_keys"] = {}
var recovered := EventServiceType.new()
recovered.register_definition(definition)
_expect(recovered.restore_snapshot_with_lock_recovery(terminal_snapshot), "recovery accepts the terminal foreground's exact Actor Lock set")
_expect(recovered.get_actor_locks().is_empty() and recovered.get_foreground_instance() == null, "terminal recovery releases only the matching foreground pointer and lock")
_expect(recovered.last_recovery_repair_report[&"released_actor_ids"] == [&"actor_a"], "terminal recovery report names the exact released Actor Lock")
var live_before: Dictionary = recovered.create_snapshot()
var mixed_corruption: Dictionary = terminal_snapshot.duplicate(true)
mixed_corruption[&"actor_locks"][&"unrelated_actor"] = "recover_instance"
_expect(not recovered.restore_snapshot_with_lock_recovery(mixed_corruption), "valid foreground lock mixed with unrelated lock corruption is hard-rejected")
_expect(recovered.create_snapshot() == live_before, "unrelated lock rejection preserves the live Event state atomically")
var missing_foreground: Dictionary = terminal_snapshot.duplicate(true)
missing_foreground[&"instances"] = []
var missing_recovered := EventServiceType.new()
missing_recovered.register_definition(definition)
_expect(missing_recovered.restore_snapshot_with_lock_recovery(missing_foreground), "missing foreground recovery releases locks that still name only the missing foreground ID")
_expect(missing_recovered.last_recovery_repair_report[&"reason"] == &"MISSING_FOREGROUND" and missing_recovered.last_recovery_repair_report[&"released_actor_ids"] == [&"actor_a"], "missing foreground report preserves the exact released Actor IDs and reason")
var unknown_mode: Dictionary = live_before.duplicate(true)
unknown_mode[&"filter_mode"] = "UNKNOWN_FILTER"
_expect(not recovered.restore_snapshot(unknown_mode), "unknown serialized Content Filter behavior is rejected instead of falling back")
_expect(recovered.create_snapshot() == live_before, "unknown filter rejection preserves the live Event state")
var out_of_range_rating: Dictionary = live_before.duplicate(true)
out_of_range_rating[&"maximum_content_rating"] = 99
_expect(not recovered.restore_snapshot(out_of_range_rating), "out-of-range serialized Content Rating is rejected instead of clamped")
_expect(recovered.create_snapshot() == live_before, "out-of-range rating rejection preserves the live Event state")
var serialized_definition: Dictionary = definition.create_snapshot()
serialized_definition[&"filter_behavior"] = "UNKNOWN_FILTER"
_expect(EventDefinitionType.from_variant(serialized_definition) == null, "serialized EventDefinition rejects an unknown FilterBehavior")
serialized_definition = definition.create_snapshot()
serialized_definition[&"content_rating"] = 99
_expect(EventDefinitionType.from_variant(serialized_definition) == null, "serialized EventDefinition rejects an out-of-range ContentRating")
var lenient_input := EventDefinitionType.new().configure(&"settings_boundary", {&"filter_behavior": "UNKNOWN_FILTER"})
_expect(lenient_input.filter_behavior == EventDefinitionType.FilterBehavior.NONE, "non-snapshot configuration keeps its explicit lenient fallback boundary")
func _transaction_service(state: Dictionary) -> Variant:
return EventServiceType.new().configure_callbacks(
Callable(),
func(action: EventAction, phase: StringName, _context: Dictionary, token: Variant) -> Dictionary:
if phase == &"validate":
return {&"ok": true}
if phase == &"apply":
if bool(action.payload.get(&"fail", false)):
return {&"ok": false, &"error": "requested failure"}
var old_value := int(state[&"value"])
state[&"value"] = old_value + int(action.payload.get(&"delta", 0))
return {&"ok": true, &"token": old_value}
if phase == &"rollback":
state[&"value"] = int(token)
return {&"ok": true}
)
func _run_choice(service: Variant, event_id: StringName, instance_id: StringName) -> CommandResult:
service.enqueue_candidate(event_id, [], 0, {&"event_instance_id": instance_id})
var started = service.start_next_event(0)
return service.submit_choice(instance_id, started.details[&"choice_instance_id"], &"accept", 0)
func _choice_definition(
event_id: StringName,
priority: int,
policy: EventDefinition.TransactionPolicy,
actions: Array,
cost: Dictionary = {}
) -> EventDefinition:
var choice := EventChoiceType.new().configure(
&"accept",
StringName("choice.%s.accept" % event_id),
[],
cost,
{},
actions
)
return EventDefinitionType.new().configure(event_id, {
&"category": &"RELATION",
&"priority": priority,
&"foreground": true,
&"transaction_policy": policy,
&"choices": [choice],
})
func _delta_action(action_id: StringName, delta: int) -> EventAction:
return EventActionType.new().configure(
action_id,
EventActionType.Type.ADD_LEDGER_ENTRY,
{&"delta": delta}
)
func _failing_action(action_id: StringName) -> EventAction:
return EventActionType.new().configure(
action_id,
EventActionType.Type.ADD_LEDGER_ENTRY,
{&"fail": true}
)
func _instance_ids(instances: Array[EventInstance]) -> Array[StringName]:
var result: Array[StringName] = []
for instance: EventInstance in instances:
result.append(instance.event_instance_id)
return result
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ok %s" % label)
else:
_failures += 1
push_error(" not ok %s" % label)
@@ -0,0 +1 @@
uid://rqdt3la3n4hp
@@ -0,0 +1,425 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S4: runtime relationship and event integration")
_test_threshold_foreground_acknowledgement_and_idempotency()
_test_paid_choice_escrow_and_commit()
_test_missing_actor_releases_reservation_and_lock()
_test_waiting_choice_snapshot_exact_replay()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime(seed: int) -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
_clock.set_paused(true)
_expect(report[&"valid"], "runtime initializes S4 definitions")
_expect(_runtime.event_service.get_definition(&"relationship.threshold") != null, "canonical relationship threshold Event is registered")
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_threshold_foreground_acknowledgement_and_idempotency() -> void:
_reset_runtime(4_001)
_expect(_spawn_guest(&"relation_guest", 150).is_success(), "relationship actor is created through Runtime Command")
var created = _runtime.submit_command(
&"create_relationship",
{
&"relationship_id": &"relationship.runtime.threshold",
&"actor_a_id": &"player",
&"actor_b_id": &"relation_guest",
&"options": {
&"trust": 35,
&"respect": 40,
&"affection": 45,
&"fear": 5,
&"dependence": 10,
&"shield_max": 60,
&"shield_current": 60,
},
},
&"player",
&"relation_guest",
80,
-1,
&"s4.relationship.create.threshold"
)
_expect(created.is_success(), "Relationship is created through Runtime Command")
var relationship = _runtime.relationship_service.get_relationship(&"relationship.runtime.threshold")
_expect(relationship != null and relationship.actor_a_id == &"player" and relationship.actor_b_id == &"relation_guest", "Runtime preserves normalized relationship actors")
var changed = _runtime.submit_command(
&"change_relationship",
{
&"relationship_id": relationship.relationship_id,
&"metric_id": &"affection",
&"delta": 10,
},
&"player",
relationship.relationship_id,
80,
-1,
&"s4.relationship.affection.gate"
)
_expect(changed.is_success() and changed.details[&"gated_threshold"] == 50, "Affection change reports the 50 threshold gate")
_expect(relationship.get_metric(&"affection") == 49, "background calculation cannot cross 50 before Event acknowledgement")
_expect(_runtime.relationship_service.get_threshold_candidates().size() == 1, "threshold crossing creates one deterministic relationship candidate")
_expect(_runtime.event_service.get_queued_instances().size() == 1, "candidate is projected into EventService without immediate playback")
_clock.set_paused(false)
_clock.advance_one_minute()
var foreground = _runtime.event_service.get_foreground_instance()
_expect(foreground != null and foreground.source_event_id == &"relationship.threshold", "relationship candidate starts as the foreground Event")
_expect(_clock.paused, "foreground Choice automatically pauses Simulation")
_expect(_runtime.event_service.get_actor_locks().size() == 2, "foreground threshold Event locks both relationship actors")
var instance_id: StringName = foreground.event_instance_id
var choice_instance_id: StringName = foreground.choice_instance_id
var acknowledged = _runtime.submit_command(
&"submit_event_choice",
{
&"event_instance_id": instance_id,
&"choice_instance_id": choice_instance_id,
&"choice_id": &"acknowledge",
},
&"player",
instance_id,
100,
-1,
&"s4.threshold.acknowledge.first"
)
_expect(acknowledged.is_success(), "foreground acknowledgement resolves through Runtime Command")
_expect(relationship.get_metric(&"affection") == 50, "acknowledged threshold Event applies the gated Affection point")
_expect(_runtime.relationship_service.get_threshold_candidates().is_empty(), "resolved Event consumes its relationship candidate")
_expect(_runtime.event_service.get_actor_locks().is_empty() and _runtime.event_service.get_foreground_instance() == null, "resolved Event releases every Actor Lock")
_expect(not _clock.paused, "Runtime resumes when no foreground Event remains")
_clock.set_paused(true)
var replayed = _runtime.submit_command(
&"submit_event_choice",
{
&"event_instance_id": instance_id,
&"choice_instance_id": choice_instance_id,
&"choice_id": &"acknowledge",
},
&"player",
instance_id,
100,
-1,
&"s4.threshold.acknowledge.transport-retry"
)
_expect(replayed.is_success() and replayed.is_replay, "same choice_instance_id returns EventService idempotent replay")
_expect(relationship.get_metric(&"affection") == 50, "choice replay does not apply relationship Actions twice")
_expect(_count_domain_events(&"EVENT_CHOICE_RESOLVED", instance_id) == 1, "choice replay emits no duplicate resolution Domain Event")
_expect(_runtime.relationship_service.validate_invariants()[&"ok"], "threshold flow preserves Relationship invariants")
_expect(_runtime.event_service.validate_invariants()[&"ok"], "threshold flow preserves Event and Actor Lock invariants")
func _test_paid_choice_escrow_and_commit() -> void:
_reset_runtime(4_002)
var registered = _register_paid_event_definition(&"event.paid.commit", 125)
_expect(registered.is_success(), "paid EventDefinition registers through Runtime Command")
var enqueued = _runtime.submit_command(
&"enqueue_event",
{
&"event_id": &"event.paid.commit",
&"actor_ids": [&"player"],
&"options": {&"event_instance_id": &"event_instance.paid.commit"},
},
&"player",
&"event_instance.paid.commit",
60,
-1,
&"s4.event.paid.enqueue"
)
_expect(enqueued.is_success(), "paid Event candidate enters Runtime queue")
_start_next_foreground_minute()
var foreground = _runtime.event_service.get_foreground_instance()
_expect(foreground != null and foreground.event_instance_id == &"event_instance.paid.commit", "paid Event reaches foreground Choice")
var cash_before: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var reserved = _runtime.submit_command(
&"reserve_event_choice_cost",
{
&"event_instance_id": foreground.event_instance_id,
&"choice_id": &"purchase",
},
&"player",
foreground.event_instance_id,
100,
-1,
&"s4.event.paid.reserve"
)
var escrow_account := &"EVENT_ESCROW:event_instance.paid.commit"
_expect(reserved.is_success(), "paid Choice reserves Cost through Runtime boundary")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 125, "Gold leaves PLAYER_CASH exactly once at reservation")
_expect(_runtime.economy_service.get_balance(escrow_account) == 125, "reserved Gold is held in Event-specific escrow")
_expect(_state.get_resource(&"gold") == cash_before - 125, "GameState Gold projection follows escrow transfer")
var committed = _runtime.submit_command(
&"submit_event_choice",
{
&"event_instance_id": foreground.event_instance_id,
&"choice_instance_id": foreground.choice_instance_id,
&"choice_id": &"purchase",
},
&"player",
foreground.event_instance_id,
100,
-1,
&"s4.event.paid.commit"
)
_expect(committed.is_success(), "paid Choice commits after revalidation")
_expect(_runtime.economy_service.get_balance(escrow_account) == 0, "committed Event leaves no Gold in escrow")
_expect(_runtime.economy_service.get_balance(&"OPERATING_EXPENSE") == 125, "escrow Gold posts to the configured expense account")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 125, "Cost commit never charges PLAYER_CASH a second time")
_expect(_runtime.event_service.has_flag(&"paid_choice_completed"), "paid Choice Action applies only after Cost reservation")
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "escrow reserve and commit remain double-entry balanced")
_expect(_runtime.event_service.validate_invariants()[&"ok"], "paid Choice completion preserves Event invariants")
func _test_missing_actor_releases_reservation_and_lock() -> void:
_reset_runtime(4_003)
var registered_staff = _runtime.submit_command(
&"register_staff",
{
&"staff_id": &"temporary_actor",
&"role_id": &"service",
&"options": {},
},
&"player",
&"temporary_actor",
80,
-1,
&"s4.actor.temporary.register"
)
_expect(registered_staff.is_success(), "temporary foreground actor registers through Runtime")
_expect(_register_paid_event_definition(&"event.actor.missing", 80).is_success(), "missing-actor fixture EventDefinition registers")
var enqueued = _runtime.submit_command(
&"enqueue_event",
{
&"event_id": &"event.actor.missing",
&"actor_ids": [&"temporary_actor"],
&"options": {&"event_instance_id": &"event_instance.actor.missing"},
},
&"player",
&"event_instance.actor.missing",
60,
-1,
&"s4.actor.missing.enqueue"
)
_expect(enqueued.is_success(), "missing-actor fixture enters Event queue")
_start_next_foreground_minute()
var foreground = _runtime.event_service.get_foreground_instance()
_expect(foreground != null and _runtime.event_service.get_actor_locks().has(&"temporary_actor"), "foreground Event holds temporary actor lock")
var cash_before: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var reserved = _runtime.submit_command(
&"reserve_event_choice_cost",
{
&"event_instance_id": foreground.event_instance_id,
&"choice_id": &"purchase",
},
&"player",
foreground.event_instance_id,
100,
-1,
&"s4.actor.missing.reserve"
)
_expect(reserved.is_success() and _runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 80, "missing-actor fixture owns reserved Gold")
_expect(_runtime.staff_service.remove_staff(&"temporary_actor", &"TEST_REMOVED", true, _clock.absolute_minute).is_success(), "actor disappears through StaffService lifecycle")
_clock.set_paused(false)
_clock.advance_one_minute()
var instance = _runtime.event_service.get_instance(&"event_instance.actor.missing")
_expect(instance != null and instance.state == EventInstance.State.CANCELLED, "Runtime audit cancels Event whose actor disappeared")
_expect(_runtime.event_service.get_actor_locks().is_empty() and _runtime.event_service.get_foreground_instance() == null, "actor disappearance releases Lock and foreground ownership")
_expect(_runtime.economy_service.get_balance(&"EVENT_ESCROW:event_instance.actor.missing") == 0, "actor disappearance empties Event escrow")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before and _state.get_resource(&"gold") == cash_before, "actor disappearance refunds reserved Gold exactly once")
_expect(_runtime.economy_service.get_balance(&"OPERATING_EXPENSE") == 0, "cancelled missing-actor Event never commits Cost")
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "missing-actor refund preserves ledger invariants")
_expect(_runtime.event_service.validate_invariants()[&"ok"], "missing-actor recovery preserves Event invariants")
func _test_waiting_choice_snapshot_exact_replay() -> void:
_reset_runtime(4_004)
_expect(_spawn_guest(&"snapshot_relation_guest", 175).is_success(), "snapshot relationship actor is created")
var relationship_id := &"relationship.runtime.snapshot"
var created = _runtime.submit_command(
&"create_relationship",
{
&"relationship_id": relationship_id,
&"actor_a_id": &"player",
&"actor_b_id": &"snapshot_relation_guest",
&"options": {&"affection": 45, &"shield_max": 50, &"shield_current": 50},
},
&"player",
relationship_id,
80,
-1,
&"s4.snapshot.relationship.create"
)
_expect(created.is_success(), "snapshot Relationship registers")
var gated = _runtime.submit_command(
&"change_relationship",
{&"relationship_id": relationship_id, &"metric_id": &"affection", &"delta": 10},
&"player",
relationship_id,
80,
-1,
&"s4.snapshot.relationship.gate"
)
_expect(gated.is_success(), "snapshot scenario creates threshold Event candidate")
_start_next_foreground_minute()
var foreground = _runtime.event_service.get_foreground_instance()
_expect(foreground != null and _clock.paused, "snapshot is captured while foreground Choice is waiting")
var instance_id: StringName = foreground.event_instance_id
var choice_instance_id: StringName = foreground.choice_instance_id
# Save capture occurs after tick completion, including the incremented Kernel
# snapshot_version; this is the exact boundary replayed below.
var waiting_snapshot: Dictionary = _runtime.create_snapshot()
var waiting_digest: String = SimulationDigest.compute(waiting_snapshot)
_expect(waiting_digest.length() == 64, "waiting-Choice snapshot produces a stable digest")
var first_resolution = _runtime.submit_command(
&"submit_event_choice",
{
&"event_instance_id": instance_id,
&"choice_instance_id": choice_instance_id,
&"choice_id": &"acknowledge",
},
&"player",
instance_id,
100,
-1,
&"s4.snapshot.choice.resolve"
)
_expect(first_resolution.is_success(), "waiting Choice resolves before rewind")
var final_digest: String = _runtime.latest_digest
_expect(_runtime.relationship_service.get_relationship(relationship_id).get_metric(&"affection") == 50, "first timeline reaches acknowledged threshold")
var restored_ok: bool = _runtime.restore_snapshot(waiting_snapshot)
_expect(restored_ok, "Runtime restores a snapshot containing Choice token and Actor Locks")
_expect(_runtime.latest_digest == waiting_digest, "waiting-Choice restore reproduces exact digest")
var restored_foreground = _runtime.event_service.get_foreground_instance()
_expect(restored_foreground != null and restored_foreground.event_instance_id == instance_id, "restored Runtime recovers the same foreground Event")
_expect(restored_foreground.choice_instance_id == choice_instance_id and _runtime.event_service.get_actor_locks().size() == 2, "Choice token and Actor Locks restore exactly")
_expect(_clock.paused and _runtime.relationship_service.get_relationship(relationship_id).get_metric(&"affection") == 49, "restore returns to pre-acknowledgement paused relationship state")
var replay_resolution = _runtime.submit_command(
&"submit_event_choice",
{
&"event_instance_id": instance_id,
&"choice_instance_id": choice_instance_id,
&"choice_id": &"acknowledge",
},
&"player",
instance_id,
100,
-1,
&"s4.snapshot.choice.resolve"
)
_expect(replay_resolution.is_success(), "restored Choice accepts the same deterministic Command")
_expect(_runtime.latest_digest == final_digest, "Choice replay after restore reproduces exact final digest")
_expect(_runtime.relationship_service.get_relationship(relationship_id).get_metric(&"affection") == 50, "replayed timeline reaches threshold once")
_expect(_runtime.relationship_service.validate_invariants()[&"ok"], "restored relationship state satisfies invariants")
_expect(_runtime.event_service.validate_invariants()[&"ok"], "restored Event queue and Lock state satisfy invariants")
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "snapshot replay preserves economic invariants")
func _spawn_guest(guest_id: StringName, gold: int) -> CommandResult:
return _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": guest_id,
&"guest_type_id": &"traveler",
&"gold": gold,
},
&"player",
guest_id,
50,
-1,
StringName("s4.guest.spawn.%s" % guest_id)
)
func _register_paid_event_definition(event_id: StringName, gold_cost: int) -> CommandResult:
return _runtime.submit_command(
&"register_event_definition",
{&"definition": {
&"event_id": event_id,
&"category": &"RELATIONSHIP",
&"priority": 70,
&"foreground": true,
&"transaction_policy": &"ROLLBACK_ALL",
&"missing_actor_policy": &"CANCEL",
&"content_rating": &"GENERAL",
&"choices": [{
&"choice_id": &"purchase",
&"text_key": StringName("event.%s.purchase" % event_id),
&"cost": {&"gold": gold_cost},
&"actions": [{
&"action_id": &"paid_choice_flag",
&"type": &"SET_FLAG",
&"payload": {&"flag_id": &"paid_choice_completed"},
}],
}],
}},
&"player",
event_id,
60,
-1,
StringName("s4.event.definition.%s" % event_id)
)
func _start_next_foreground_minute() -> void:
_clock.set_paused(false)
_clock.advance_one_minute()
func _count_domain_events(event_type: StringName, entity_id: StringName) -> int:
var count := 0
for event in _runtime.domain_events.recent_events():
if event.event_type == event_type and (
entity_id in event.entity_ids
or StringName(event.payload.get(&"event_instance_id", &"")) == entity_id
):
count += 1
return count
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://dfrs30r1g0jqe
+362
View File
@@ -0,0 +1,362 @@
extends SceneTree
const HeatStateType = preload("res://scripts/authority/heat_state.gd")
const IncidentType = preload("res://scripts/authority/incident.gd")
const EvidenceType = preload("res://scripts/authority/evidence.gd")
const InspectionType = preload("res://scripts/authority/inspection.gd")
const AuthorityServiceType = preload("res://scripts/authority/authority_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S5: Authority, Incident, Evidence and Inspection")
_test_heat_incident_dedup_and_decay_floor()
_test_evidence_visibility_and_incident_scoring()
_test_inspection_selection_and_patrol_order()
_test_path_unreachable_records_departure_reason()
_test_resolution_idempotency()
_test_resolution_failure_is_atomic()
_test_snapshot_round_trip_and_atomicity()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_heat_incident_dedup_and_decay_floor() -> void:
var service := AuthorityServiceType.new()
var first = service.create_incident(
&"incident_shared",
&"illegal_service",
10,
&"noise_detector",
{&"severity": 80, &"heat_delta": 30, &"facility_id": &"hidden_room"}
)
_expect(first.is_success(), "first Incident report is accepted")
_expect(service.get_authority_state().heat == 30, "Incident applies its Heat delta once")
var duplicate = service.report_incident(
IncidentType.new().configure(
&"incident_shared",
&"conflicting_duplicate",
11,
{&"severity": 100, &"heat_delta": 100}
),
&"witness_detector"
)
_expect(duplicate.is_success() and duplicate.is_replay, "duplicate incident_id is an idempotent replay")
_expect(duplicate.details[&"heat_applied"] == 0 and service.get_authority_state().heat == 30, "second detector cannot add Heat again")
var canonical := service.get_incident(&"incident_shared")
_expect(canonical.detector_ids == [&"noise_detector", &"witness_detector"], "all unique detector IDs remain auditable")
_expect(canonical.incident_type == &"illegal_service" and canonical.severity == 80, "first Incident payload remains canonical")
service.advance_time(1_440, 15, 20)
_expect(service.get_authority_state().heat == 20, "daily decay stops at the active-case floor")
service.advance_time(2_880, 15, 20)
_expect(service.get_authority_state().heat == 20, "active-case floor remains stable on later days")
service.dismiss_incident(&"incident_shared")
service.advance_time(4_320, 15, 20)
_expect(service.get_authority_state().heat == 5, "Heat decays below the floor after the case closes")
var low_state := HeatStateType.new().configure({&"heat": 10, &"active_case_ids": [&"case_a"]})
low_state.apply_daily_decay(1, 5, 20)
_expect(low_state.heat == 10, "daily decay never raises pre-existing low Heat up to its floor")
_expect(service.validate_invariants()[&"ok"], "Incident dedup and active-case state satisfy invariants")
func _test_evidence_visibility_and_incident_scoring() -> void:
var los_calls := {&"count": 0, &"visible": false}
var service := AuthorityServiceType.new().configure_callbacks(
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool:
los_calls[&"count"] = int(los_calls[&"count"]) + 1
return bool(los_calls[&"visible"])
)
service.create_incident(&"incident_evidence", &"hidden_facility", 0, &"detector_a", {&"heat_delta": 5})
service.create_evidence(&"evidence_wall", &"visual_trace", 25, {
&"incident_id": &"incident_evidence",
&"facility_id": &"illegal_room",
&"location_cell": Vector3i(5, 5, -1),
&"created_at": 0,
&"expires_at": 1_000,
})
var blocked = service.attempt_discover_evidence(
&"evidence_wall", &"inspector_a", Vector3i(4, 5, -1), 10
)
_expect(not blocked.is_success() and blocked.details[&"reason"] == &"LINE_OF_SIGHT_BLOCKED", "SYS-AUT-001: a wall-blocked callback prevents discovery")
_expect(service.get_authority_state().evidence_points == 0, "blocked visual evidence contributes no points")
var other_layer = service.attempt_discover_evidence(
&"evidence_wall", &"inspector_a", Vector3i(5, 5, 0), 11
)
_expect(not other_layer.is_success() and other_layer.details[&"reason"] == &"DIFFERENT_LAYER", "different layers never fall through to line-of-sight")
_expect(los_calls[&"count"] == 1, "cross-layer evidence does not call the visibility boundary")
los_calls[&"visible"] = true
var visible = service.attempt_discover_evidence(
&"evidence_wall", &"inspector_a", Vector3i(4, 5, -1), 12,
{&"illegal_confirmed": true}
)
_expect(visible.is_success() and visible.details[&"discovery_method"] == &"LINE_OF_SIGHT", "same-layer evidence is found only after the injected boundary confirms sight")
_expect(service.get_authority_state().evidence_points == 25, "discovered evidence contributes its severity")
_expect(service.get_authority_state().known_illegal_facility_ids == [&"illegal_room"], "confirmed visual evidence records the known illegal facility")
service.create_evidence(&"evidence_same_incident", &"second_photo", 25, {
&"incident_id": &"incident_evidence",
&"location_cell": Vector3i(8, 8, -1),
&"created_at": 0,
})
service.attempt_discover_evidence(
&"evidence_same_incident", &"inspector_a", Vector3i(8, 8, -1), 13
)
_expect(service.get_authority_state().evidence_points == 25, "SYS-AUT-002: the same Incident is not added to evidence points twice")
service.create_evidence(&"evidence_stronger", &"ledger", 40, {
&"incident_id": &"incident_evidence",
&"location_cell": Vector3i(99, 99, 5),
&"created_at": 0,
})
service.attempt_discover_evidence(
&"evidence_stronger", &"inspector_a", Vector3i.ZERO, 14,
{&"trusted_source": true}
)
_expect(service.get_authority_state().evidence_points == 40, "corroboration upgrades an Incident to its strongest evidence rather than stacking")
var replay = service.attempt_discover_evidence(
&"evidence_stronger", &"inspector_a", Vector3i.ZERO, 15,
{&"trusted_source": true}
)
_expect(replay.is_success() and replay.is_replay and service.get_authority_state().evidence_points == 40, "repeated evidence discovery is idempotent")
_expect(service.validate_invariants()[&"ok"], "visibility discoveries and reverse references satisfy invariants")
func _test_inspection_selection_and_patrol_order() -> void:
var service := AuthorityServiceType.new().configure_callbacks(
Callable(),
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> Dictionary:
return {&"reachable": true, &"travel_minutes": 2}
)
service.apply_heat_delta(30, &"fixture", 0)
service.set_suspicion(20)
var selected = service.evaluate_inspection(360, {
&"base_chance": 5,
&"heat_weight": 1,
&"suspicion_weight": 1,
&"overdue_case_bonus": 0,
&"reputation_protection": 0,
&"roll": 54,
&"inspection_id": &"inspection_order",
&"inspector_id": &"inspector_order",
&"entrance_cell": Vector3i.ZERO,
})
_expect(selected.is_success() and selected.details[&"chance"] == 55 and selected.details[&"selected"], "06:00 inspection chance uses deterministic integer terms")
var replay = service.evaluate_inspection(360, {&"roll": 99})
_expect(replay.is_success() and replay.is_replay and replay.details[&"inspection_id"] == &"inspection_order", "same-day inspection selection replays the stored result")
_expect(service.get_authority_state().next_inspection_window == 1_800, "selection updates the next inspection Window")
service.start_inspection(&"inspection_order", 360)
service.mark_inspector_arrived(&"inspection_order", 361)
var candidates := [
{&"waypoint_id": &"z_tie", &"cell": Vector3i(3, 0, 0), &"suspect_score": 10, &"evidence_potential": 10, &"travel_minutes": 5, &"inspection_minutes": 5},
{&"waypoint_id": &"top", &"cell": Vector3i(1, 0, 0), &"suspect_score": 10, &"evidence_potential": 20, &"travel_minutes": 5, &"inspection_minutes": 5},
{&"waypoint_id": &"a_tie", &"cell": Vector3i(2, 0, 0), &"suspect_score": 10, &"evidence_potential": 10, &"travel_minutes": 5, &"inspection_minutes": 5},
]
var plan = service.plan_patrol(&"inspection_order", candidates, 362, 30)
_expect(plan.is_success(), "patrol plan is accepted after CHECK_IN")
_expect(plan.details[&"waypoint_ids"] == [&"top", &"a_tie", &"z_tie"], "patrol sorts Utility descending then waypoint ID lexically")
_expect(service.get_inspection(&"inspection_order").discovered_evidence_ids.is_empty(), "planning patrol does not perform an instantaneous map scan")
var moved = service.advance_patrol(&"inspection_order", 363)
_expect(moved.is_success() and moved.details[&"waypoint_id"] == &"top", "patrol advances exactly one waypoint per call")
var second := AuthorityServiceType.new()
second.schedule_inspection(360, {&"inspection_id": &"stable", &"inspector_id": &"stable_i"})
second.start_inspection(&"stable", 360)
second.mark_inspector_arrived(&"stable", 361)
var reversed := candidates.duplicate(true)
reversed.reverse()
var second_plan = second.plan_patrol(&"stable", reversed, 362, 30)
_expect(second_plan.details[&"waypoint_ids"] == plan.details[&"waypoint_ids"], "patrol route is independent of candidate input order")
func _test_path_unreachable_records_departure_reason() -> void:
var service := AuthorityServiceType.new().configure_callbacks(
Callable(),
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> Dictionary:
return {&"reachable": false, &"reason": &"SEALED_BY_WALL"}
)
service.schedule_inspection(10, {
&"inspection_id": &"inspection_blocked",
&"inspector_id": &"inspector_blocked",
&"entrance_cell": Vector3i.ZERO,
})
service.start_inspection(&"inspection_blocked", 10)
service.mark_inspector_arrived(&"inspection_blocked", 11)
service.plan_patrol(&"inspection_blocked", [{
&"waypoint_id": &"sealed_room",
&"cell": Vector3i(9, 9, -1),
&"suspect_score": 90,
&"evidence_potential": 90,
&"travel_minutes": 10,
}], 12)
var blocked = service.advance_patrol(&"inspection_blocked", 13)
var inspection := service.get_inspection(&"inspection_blocked")
_expect(blocked.is_success() and not blocked.details[&"reachable"], "unreachable patrol target is a handled inspection outcome")
_expect(inspection.state == InspectionType.State.DEPARTING, "SYS-AUT-004: unreachable inspector enters DEPARTING instead of teleporting")
_expect(inspection.exit_reason == &"PATH_UNREACHABLE" and inspection.result_reason == &"SEALED_BY_WALL", "path failure records both stable exit and detailed result reasons")
_expect(inspection.inaccessible_waypoint_ids == [&"sealed_room"] and inspection.visited_waypoint_ids.is_empty(), "unreachable waypoint is recorded without being marked visited")
var departed = service.depart_inspection(&"inspection_blocked", 14)
_expect(departed.is_success() and inspection.state == InspectionType.State.CLOSED, "inspector safely closes after departure")
_expect(departed.details[&"exit_reason"] == &"PATH_UNREACHABLE", "closed inspection preserves the path-unreachable exit reason")
_expect(service.validate_invariants()[&"ok"], "path-unreachable inspection remains snapshot-safe")
func _test_resolution_idempotency() -> void:
var applications := {&"validate": 0, &"apply": 0, &"fine_total": 0, &"sanctions": []}
var service := AuthorityServiceType.new().configure_callbacks(
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
func(payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
applications[phase] = int(applications.get(phase, 0)) + 1
if phase == &"apply":
applications[&"fine_total"] = int(applications[&"fine_total"]) + int(payload[&"fine_amount"])
(applications[&"sanctions"] as Array).append(payload[&"sanction"])
return {&"ok": true, &"token": &"validated"}
)
service.create_incident(&"incident_fine", &"illegal_facility", 0, &"detector", {&"severity": 80, &"heat_delta": 10})
service.create_evidence(&"evidence_fine", &"physical", 60, {
&"incident_id": &"incident_fine",
&"facility_id": &"facility_illegal",
&"location_cell": Vector3i(2, 0, 0),
&"created_at": 0,
})
_prepare_reviewing_inspection(service, &"inspection_fine", &"inspector_fine", &"evidence_fine")
var first = service.resolve_inspection(&"inspection_fine", &"resolution_once", 5, {&"fine_per_point": 10})
_expect(first.is_success() and first.details[&"result"] == &"TEMPORARY_CLOSURE", "evidence thresholds deterministically select the sanction tier")
_expect(first.details[&"fine_amount"] == 600 and first.details[&"sanction"] == &"TEMPORARY_CLOSURE", "resolution projection contains fine and sanction for external domains")
_expect(applications[&"validate"] == 1 and applications[&"apply"] == 1, "resolution boundary validates then applies exactly once")
_expect(service.get_authority_state().fines_due == 600 and applications[&"fine_total"] == 600, "successful resolution records one fine liability and one external application")
_expect(service.get_incident(&"incident_fine").state == IncidentType.State.RESOLVED, "resolution closes linked active Incident cases")
var duplicate = service.resolve_inspection(&"inspection_fine", &"resolution_once", 6, {&"fine_per_point": 999})
_expect(duplicate.is_success() and duplicate.is_replay, "SYS-AUT-003: duplicate resolution_id returns its stored result")
_expect(applications[&"apply"] == 1 and service.get_authority_state().fines_due == 600, "duplicate resolution cannot apply fine or sanction twice")
_expect(duplicate.details[&"fine_amount"] == 600, "replay ignores conflicting later resolution terms")
_expect(service.validate_invariants()[&"ok"], "resolved inspection, resolution and Incident references satisfy invariants")
func _test_resolution_failure_is_atomic() -> void:
var external := {&"fine": 0, &"rollback": 0}
var service := AuthorityServiceType.new().configure_callbacks(
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
if phase == &"validate":
return {&"ok": true, &"token": int(external[&"fine"])}
if phase == &"apply":
external[&"fine"] = int(external[&"fine"]) + int(payload[&"fine_amount"])
return {&"ok": false, &"token": token, &"reason": &"LEDGER_REJECTED"}
if phase == &"rollback":
external[&"fine"] = int(token)
external[&"rollback"] = int(external[&"rollback"]) + 1
return {&"ok": true}
return {&"ok": false}
)
service.create_incident(&"incident_fail", &"illegal_facility", 0, &"detector", {&"heat_delta": 5})
service.create_evidence(&"evidence_fail", &"physical", 30, {
&"incident_id": &"incident_fail",
&"location_cell": Vector3i(2, 0, 0),
&"created_at": 0,
})
_prepare_reviewing_inspection(service, &"inspection_fail", &"inspector_fail", &"evidence_fail")
var before: Dictionary = service.create_snapshot()
var failed = service.resolve_inspection(&"inspection_fail", &"resolution_fail", 5)
_expect(not failed.is_success() and failed.details[&"rollback_ok"], "failed external resolution invokes its rollback boundary")
_expect(external[&"fine"] == 0 and external[&"rollback"] == 1, "failed fine application leaves no external value behind")
_expect(service.create_snapshot() == before, "failed resolution leaves the full Authority snapshot unchanged")
_expect(service.get_resolution(&"resolution_fail").is_empty(), "failed resolution_id is not marked processed")
func _test_snapshot_round_trip_and_atomicity() -> void:
var application_count := {&"apply": 0}
var service := AuthorityServiceType.new().configure_callbacks(
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
if phase == &"apply":
application_count[&"apply"] = int(application_count[&"apply"]) + 1
return {&"ok": true}
)
service.create_incident(&"incident_save", &"contraband", 2, &"detector_b", {&"heat_delta": 12})
service.report_incident(
IncidentType.new().configure(&"incident_save", &"contraband", 2, {&"heat_delta": 12}),
&"detector_a"
)
service.create_evidence(&"evidence_save", &"inventory_record", 30, {
&"incident_id": &"incident_save",
&"location_cell": Vector3i(1, 1, 0),
&"created_at": 2,
})
_prepare_reviewing_inspection(service, &"inspection_save", &"inspector_save", &"evidence_save")
service.resolve_inspection(&"inspection_save", &"resolution_save", 5)
service.depart_inspection(&"inspection_save", 6)
service.advance_time(1_440)
var snapshot: Dictionary = service.create_snapshot()
var restored := AuthorityServiceType.new().configure_callbacks(
Callable(), Callable(),
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
if phase == &"apply":
application_count[&"apply"] = int(application_count[&"apply"]) + 1
return {&"ok": true}
)
_expect(restored.restore_snapshot(snapshot), "valid Authority snapshot restores")
_expect(restored.create_snapshot() == snapshot, "Authority snapshot round trips exactly")
_expect(restored.validate_invariants()[&"ok"], "restored Authority aggregate satisfies invariants")
var replay = restored.resolve_inspection(&"inspection_save", &"resolution_save", 1_441)
_expect(replay.is_success() and replay.is_replay, "resolution idempotency survives save and load")
_expect(application_count[&"apply"] == 1, "loaded resolution replay never calls external application again")
var before: Dictionary = restored.create_snapshot()
var corrupt := snapshot.duplicate(true)
corrupt[&"authority_state"][&"evidence_points"] = 999
_expect(not restored.restore_snapshot(corrupt), "cross-domain cache mismatch rejects a corrupt Authority snapshot")
_expect(restored.create_snapshot() == before, "failed Authority restore leaves every live field unchanged")
var wrong_version := snapshot.duplicate(true)
wrong_version[&"version"] = 999
_expect(not restored.restore_snapshot(wrong_version), "unsupported Authority snapshot version is rejected")
_expect(restored.create_snapshot() == before, "version rejection is also atomic")
var out_of_range := snapshot.duplicate(true)
out_of_range[&"authority_state"][&"heat"] = 101
_expect(not restored.restore_snapshot(out_of_range), "out-of-range snapshot values are rejected instead of silently clamped")
_expect(restored.create_snapshot() == before, "strict value rejection preserves the live aggregate")
func _prepare_reviewing_inspection(
service: AuthorityService,
inspection_id: StringName,
inspector_id: StringName,
evidence_id: StringName
) -> void:
service.schedule_inspection(0, {
&"inspection_id": inspection_id,
&"inspector_id": inspector_id,
&"entrance_cell": Vector3i.ZERO,
})
service.start_inspection(inspection_id, 0)
service.mark_inspector_arrived(inspection_id, 1)
service.plan_patrol(inspection_id, [{
&"waypoint_id": StringName("waypoint_%s" % inspection_id),
&"cell": Vector3i(1, 1, 0) if evidence_id == &"evidence_save" else Vector3i(2, 0, 0),
&"suspect_score": 100,
&"evidence_potential": 100,
&"travel_minutes": 1,
&"inspection_minutes": 1,
}], 2)
service.advance_patrol(inspection_id, 3)
service.inspect_current_waypoint(inspection_id, [evidence_id], 4)
service.finish_patrol(inspection_id, 5)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://bdrksuqlwvmu4
+970
View File
@@ -0,0 +1,970 @@
extends SceneTree
const DefenseServiceType = preload("res://scripts/defense/defense_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S5: deterministic wave-by-wave Raid defense")
_test_definition_and_registration_validation()
_test_scores_penalties_and_stable_order()
_test_layered_assets_and_breach()
_test_explicit_and_offscreen_attackers_share_one_formula()
_test_active_skills_and_successful_boundaries()
_test_skill_boundary_validation_rollback()
_test_skill_boundary_apply_and_outcome_rollback()
_test_rng_transaction_retry_stability()
_test_retreat_policies_and_manual_retreat_replay()
_test_cancel_transaction_and_reconciliation()
_test_resolution_operation_namespace_and_legacy_v1()
_test_multiple_waves_require_explicit_transition()
_test_raid_duration_and_wave_round_limits()
_test_round_resolution_replay()
_test_round_clock_contract()
_test_wave_entry_and_metadata_validation()
_test_armor_pool_accounting()
_test_snapshot_round_trip_and_corrupt_atomicity()
_test_cross_time_snapshot_rejection()
_test_input_order_determinism()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_definition_and_registration_validation() -> void:
var service := DefenseServiceType.new()
_expect(not service.create_raid(&"", 0, _definition()).is_success(), "Raid ID is required")
_expect(not service.create_raid(&"bad_minute", -1, _definition()).is_success(), "creation minute cannot be negative")
_expect(not service.create_raid(&"no_entries", 0, _definition({&"entry_points": []})).is_success(), "at least one entry point is required")
_expect(not service.create_raid(&"no_objective", 0, _definition({&"objective_ids": []})).is_success(), "at least one objective is required")
_expect(not service.create_raid(&"no_waves", 0, _definition({&"waves": []})).is_success(), "at least one wave is required")
_expect(not service.create_raid(&"bad_threat", 0, _definition({&"waves": [{&"wave_id": &"w", &"threat_strength": 0}]})).is_success(), "wave strength must be positive")
_expect(not service.create_raid(&"duplicate_wave", 0, _definition({&"waves": [
{&"wave_id": &"same", &"threat_strength": 10},
{&"wave_id": &"same", &"threat_strength": 20},
]})).is_success(), "wave IDs are unique")
_expect(not service.create_raid(&"unknown_entry", 0, _definition({&"waves": [{
&"wave_id": &"w", &"threat_strength": 10, &"entry_points": [&"not_declared"],
}]})).is_success(), "wave entry points must belong to the Raid")
_expect(not service.create_raid(&"unknown_objective", 0, _definition({&"waves": [{
&"wave_id": &"w", &"threat_strength": 10, &"objective_ids": [&"not_declared"],
}]})).is_success(), "wave objectives must belong to the Raid")
_expect(not service.create_raid(&"bad_policy", 0, _definition({&"retreat_policy": &"PANIC"})).is_success(), "retreat policy is enumerated")
_expect(not service.create_raid(&"bad_duration", 0, _definition({&"round_minutes": 0})).is_success(), "round duration must be positive")
_expect(not service.create_raid(&"bad_threshold", 0, _definition({&"defender_retreat_threshold": 101})).is_success(), "defender retreat threshold is bounded")
var created = service.create_raid(&"valid", 10, _definition({
&"entry_points": [&"west", &"east", &"west"],
&"objective_ids": [&"vault", &"core", &"vault"],
&"waves": [{
&"wave_id": &"wave_a",
&"threat_strength": 40,
&"entry_points": [&"west", &"east"],
&"objective_ids": [&"vault", &"core"],
}],
}))
_expect(created.is_success() and created.details[&"wave_count"] == 1, "valid Raid definition is accepted")
var canonical := service.get_raid(&"valid")
_expect(canonical[&"entry_points"] == [&"east", &"west"], "entry point IDs are canonicalized")
_expect(canonical[&"objective_ids"] == [&"core", &"vault"], "objective IDs are canonicalized")
_expect(not service.create_raid(&"valid", 10, _definition()).is_success(), "duplicate Raid ID is rejected")
_expect(not service.register_combatant(&"valid", &"bad_side", &"NEUTRAL", 10, 10).is_success(), "combatant side is validated")
_expect(not service.register_combatant(&"valid", &"bad_health", &"DEFENDER", 10, 0).is_success(), "combatant health is validated")
_expect(not service.register_combatant(&"valid", &"bad_wave", &"ATTACKER", 10, 10, {&"wave_id": &"missing"}).is_success(), "attacker wave reference is validated")
_expect(not service.register_combatant(&"valid", &"bad_defender_wave", &"DEFENDER", 10, 10, {&"wave_id": &"wave_a"}).is_success(), "defenders cannot be wave-bound")
_expect(service.register_combatant(&"valid", &"guard", &"DEFENDER", 20, 100, {&"health": 60, &"entry_point_id": &"east"}).is_success(), "valid wounded defender is registered")
_expect(service.get_raid(&"valid")[&"combatants"][&"guard"][&"initial_health"] == 100, "retreat baseline uses maximum health")
_expect(not service.register_asset(&"valid", &"core", &"DOOR", 100, 10).is_success(), "declared objective cannot be registered as a door")
_expect(not service.register_asset(&"valid", &"ordinary", &"OBJECTIVE", 100, 10).is_success(), "undeclared asset cannot be registered as an objective")
_expect(not service.register_asset(&"valid", &"door_bad_entry", &"DOOR", 100, 10, {&"entry_points": [&"north"]}).is_success(), "asset entry coverage is validated")
_expect(service.register_asset(&"valid", &"core", &"OBJECTIVE", 100, 5, {&"integrity": 40}).is_success(), "first objective asset is registered")
_expect(service.get_raid(&"valid")[&"assets"][&"core"][&"initial_integrity"] == 100, "objective retreat baseline uses maximum integrity")
_expect(not service.start_raid(&"valid", 10).is_success(), "Raid cannot start until every objective is registered")
_expect(service.register_asset(&"valid", &"vault", &"OBJECTIVE", 80, 5).is_success(), "second objective asset is registered")
_expect(service.start_raid(&"valid", 10).is_success(), "complete Raid can start")
_expect(service.get_raid(&"valid")[&"waves"][0][&"state"] == &"ACTIVE", "starting marks the first wave ACTIVE")
_expect(not service.register_asset(&"valid", &"late", &"DOOR", 10, 1).is_success(), "assets cannot be added after start")
_expect(not service.register_combatant(&"valid", &"late", &"DEFENDER", 1, 1).is_success(), "combatants cannot be added after start")
_expect(service.validate_invariants()[&"ok"], "validated registration state satisfies all invariants")
func _test_scores_penalties_and_stable_order() -> void:
var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(service.create_raid(&"scores", 0, _definition({
&"entry_points": [&"west", &"east"],
&"terrain_bonus": 3,
&"command_bonus": 4,
&"surprise_penalty": 7,
&"multiple_entry_penalty": 5,
&"waves": [{
&"wave_id": &"wave_1", &"threat_strength": 40,
&"entry_points": [&"west", &"east"], &"objective_ids": [&"core"],
&"retreat_threshold": 0, &"max_rounds": 10,
}],
})), "score fixture Raid is created")
_must(service.register_combatant(&"scores", &"z_guard", &"DEFENDER", 20, 100, {&"fatigue": 25}), "fatigued defender is registered")
_must(service.register_combatant(&"scores", &"a_guard", &"DEFENDER", 10, 100), "fresh defender is registered")
_must(service.register_combatant(&"scores", &"raider", &"ATTACKER", 20, 100, {&"wave_id": &"wave_1"}), "explicit attacker is registered")
_must(service.register_asset(&"scores", &"core", &"OBJECTIVE", 100, 5), "score objective is registered")
_must(service.register_asset(&"scores", &"tower", &"DEFENSE_DEVICE", 100, 12, {&"terrain_bonus": 2}), "score device is registered")
_must(service.register_asset(&"scores", &"gate", &"DOOR", 100, 8), "score door is registered")
_must(service.start_raid(&"scores", 0), "score Raid starts")
var first = service.resolve_next_round(&"scores", &"score_round_1", 5)
_expect(first.is_success(), "first score round resolves")
var components: Dictionary = first.details[&"score_components"]
_expect(components[&"defender_combat"] == 30, "defender score sums effective combat")
_expect(components[&"asset_defense"] == 25, "door, device and objective all contribute defense")
_expect(components[&"terrain_bonus"] == 5 and components[&"command_bonus"] == 4, "terrain and command bonuses are explicit score components")
_expect(components[&"fatigue_penalty"] == 5, "fatigue scales the affected defender's effective combat")
_expect(components[&"multiple_entry_penalty"] == 5, "second entry point applies one deterministic penalty")
_expect(components[&"surprise_penalty"] == 7, "surprise applies on the first wave round")
_expect(components[&"attacker_combat"] == 20 and components[&"aggregate_attacker_combat"] == 40, "explicit and aggregate attackers are visible score components")
_expect(first.details[&"defense_score"] == 47 and first.details[&"attack_score"] == 60, "integer score formula has the expected result")
var raid_after_first := service.get_raid(&"scores")
_expect(raid_after_first[&"combatants"][&"a_guard"][&"fatigue"] == 5, "fresh defender gains five fatigue per round")
_expect(raid_after_first[&"combatants"][&"z_guard"][&"fatigue"] == 30, "existing fatigue increases by the same fixed amount")
var second = service.resolve_next_round(&"scores", &"score_round_2", 10)
_expect(second.is_success() and second.details[&"score_components"][&"surprise_penalty"] == 0, "surprise never repeats after round one")
_expect(second.details[&"defense_roll"] == 50 and second.details[&"attack_roll"] == 50, "injected rolls are deterministic")
_expect(service.validate_invariants()[&"ok"], "multi-entry and fatigue score state remains valid")
func _test_layered_assets_and_breach() -> void:
var outcomes := {&"count": 0, &"payload": {}}
var service := DefenseServiceType.new().configure_callbacks(
_fixed_roll(50), Callable(),
func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
if phase == &"apply":
outcomes[&"count"] = int(outcomes[&"count"]) + 1
outcomes[&"payload"] = _payload.duplicate(true)
return {&"ok": true, &"token": token}
)
_must(service.create_raid(&"layers", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "layered Raid is created")
_must(service.register_asset(&"layers", &"core", &"OBJECTIVE", 3, 0), "fragile objective is registered")
_must(service.register_asset(&"layers", &"z_door", &"DOOR", 3, 0), "door is registered")
_must(service.register_asset(&"layers", &"a_device", &"DEFENSE_DEVICE", 3, 0), "device is registered")
_must(service.start_raid(&"layers", 0), "layered Raid starts")
var door_round = service.resolve_next_round(&"layers", &"layer_1", 5)
_expect(door_round.is_success() and door_round.details[&"defense_damage"][&"target_id"] == &"z_door", "lowest target-order door is damaged first")
_expect(door_round.details[&"defense_damage"][&"destroyed"], "door integrity reaches zero in its round")
var device_round = service.resolve_next_round(&"layers", &"layer_2", 10)
_expect(device_round.is_success() and device_round.details[&"defense_damage"][&"target_id"] == &"a_device", "device is targeted after destroyed doors")
_expect(device_round.details[&"score_components"][&"asset_defense"] == 0, "destroyed door no longer contributes defense")
var objective_round = service.resolve_next_round(&"layers", &"layer_3", 15)
_expect(objective_round.is_success() and objective_round.details[&"defense_damage"][&"target_id"] == &"core", "objective is reached after devices")
_expect(objective_round.details[&"state"] == &"BREACHED" and objective_round.details[&"reason"] == &"OBJECTIVE_DESTROYED", "destroyed objective breaches the Raid")
var raid := service.get_raid(&"layers")
_expect(raid[&"assets"][&"z_door"][&"integrity"] == 0 and raid[&"assets"][&"a_device"][&"integrity"] == 0 and raid[&"assets"][&"core"][&"integrity"] == 0, "all three defense layers retain their per-round integrity updates")
_expect(raid[&"waves"][0][&"state"] == &"BREACHED" and raid[&"result"] == &"BREACHED", "wave and Raid terminal states agree")
_expect(outcomes[&"count"] == 1, "terminal outcome boundary applies exactly once")
var asset_results: Array = outcomes[&"payload"][&"asset_results"]
_expect([asset_results[0][&"asset_id"], asset_results[1][&"asset_id"], asset_results[2][&"asset_id"]] == [&"a_device", &"core", &"z_door"], "terminal asset reconciliation payload uses stable ID order")
_expect(asset_results.all(func(asset: Dictionary) -> bool: return bool(asset[&"destroyed"])), "terminal payload exposes final integrity for every destroyed defense layer")
_expect(service.validate_invariants()[&"ok"], "breached layered Raid remains valid")
func _test_explicit_and_offscreen_attackers_share_one_formula() -> void:
var aggregate_only := _attack_mix_service(&"aggregate_only", false)
var mixed := _attack_mix_service(&"mixed", true)
var aggregate_round = aggregate_only.resolve_next_round(&"aggregate_only", &"round", 5)
var mixed_round = mixed.resolve_next_round(&"mixed", &"round", 5)
_expect(aggregate_round.is_success() and mixed_round.is_success(), "aggregate-only and mixed rounds resolve")
_expect(aggregate_round.details[&"attack_score"] == mixed_round.details[&"attack_score"], "equal explicit/offscreen effective power produces the same attack score")
_expect(aggregate_round.details[&"damage_to_threat"] == mixed_round.details[&"damage_to_threat"], "equal attack power uses the same damage formula")
_expect(mixed_round.details[&"attacker_damage"].size() == 1, "mixed damage is allocated to the stable explicit attacker first")
_expect(mixed_round.details[&"attacker_damage"][0][&"target_id"] == &"visible_raider" and mixed_round.details[&"attacker_damage"][0][&"damage"] == 2, "explicit attacker consumes only its actual remaining durability")
_expect(mixed_round.details[&"aggregate_damage"] == mixed_round.details[&"damage_applied_to_threat"] - 2, "only leftover damage reaches the offscreen aggregate")
_expect(mixed.get_raid(&"mixed")[&"waves"][0][&"remaining_threat"] == 10 - mixed_round.details[&"aggregate_damage"], "aggregate threat is reduced once, never in parallel with explicit damage")
_expect(mixed.get_raid(&"mixed")[&"combatants"][&"visible_raider"][&"state"] == DefenseServiceType.CombatantState.INCAPACITATED, "explicit attacker state follows its health")
_expect(mixed.validate_invariants()[&"ok"], "mixed on/offscreen attacker state remains valid")
func _test_active_skills_and_successful_boundaries() -> void:
var calls: Array[String] = []
var service := DefenseServiceType.new().configure_callbacks(
_fixed_roll(50),
func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
calls.append("%s:%s" % [phase, payload[&"action_id"]])
return {&"ok": true, &"token": token if token != null else String(payload[&"action_id"])}
)
_must(service.create_raid(&"skills", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 50, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "skill Raid is created")
_must(service.register_combatant(&"skills", &"caster", &"DEFENDER", 20, 100, {&"health": 50}), "skill caster is registered")
_must(service.register_asset(&"skills", &"core", &"OBJECTIVE", 100, 5), "skill objective is registered")
_must(service.register_asset(&"skills", &"door", &"DOOR", 100, 5, {&"integrity": 40}), "fortify target is registered")
_must(service.start_raid(&"skills", 0), "skill Raid starts")
_must(service.queue_active_skill(&"skills", &"z_rally", &"caster", &"RALLY", {&"potency": 10}), "RALLY is queued")
_must(service.queue_active_skill(&"skills", &"a_power", &"caster", &"POWER_STRIKE", {&"potency": 5}), "POWER_STRIKE is queued")
_must(service.queue_active_skill(&"skills", &"m_heal", &"caster", &"HEAL", {&"potency": 7, &"target_id": &"caster"}), "HEAL is queued")
_must(service.queue_active_skill(&"skills", &"n_fortify", &"caster", &"FORTIFY", {&"potency": 6, &"target_id": &"door"}), "FORTIFY is queued")
var queued_replay = service.queue_active_skill(&"skills", &"z_rally", &"caster", &"RALLY", {&"potency": 99})
_expect(queued_replay.is_success() and queued_replay.is_replay, "duplicate queued action ID is an idempotent replay")
var result = service.resolve_next_round(&"skills", &"skill_round", 5)
_expect(result.is_success(), "round with all active skills resolves")
_expect(result.details[&"skill_ids"] == [&"a_power", &"m_heal", &"n_fortify", &"z_rally"], "skills resolve in stable action-ID order")
_expect(result.details[&"score_components"][&"skill_bonus"] == 10, "RALLY contributes its score bonus")
_expect(result.details[&"damage_to_threat"] >= 6, "POWER_STRIKE adds direct threat damage")
var raid := service.get_raid(&"skills")
_expect(raid[&"combatants"][&"caster"][&"health"] == 57, "HEAL updates defender health before damage")
var fortify_effect: Dictionary = result.details[&"skill_effects"][2]
_expect(fortify_effect[&"action_id"] == &"n_fortify" and fortify_effect[&"repaired"] == 6, "FORTIFY repairs its target before the attack")
_expect(raid[&"assets"][&"door"][&"integrity"] == 46 - int(result.details[&"defense_damage"][&"damage"]), "round damage is applied after the FORTIFY repair")
_expect(raid[&"queued_skills"].is_empty() and raid[&"applied_skill_ids"].size() == 4, "applied skills move from queue to idempotency history")
_expect(calls.slice(0, 4) == ["validate:a_power", "validate:m_heal", "validate:n_fortify", "validate:z_rally"], "all boundary validations use stable action-ID order")
_expect(calls.slice(4, 8) == ["apply:a_power", "apply:m_heal", "apply:n_fortify", "apply:z_rally"], "all boundary applications follow the validated order")
var applied_replay = service.queue_active_skill(&"skills", &"z_rally", &"caster", &"RALLY")
_expect(applied_replay.is_success() and applied_replay.is_replay, "applied skill action ID also replays without recharging")
_expect(service.validate_invariants()[&"ok"], "active skill state satisfies invariants")
func _test_skill_boundary_validation_rollback() -> void:
var calls: Array[String] = []
var service := DefenseServiceType.new().configure_callbacks(
_fixed_roll(50),
func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
calls.append("%s:%s" % [phase, payload[&"action_id"]])
if phase == &"validate" and payload[&"action_id"] == &"b_reject":
return {&"ok": false, &"reason": &"NO_RESOURCE"}
return {&"ok": true, &"token": token if token != null else String(payload[&"action_id"])}
)
_prepare_basic(service, &"validation_rollback")
_must(service.queue_active_skill(&"validation_rollback", &"a_reserved", &"guard", &"RALLY", {&"potency": 3}), "first reserved skill is queued")
_must(service.queue_active_skill(&"validation_rollback", &"b_reject", &"guard", &"RALLY", {&"potency": 3}), "rejecting skill is queued")
var before := service.create_snapshot()
var failed = service.resolve_next_round(&"validation_rollback", &"rejected_round", 5)
_expect(not failed.is_success(), "skill validation rejection aborts the round")
_expect(service.create_snapshot() == before, "validation rejection leaves all local Raid state unchanged")
_expect(calls == ["validate:a_reserved", "validate:b_reject", "rollback:a_reserved"], "earlier validation reservations roll back in reverse order")
_expect(service.validate_invariants()[&"ok"], "validation rollback preserves invariants")
func _test_skill_boundary_apply_and_outcome_rollback() -> void:
var apply_calls: Array[String] = []
var applying := DefenseServiceType.new().configure_callbacks(
_fixed_roll(50),
func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
apply_calls.append("%s:%s" % [phase, payload[&"action_id"]])
if phase == &"apply" and payload[&"action_id"] == &"b_fail":
return {&"ok": false, &"token": "failed_token"}
return {&"ok": true, &"token": token if token != null else String(payload[&"action_id"])}
)
_prepare_basic(applying, &"apply_rollback")
for action_id: StringName in [&"a_apply", &"b_fail", &"c_validated"]:
_must(applying.queue_active_skill(&"apply_rollback", action_id, &"guard", &"RALLY", {&"potency": 2}), "apply rollback skill queues: %s" % action_id)
var apply_before := applying.create_snapshot()
var apply_failed = applying.resolve_next_round(&"apply_rollback", &"apply_failed", 5)
_expect(not apply_failed.is_success(), "skill application rejection aborts the round")
_expect(applying.create_snapshot() == apply_before, "application rejection leaves local state atomic")
_expect(apply_calls == [
"validate:a_apply", "validate:b_fail", "validate:c_validated",
"apply:a_apply", "apply:b_fail",
"rollback:c_validated", "rollback:b_fail", "rollback:a_apply",
], "applied, failed and not-yet-applied validation tokens all roll back once")
var skill_calls: Array[String] = []
var outcome_calls: Array[String] = []
var skill_boundary := func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
skill_calls.append("%s:%s" % [phase, payload[&"action_id"]])
return {&"ok": true, &"token": token if token != null else &"skill_token"}
var outcome_boundary := func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
outcome_calls.append(String(phase))
if phase == &"apply":
return {&"ok": false, &"token": &"outcome_token"}
return {&"ok": true, &"token": token if token != null else &"outcome_token"}
var terminal := DefenseServiceType.new().configure_callbacks(
_fixed_roll(50), skill_boundary, outcome_boundary
)
_must(terminal.create_raid(&"outcome_rollback", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}],
})), "terminal rollback Raid is created")
_must(terminal.register_combatant(&"outcome_rollback", &"guard", &"DEFENDER", 10, 100), "terminal rollback guard is registered")
_must(terminal.register_asset(&"outcome_rollback", &"core", &"OBJECTIVE", 100, 0), "terminal rollback objective is registered")
_must(terminal.start_raid(&"outcome_rollback", 0), "terminal rollback Raid starts")
_must(terminal.queue_active_skill(&"outcome_rollback", &"skill", &"guard", &"RALLY", {&"potency": 10}), "terminal rollback skill is queued")
var terminal_before := terminal.create_snapshot()
var outcome_failed = terminal.resolve_next_round(&"outcome_rollback", &"outcome_failed", 5)
_expect(not outcome_failed.is_success(), "rejected terminal outcome aborts the round")
_expect(terminal.create_snapshot() == terminal_before, "outcome rejection restores round, skill and wave state atomically")
_expect(skill_calls == ["validate:skill", "apply:skill", "rollback:skill"], "successful external skill application is rolled back after outcome failure")
_expect(outcome_calls == ["validate", "apply", "rollback"], "failed outcome apply invokes its rollback boundary")
_expect(terminal.validate_invariants()[&"ok"], "cross-boundary rollback leaves a valid Raid")
func _test_rng_transaction_retry_stability() -> void:
var rng_state := {&"cursor": 0, &"values": [17, 83, 41, 59], &"phases": []}
var outcome_state := {&"apply_attempts": 0}
var roll_callback := func(_key: StringName, _context: Dictionary) -> int:
var cursor := int(rng_state[&"cursor"])
var value := int(rng_state[&"values"][cursor])
rng_state[&"cursor"] = cursor + 1
return value
var rng_boundary := func(_context: Dictionary, phase: StringName, token: Variant) -> Dictionary:
(rng_state[&"phases"] as Array).append(phase)
if phase == &"snapshot":
return {&"ok": true, &"token": int(rng_state[&"cursor"])}
if phase == &"rollback":
rng_state[&"cursor"] = int(token)
return {&"ok": true, &"token": token}
var outcome_boundary := func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
if phase == &"apply":
outcome_state[&"apply_attempts"] = int(outcome_state[&"apply_attempts"]) + 1
if int(outcome_state[&"apply_attempts"]) == 1:
return {&"ok": false, &"token": &"first_attempt"}
return {&"ok": true, &"token": token}
var service := DefenseServiceType.new().configure_callbacks(
roll_callback, Callable(), outcome_boundary, rng_boundary
)
_must(service.create_raid(&"rng_retry", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}],
})), "RNG retry Raid is created")
_must(service.register_asset(&"rng_retry", &"core", &"OBJECTIVE", 1_000, 0), "RNG retry objective is registered")
_must(service.start_raid(&"rng_retry", 0), "RNG retry Raid starts")
var before := service.create_snapshot()
var failed = service.resolve_next_round(&"rng_retry", &"rng_resolution", 5)
_expect(not failed.is_success(), "terminal outcome failure aborts a stateful-RNG round")
_expect(service.create_snapshot() == before, "failed stateful-RNG round leaves Defense state atomic")
_expect(rng_state[&"cursor"] == 0, "RNG rollback restores the cursor consumed by both rolls")
_expect(rng_state[&"phases"] == [&"snapshot", &"rollback"], "failed round snapshots then rolls back RNG state")
var retried = service.resolve_next_round(&"rng_retry", &"rng_resolution", 5)
_expect(retried.is_success() and retried.details[&"defense_roll"] == 17 and retried.details[&"attack_roll"] == 83, "retry consumes the exact same RNG values")
_expect(rng_state[&"cursor"] == 2, "successful retry commits exactly two RNG draws")
_expect(rng_state[&"phases"] == [&"snapshot", &"rollback", &"snapshot", &"commit"], "successful retry commits its RNG transaction")
var replay = service.resolve_next_round(&"rng_retry", &"rng_resolution", 999)
_expect(replay.is_success() and replay.is_replay and replay.details == retried.details, "RNG-backed resolution replays its exact committed payload")
_expect(rng_state[&"cursor"] == 2 and (rng_state[&"phases"] as Array).size() == 4, "resolution replay consumes no RNG or boundary phase")
_expect(service.validate_invariants()[&"ok"], "RNG retry result satisfies invariants")
func _test_retreat_policies_and_manual_retreat_replay() -> void:
var threshold := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(threshold.create_raid(&"threshold", 0, _definition({&"retreat_policy": &"THRESHOLD", &"defender_retreat_threshold": 50})), "threshold Raid is created")
_must(threshold.register_combatant(&"threshold", &"guard", &"DEFENDER", 10, 100, {&"health": 40}), "wounded threshold guard is registered")
_must(threshold.register_asset(&"threshold", &"core", &"OBJECTIVE", 100, 5), "threshold objective is registered")
_must(threshold.start_raid(&"threshold", 0), "threshold Raid starts")
var threshold_round = threshold.resolve_next_round(&"threshold", &"threshold_round", 5)
_expect(threshold_round.is_success() and threshold_round.details[&"state"] == &"DEFENDERS_RETREATED", "THRESHOLD retreats at or below configured health percentage")
_expect(threshold.get_raid(&"threshold")[&"combatants"][&"guard"][&"state"] == DefenseServiceType.CombatantState.RETREATED, "automatic retreat updates active defenders")
var protect := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(protect.create_raid(&"protect", 0, _definition({&"retreat_policy": &"PROTECT_AND_RETREAT", &"defender_retreat_threshold": 50})), "protect Raid is created")
_must(protect.register_combatant(&"protect", &"guard", &"DEFENDER", 10, 100), "protect guard is registered")
_must(protect.register_asset(&"protect", &"core", &"OBJECTIVE", 100, 5, {&"integrity": 40}), "pre-damaged protected objective is registered")
_must(protect.start_raid(&"protect", 0), "protect Raid starts")
var protect_round = protect.resolve_next_round(&"protect", &"protect_round", 5)
_expect(protect_round.is_success() and protect_round.details[&"reason"] == &"RETREAT_POLICY_TRIGGERED", "PROTECT_AND_RETREAT observes objective integrity against full capacity")
for policy: StringName in [&"HOLD", &"MANUAL"]:
var holding := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(holding.create_raid(policy, 0, _definition({&"retreat_policy": policy, &"defender_retreat_threshold": 90})), "%s Raid is created" % policy)
_must(holding.register_combatant(policy, &"guard", &"DEFENDER", 10, 100, {&"health": 10}), "%s wounded guard is registered" % policy)
_must(holding.register_asset(policy, &"core", &"OBJECTIVE", 100, 5), "%s objective is registered" % policy)
_must(holding.start_raid(policy, 0), "%s Raid starts" % policy)
var round = holding.resolve_next_round(policy, StringName("%s_round" % policy), 5)
_expect(round.is_success() and round.details[&"state"] == &"ACTIVE", "%s never triggers automatic retreat" % policy)
var outcome_count := {&"apply": 0}
var manual := DefenseServiceType.new().configure_callbacks(
_fixed_roll(50), Callable(),
func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
if phase == &"apply":
outcome_count[&"apply"] = int(outcome_count[&"apply"]) + 1
return {&"ok": true, &"token": token}
)
_prepare_basic(manual, &"manual")
_must(manual.queue_active_skill(&"manual", &"unused_skill", &"guard", &"RALLY", {&"potency": 2}), "manual retreat fixture queues an unused skill")
var retreated = manual.retreat_defenders(&"manual", &"manual_resolution", 3)
var replay = manual.retreat_defenders(&"manual", &"manual_resolution", 99)
_expect(retreated.is_success() and retreated.details[&"result"] == &"DEFENDERS_RETREATED", "manual retreat reaches its terminal state")
_expect(replay.is_success() and replay.is_replay and replay.details == retreated.details, "manual retreat resolution ID replays exact stored outcome")
_expect(outcome_count[&"apply"] == 1, "manual retreat consequence applies once across replay")
_expect(retreated.details[&"retreated_defender_ids"] == [&"guard"] and retreated.details[&"retreated_attacker_ids"].is_empty(), "outcome separates defender and attacker retreat IDs for reconciliation")
_expect(manual.get_raid(&"manual")[&"waves"][0][&"state"] == &"RETREATED", "manual retreat updates current wave state")
_expect(manual.get_raid(&"manual")[&"queued_skills"].is_empty(), "manual retreat discards skills that never crossed the apply boundary")
_expect(manual.validate_invariants()[&"ok"], "all retreat policy outcomes preserve invariants")
func _test_cancel_transaction_and_reconciliation() -> void:
var boundary_state := {&"apply_attempts": 0, &"rollbacks": 0, &"last_payload": {}}
var resolved_signals := {&"count": 0, &"payload": {}}
var outcome_boundary := func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
if phase == &"apply":
boundary_state[&"apply_attempts"] = int(boundary_state[&"apply_attempts"]) + 1
boundary_state[&"last_payload"] = payload.duplicate(true)
if int(boundary_state[&"apply_attempts"]) == 1:
return {&"ok": false, &"token": &"cancel_attempt"}
elif phase == &"rollback":
boundary_state[&"rollbacks"] = int(boundary_state[&"rollbacks"]) + 1
return {&"ok": true, &"token": token}
var service := DefenseServiceType.new().configure_callbacks(
_fixed_roll(50), Callable(), outcome_boundary
)
service.raid_resolved.connect(func(_raid_id: StringName, payload: Dictionary) -> void:
resolved_signals[&"count"] = int(resolved_signals[&"count"]) + 1
resolved_signals[&"payload"] = payload.duplicate(true)
)
_must(service.create_raid(&"cancel_tx", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "cancel transaction Raid is created")
_must(service.register_combatant(&"cancel_tx", &"guard", &"DEFENDER", 10, 100), "cancel transaction guard is registered")
_must(service.register_asset(&"cancel_tx", &"core", &"OBJECTIVE", 1_000, 0), "cancel transaction objective is registered")
_must(service.register_asset(&"cancel_tx", &"door", &"DOOR", 30, 0), "cancel transaction door is registered")
_must(service.start_raid(&"cancel_tx", 0), "cancel transaction Raid starts")
_must(service.resolve_next_round(&"cancel_tx", &"damage_round", 5), "cancel transaction accumulates one round of damage")
_must(service.queue_active_skill(&"cancel_tx", &"pending_on_cancel", &"guard", &"RALLY", {
&"potency": 3, &"metadata": {&"source": "cancel-test"},
}), "cancel transaction retains a pending skill before cancellation")
var before_cancel := service.create_snapshot()
var rejected = service.cancel_raid(&"cancel_tx", 6, &"PLAYER_ABORT", &"cancel_resolution")
_expect(not rejected.is_success(), "cancel outcome rejection aborts cancellation")
_expect(service.create_snapshot() == before_cancel, "rejected cancellation preserves accumulated Raid state atomically")
_expect(boundary_state[&"apply_attempts"] == 1 and boundary_state[&"rollbacks"] == 1, "failed cancel apply invokes outcome rollback")
_expect(resolved_signals[&"count"] == 0, "rejected cancellation emits no raid_resolved signal")
var cancelled = service.cancel_raid(&"cancel_tx", 6, &"PLAYER_ABORT", &"cancel_resolution")
_expect(cancelled.is_success() and cancelled.details[&"result"] == &"CANCELLED", "accepted cancellation reaches CANCELLED through the outcome boundary")
_expect(cancelled.details[&"operation_kind"] == &"CANCEL", "cancel result stores its operation namespace")
_expect(cancelled.details[&"asset_results"] == boundary_state[&"last_payload"][&"asset_results"], "cancel boundary receives final accumulated asset integrity")
_expect(cancelled.details[&"combatant_results"] == boundary_state[&"last_payload"][&"combatant_results"], "cancel boundary receives final accumulated combatant state")
_expect(service.get_raid(&"cancel_tx")[&"queued_skills"].is_empty(), "accepted cancel discards skills that never crossed the apply boundary")
_expect(resolved_signals[&"count"] == 1 and resolved_signals[&"payload"] == cancelled.details, "accepted cancel emits one exact raid_resolved payload")
var replay = service.cancel_raid(&"cancel_tx", 999, &"DIFFERENT_REASON", &"cancel_resolution")
_expect(replay.is_success() and replay.is_replay and replay.details == cancelled.details, "cancel resolution replays canonically after terminal state")
_expect(boundary_state[&"apply_attempts"] == 2 and resolved_signals[&"count"] == 1, "cancel replay cannot reapply consequences or signals")
_expect(service.validate_invariants()[&"ok"], "transactional cancellation satisfies invariants")
func _test_resolution_operation_namespace_and_legacy_v1() -> void:
var round_service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_prepare_basic(round_service, &"namespace")
var round_result = round_service.resolve_next_round(&"namespace", &"shared_id", 5)
_expect(round_result.is_success() and round_result.details[&"operation_kind"] == &"ROUND", "round history stores ROUND operation kind")
var after_round := round_service.create_snapshot()
var wrong_retreat = round_service.retreat_defenders(&"namespace", &"shared_id", 6)
_expect(not wrong_retreat.is_success() and not wrong_retreat.is_replay, "round resolution ID cannot replay as manual retreat")
_expect(round_service.create_snapshot() == after_round, "cross-operation retreat collision is atomic")
var wrong_cancel = round_service.cancel_raid(&"namespace", 6, &"CANCELLED", &"shared_id")
_expect(not wrong_cancel.is_success() and not wrong_cancel.is_replay, "round resolution ID cannot replay as cancellation")
_expect(round_service.create_snapshot() == after_round, "cross-operation cancel collision is atomic")
var retreat_service := DefenseServiceType.new()
_prepare_basic(retreat_service, &"retreat_namespace")
var retreat = retreat_service.retreat_defenders(&"retreat_namespace", &"terminal_id", 3)
_expect(retreat.is_success() and retreat.details[&"operation_kind"] == &"RETREAT", "manual retreat history stores RETREAT operation kind")
var wrong_round = retreat_service.resolve_next_round(&"retreat_namespace", &"terminal_id", 500)
_expect(not wrong_round.is_success() and not wrong_round.is_replay, "retreat resolution ID cannot replay as a round")
var legacy_round_snapshot := after_round.duplicate(true)
legacy_round_snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"].erase("operation_kind")
var legacy_round := DefenseServiceType.new()
_expect(legacy_round.restore_snapshot(legacy_round_snapshot), "version-1 round snapshot without operation_kind restores")
_expect(legacy_round.create_snapshot() == legacy_round_snapshot, "legacy round snapshot round-trips without forced schema rewrite")
var legacy_round_replay = legacy_round.resolve_next_round(&"namespace", &"shared_id", 999)
_expect(legacy_round_replay.is_success() and legacy_round_replay.is_replay, "legacy round operation kind is inferred for exact replay")
_expect(not legacy_round.retreat_defenders(&"namespace", &"shared_id", 999).is_success(), "legacy inferred round still rejects cross-operation replay")
var legacy_retreat_snapshot := retreat_service.create_snapshot()
legacy_retreat_snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"].erase("operation_kind")
var legacy_retreat := DefenseServiceType.new()
_expect(legacy_retreat.restore_snapshot(legacy_retreat_snapshot), "version-1 retreat snapshot without operation_kind restores")
var legacy_retreat_replay = legacy_retreat.retreat_defenders(&"retreat_namespace", &"terminal_id", 999)
_expect(legacy_retreat_replay.is_success() and legacy_retreat_replay.is_replay, "legacy retreat operation kind is inferred for exact replay")
var cancel_service := DefenseServiceType.new()
_prepare_basic(cancel_service, &"cancel_namespace")
_must(cancel_service.cancel_raid(&"cancel_namespace", 3, &"LEGACY_CANCEL", &"cancel_terminal_id"), "legacy cancel fixture reaches its terminal state")
var legacy_cancel_snapshot := cancel_service.create_snapshot()
legacy_cancel_snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"].erase("operation_kind")
var legacy_cancel := DefenseServiceType.new()
_expect(legacy_cancel.restore_snapshot(legacy_cancel_snapshot), "version-1 cancel snapshot without operation_kind restores")
_expect(legacy_cancel.create_snapshot() == legacy_cancel_snapshot, "legacy cancel snapshot round-trips without forced schema rewrite")
var legacy_cancel_replay = legacy_cancel.cancel_raid(&"cancel_namespace", 999, &"DIFFERENT", &"cancel_terminal_id")
_expect(legacy_cancel_replay.is_success() and legacy_cancel_replay.is_replay, "legacy cancel operation kind is inferred for exact replay")
_expect(not legacy_cancel.retreat_defenders(&"cancel_namespace", &"cancel_terminal_id", 999).is_success(), "legacy inferred cancel rejects cross-operation replay")
_expect(round_service.validate_invariants()[&"ok"] and retreat_service.validate_invariants()[&"ok"] and cancel_service.validate_invariants()[&"ok"], "operation namespaces preserve valid aggregates")
func _test_multiple_waves_require_explicit_transition() -> void:
var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(service.create_raid(&"multi", 0, _definition({
&"waves": [
{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1},
{&"wave_id": &"wave_2", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1},
],
})), "two-wave Raid is created")
_must(service.register_asset(&"multi", &"core", &"OBJECTIVE", 1_000, 0), "durable multi-wave objective is registered")
_must(service.start_raid(&"multi", 0), "multi-wave Raid starts")
var wave_one = service.resolve_next_round(&"multi", &"wave_one_round", 5)
_expect(wave_one.is_success() and wave_one.details[&"state"] == &"BETWEEN_WAVES", "first max-round wave ends BETWEEN_WAVES")
var between_snapshot := service.create_snapshot()
_expect(not service.resolve_next_round(&"multi", &"illegal_between_round", 6).is_success(), "round cannot resolve while waiting between waves")
_expect(service.create_snapshot() == between_snapshot, "failed between-wave round is atomic")
_expect(service.get_raid(&"multi")[&"current_wave_index"] == 0, "next wave never starts implicitly")
var begun = service.begin_next_wave(&"multi", 7)
_expect(begun.is_success() and begun.details[&"wave_id"] == &"wave_2" and begun.details[&"wave_index"] == 1, "explicit command starts exactly the next wave")
_expect(service.get_raid(&"multi")[&"waves"][1][&"state"] == &"ACTIVE", "next wave state becomes ACTIVE")
_expect(not service.begin_next_wave(&"multi", 7).is_success(), "active wave cannot be skipped")
var wave_two = service.resolve_next_round(&"multi", &"wave_two_round", 12)
_expect(wave_two.is_success() and wave_two.details[&"state"] == &"DEFENDED", "last wave completion defends the Raid")
var raid := service.get_raid(&"multi")
_expect(raid[&"current_round"] == 2 and raid[&"elapsed_minutes"] == 10, "round and elapsed counters span all waves")
_expect(raid[&"waves"][0][&"state"] == &"RESOLVED" and raid[&"waves"][1][&"state"] == &"RESOLVED", "each completed wave retains its own state")
_expect(not service.begin_next_wave(&"multi", 13).is_success(), "terminal Raid has no phantom next wave")
_expect(service.validate_invariants()[&"ok"], "multi-wave terminal state satisfies invariants")
func _test_raid_duration_and_wave_round_limits() -> void:
var duration := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(duration.create_raid(&"duration", 0, _definition({
&"max_duration_minutes": 5,
&"round_minutes": 5,
&"waves": [
{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10},
{&"wave_id": &"wave_2", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10},
],
})), "duration-limited Raid is created")
_must(duration.register_asset(&"duration", &"core", &"OBJECTIVE", 1_000, 0), "duration objective is registered")
_must(duration.start_raid(&"duration", 0), "duration Raid starts")
var duration_result = duration.resolve_next_round(&"duration", &"duration_round", 5)
_expect(duration_result.is_success() and duration_result.details[&"state"] == &"DEFENDED", "Raid-wide duration is a hard terminal boundary")
_expect(duration_result.details[&"reason"] == &"RAID_DURATION_EXHAUSTED", "duration terminal reason is explicit")
_expect(duration.get_raid(&"duration")[&"current_wave_index"] == 0, "duration never opens a later wave")
var max_round := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(max_round.create_raid(&"max_round", 0, _definition({
&"max_duration_minutes": 100,
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}],
})), "max-round Raid is created")
_must(max_round.register_asset(&"max_round", &"core", &"OBJECTIVE", 1_000, 0), "max-round objective is registered")
_must(max_round.start_raid(&"max_round", 0), "max-round Raid starts")
var round_result = max_round.resolve_next_round(&"max_round", &"max_round_resolution", 5)
_expect(round_result.is_success() and round_result.details[&"reason"] == &"WAVE_DURATION_EXHAUSTED", "wave max-round limit has a stable terminal reason")
_expect(max_round.get_raid(&"max_round")[&"result"] == &"DEFENDED", "surviving the final wave round limit defends the Raid")
_expect(duration.validate_invariants()[&"ok"] and max_round.validate_invariants()[&"ok"], "both duration boundaries remain valid")
func _test_round_resolution_replay() -> void:
var roll_calls := {&"count": 0}
var roll_keys: Array[StringName] = []
var service := DefenseServiceType.new().configure_callbacks(
func(key: StringName, _context: Dictionary) -> int:
roll_calls[&"count"] = int(roll_calls[&"count"]) + 1
roll_keys.append(key)
return 50
)
_prepare_basic(service, &"replay")
var first = service.resolve_next_round(&"replay", &"same_resolution", 5)
var snapshot_after_first := service.create_snapshot()
var replay = service.resolve_next_round(&"replay", &"same_resolution", 500)
_expect(first.is_success() and replay.is_success() and replay.is_replay, "duplicate resolution ID returns an idempotent replay")
_expect(replay.details == first.details, "resolution replay returns the exact canonical payload")
_expect(service.create_snapshot() == snapshot_after_first, "resolution replay mutates no Raid or clock state")
_expect(roll_calls[&"count"] == 2, "defense and attack rolls are consumed only on first resolution")
_expect(roll_keys == [&"replay:wave_1:1:DEFENSE", &"replay:wave_1:1:ATTACK"], "round roll keys and call order are stable")
_expect(service.get_raid(&"replay")[&"current_round"] == 1, "replay cannot advance the round twice")
_expect(service.validate_invariants()[&"ok"], "replayed Raid remains valid")
var default_a := DefenseServiceType.new()
var default_b := DefenseServiceType.new()
_prepare_basic(default_a, &"default_roll")
_prepare_basic(default_b, &"default_roll")
var default_result_a = default_a.resolve_next_round(&"default_roll", &"default_round", 5)
var default_result_b = default_b.resolve_next_round(&"default_roll", &"default_round", 5)
_expect(default_result_a.details[&"defense_roll"] == default_result_b.details[&"defense_roll"] and default_result_a.details[&"attack_roll"] == default_result_b.details[&"attack_roll"], "built-in fallback rolls are deterministic from stable keys")
_expect(default_result_a.details == default_result_b.details and default_a.create_snapshot() == default_b.create_snapshot(), "built-in deterministic roll reproduces the full round")
func _test_round_clock_contract() -> void:
var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_prepare_basic(service, &"clock_contract")
var initial := service.create_snapshot()
var too_early = service.resolve_next_round(&"clock_contract", &"too_early", 4)
_expect(not too_early.is_success() and too_early.details[&"earliest_minute"] == 5, "first round waits one full configured interval after start")
_expect(service.create_snapshot() == initial, "early first round rejection is atomic")
_must(service.resolve_next_round(&"clock_contract", &"round_1", 5), "first round resolves at its exact boundary")
var after_first := service.create_snapshot()
_expect(not service.resolve_next_round(&"clock_contract", &"same_minute_other_id", 5).is_success(), "different resolution ID cannot create a second round in the same minute")
_expect(not service.resolve_next_round(&"clock_contract", &"before_interval", 9).is_success(), "later round cannot violate previous-round interval")
_expect(service.create_snapshot() == after_first, "same-minute and interval failures preserve round history")
_must(service.resolve_next_round(&"clock_contract", &"round_2", 10), "second round resolves after the full interval")
_must(service.advance_time(20), "Defense clock advances independently")
var after_clock := service.create_snapshot()
_expect(not service.resolve_next_round(&"clock_contract", &"past_global_clock", 19).is_success(), "round cannot be inserted behind the current Defense clock")
_expect(service.create_snapshot() == after_clock, "past-global-clock rejection is atomic")
_must(service.resolve_next_round(&"clock_contract", &"round_3", 20), "round at current clock succeeds when interval is satisfied")
var historical := DefenseServiceType.new()
_must(historical.advance_time(10), "historical fixture clock advances")
_expect(not historical.create_raid(&"past_create", 9, _definition()).is_success(), "Raid cannot be created in the past of the Defense clock")
_must(historical.create_raid(&"future_start", 10, _definition()), "Raid can be created at the current clock")
_must(historical.register_asset(&"future_start", &"core", &"OBJECTIVE", 100, 0), "future-start objective is registered")
_must(historical.advance_time(15), "clock advances before Raid start")
_expect(not historical.start_raid(&"future_start", 14).is_success(), "Raid cannot start behind the current Defense clock")
_must(historical.start_raid(&"future_start", 15), "Raid starts at the current Defense clock")
_expect(not historical.resolve_next_round(&"future_start", &"early_after_start", 19).is_success(), "first round interval is measured from actual start time")
_must(historical.resolve_next_round(&"future_start", &"on_time_after_start", 20), "first round resolves exactly one interval after late start")
_expect(service.validate_invariants()[&"ok"] and historical.validate_invariants()[&"ok"], "clock-contract fixtures satisfy invariants")
func _test_wave_entry_and_metadata_validation() -> void:
var entry_service := DefenseServiceType.new()
_must(entry_service.create_raid(&"entry_scope", 0, _definition({
&"entry_points": [&"east", &"west"],
&"waves": [{
&"wave_id": &"east_wave", &"threat_strength": 20,
&"entry_points": [&"east"], &"objective_ids": [&"core"],
}],
})), "entry-scope Raid is created")
_expect(not entry_service.register_combatant(&"entry_scope", &"wrong_gate", &"ATTACKER", 10, 10, {
&"wave_id": &"east_wave", &"entry_point_id": &"west",
}).is_success(), "attacker cannot use a global entry omitted by its assigned wave")
_expect(entry_service.register_combatant(&"entry_scope", &"east_raider", &"ATTACKER", 10, 10, {
&"wave_id": &"east_wave", &"entry_point_id": &"east",
}).is_success(), "attacker can use an entry included by its assigned wave")
var metadata_service := DefenseServiceType.new()
_prepare_basic(metadata_service, &"metadata")
var metadata_before := metadata_service.create_snapshot()
var invalid_metadata: Array = [
{&"label": "non-dictionary root", &"value": 7},
{&"label": "Object", &"value": {"bad": RefCounted.new()}},
{&"label": "Callable", &"value": {"bad": Callable(self, "_run")}},
{&"label": "RID", &"value": {"bad": RID()}},
{&"label": "NaN", &"value": {"bad": NAN}},
{&"label": "positive infinity", &"value": {"nested": [1, INF]}},
{&"label": "non-string dictionary key", &"value": {1: "bad"}},
]
for fixture: Dictionary in invalid_metadata:
var result = metadata_service.queue_active_skill(&"metadata", &"invalid_meta", &"guard", &"RALLY", {
&"potency": 1, &"metadata": fixture[&"value"],
})
_expect(not result.is_success(), "skill metadata rejects %s" % fixture[&"label"])
_expect(metadata_service.create_snapshot() == metadata_before, "rejected %s metadata is atomic" % fixture[&"label"])
var metadata_a := {"zeta": [&"name", 2, true, null], "alpha": {"b": 2.5, "a": "first"}}
var metadata_b := {"alpha": {"a": "first", "b": 2.5}, "zeta": [&"name", 2, true, null]}
_must(metadata_service.queue_active_skill(&"metadata", &"canonical", &"guard", &"RALLY", {
&"potency": 1, &"metadata": metadata_a,
}), "nested saveable metadata is accepted")
var stored_metadata: Dictionary = metadata_service.get_raid(&"metadata")[&"queued_skills"][&"canonical"][&"metadata"]
_expect(stored_metadata.keys() == ["alpha", "zeta"], "metadata dictionary keys are stored in lexical order")
_expect(stored_metadata["zeta"][0] == "name" and stored_metadata["alpha"].keys() == ["a", "b"], "metadata recursively canonicalizes StringName values and nested keys")
var metadata_peer := DefenseServiceType.new()
_prepare_basic(metadata_peer, &"metadata")
_must(metadata_peer.queue_active_skill(&"metadata", &"canonical", &"guard", &"RALLY", {
&"potency": 1, &"metadata": metadata_b,
}), "reverse-order metadata fixture is accepted")
_expect(metadata_service.create_snapshot() == metadata_peer.create_snapshot(), "metadata snapshot is independent of dictionary insertion order")
var corrupt_metadata := metadata_service.create_snapshot()
corrupt_metadata[&"raids"][0][&"queued_skills"][0][&"metadata"] = {"bad": NAN}
var restored := DefenseServiceType.new()
_prepare_basic(restored, &"restored_baseline")
var restored_before := restored.create_snapshot()
_expect(not restored.restore_snapshot(corrupt_metadata), "snapshot restore rejects non-finite queued metadata")
_expect(restored.create_snapshot() == restored_before, "corrupt metadata snapshot rejection is atomic")
_expect(metadata_service.validate_invariants()[&"ok"] and metadata_peer.validate_invariants()[&"ok"], "canonical metadata fixtures satisfy invariants")
func _test_armor_pool_accounting() -> void:
var blocked := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(blocked.create_raid(&"armor_block", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 20, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "armor-block Raid is created")
_must(blocked.register_combatant(&"armor_block", &"guard", &"DEFENDER", 100, 100), "armor-block guard is registered")
_must(blocked.register_combatant(&"armor_block", &"armored_raider", &"ATTACKER", 1, 100, {
&"wave_id": &"wave_1", &"armor": 100,
}), "heavily armored attacker is registered")
_must(blocked.register_asset(&"armor_block", &"core", &"OBJECTIVE", 1_000, 0), "armor-block objective is registered")
_must(blocked.start_raid(&"armor_block", 0), "armor-block Raid starts")
var blocked_round = blocked.resolve_next_round(&"armor_block", &"blocked_round", 5)
_expect(blocked_round.is_success() and blocked_round.details[&"damage_to_threat"] > 0, "armored target receives a positive raw damage budget")
_expect(blocked_round.details[&"damage_applied_to_threat"] == 0, "armor uses max(0, damage - armor) without forced chip damage")
_expect(blocked_round.details[&"aggregate_damage"] == 0, "damage absorbed by explicit armor cannot leak into aggregate threat")
_expect(blocked_round.details[&"damage_mitigated_by_attackers"] == blocked_round.details[&"damage_to_threat"], "mitigation accounts for the entire blocked damage pool")
_expect(blocked.get_raid(&"armor_block")[&"waves"][0][&"remaining_threat"] == 20 and blocked.get_raid(&"armor_block")[&"combatants"][&"armored_raider"][&"health"] == 100, "full armor block changes neither explicit nor aggregate durability")
var overflow := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(overflow.create_raid(&"armor_overflow", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 10, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "armor-overflow Raid is created")
_must(overflow.register_combatant(&"armor_overflow", &"guard", &"DEFENDER", 200, 100), "armor-overflow guard is registered")
_must(overflow.register_combatant(&"armor_overflow", &"front_raider", &"ATTACKER", 2, 2, {
&"wave_id": &"wave_1", &"armor": 5,
}), "front armored attacker is registered")
_must(overflow.register_asset(&"armor_overflow", &"core", &"OBJECTIVE", 1_000, 0), "armor-overflow objective is registered")
_must(overflow.start_raid(&"armor_overflow", 0), "armor-overflow Raid starts")
var overflow_round = overflow.resolve_next_round(&"armor_overflow", &"overflow_round", 5)
_expect(overflow_round.details[&"damage_to_threat"] == 12, "raw damage is capped by combined explicit and aggregate durability")
_expect(overflow_round.details[&"attacker_damage"][0][&"damage"] == 2 and overflow_round.details[&"attacker_damage"][0][&"mitigated"] == 5, "front attacker consumes armor plus remaining health from the pool")
_expect(overflow_round.details[&"aggregate_damage"] == 5, "only true raw overflow reaches aggregate threat")
_expect(overflow_round.details[&"damage_applied_to_threat"] == 7 and overflow_round.details[&"damage_mitigated_by_attackers"] == 5, "applied plus mitigated damage reconciles the complete pool")
_expect(overflow.get_raid(&"armor_overflow")[&"waves"][0][&"remaining_threat"] == 5, "aggregate durability decreases by overflow exactly once")
var defended := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(defended.create_raid(&"defense_armor", 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "defense-armor Raid is created")
_must(defended.register_asset(&"defense_armor", &"core", &"OBJECTIVE", 1_000, 0), "defense-armor objective is registered")
_must(defended.register_asset(&"defense_armor", &"door", &"DOOR", 100, 0, {&"armor": 100}), "fully armored defense door is registered")
_must(defended.start_raid(&"defense_armor", 0), "defense-armor Raid starts")
var defense_round = defended.resolve_next_round(&"defense_armor", &"defense_armor_round", 5)
_expect(defense_round.details[&"damage_to_defense"] > 0 and defense_round.details[&"defense_damage"][&"damage"] == 0, "defense armor also applies max(0, damage - armor)")
_expect(defense_round.details[&"defense_damage"][&"mitigated"] == defense_round.details[&"damage_to_defense"] and defended.get_raid(&"defense_armor")[&"assets"][&"door"][&"integrity"] == 100, "fully mitigated defense damage leaves integrity unchanged")
_expect(blocked.validate_invariants()[&"ok"] and overflow.validate_invariants()[&"ok"] and defended.validate_invariants()[&"ok"], "armor pool fixtures satisfy invariants")
func _test_snapshot_round_trip_and_corrupt_atomicity() -> void:
var source := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_must(source.create_raid(&"snapshot_raid", 0, _definition({
&"entry_points": [&"west", &"east"],
&"waves": [
{&"wave_id": &"wave_1", &"threat_strength": 60, &"entry_points": [&"east", &"west"], &"max_rounds": 10},
{&"wave_id": &"wave_2", &"threat_strength": 30, &"entry_points": [&"west"], &"max_rounds": 10},
],
})), "snapshot Raid is created")
_must(source.register_combatant(&"snapshot_raid", &"guard_b", &"DEFENDER", 20, 100), "snapshot defender B is registered")
_must(source.register_combatant(&"snapshot_raid", &"guard_a", &"DEFENDER", 10, 80), "snapshot defender A is registered")
_must(source.register_combatant(&"snapshot_raid", &"raider", &"ATTACKER", 15, 30, {&"wave_id": &"wave_1", &"entry_point_id": &"east"}), "snapshot attacker is registered")
_must(source.register_asset(&"snapshot_raid", &"core", &"OBJECTIVE", 200, 20), "snapshot objective is registered")
_must(source.register_asset(&"snapshot_raid", &"door", &"DOOR", 50, 10, {&"entry_points": [&"east"]}), "snapshot door is registered")
_must(source.start_raid(&"snapshot_raid", 0), "snapshot Raid starts")
_must(source.queue_active_skill(&"snapshot_raid", &"applied", &"guard_a", &"RALLY", {&"potency": 4, &"metadata": {&"source": "test"}}), "snapshot applied skill is queued")
_must(source.resolve_next_round(&"snapshot_raid", &"snapshot_round", 5), "snapshot round is resolved")
_must(source.queue_active_skill(&"snapshot_raid", &"queued", &"guard_b", &"POWER_STRIKE", {&"potency": 2, &"metadata": {&"source": "test"}}), "snapshot pending skill is queued")
var snapshot := source.create_snapshot()
var restored := DefenseServiceType.new()
_expect(restored.restore_snapshot(snapshot), "valid Defense snapshot restores")
_expect(restored.create_snapshot() == snapshot, "Defense snapshot has an exact round trip")
_expect(restored.validate_invariants()[&"ok"], "restored Defense snapshot satisfies invariants")
_expect(restored.get_raid_ids() == [&"snapshot_raid"] and restored.get_active_raid_ids() == [&"snapshot_raid"], "restored indexes expose canonical Raid IDs")
var restored_replay = restored.resolve_next_round(&"snapshot_raid", &"snapshot_round", 999)
_expect(restored_replay.is_success() and restored_replay.is_replay and restored.get_raid(&"snapshot_raid")[&"current_round"] == 1, "restored resolution history remains idempotent")
_expect(JSON.stringify(restored_replay.details) == JSON.stringify(snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"]), "restored replay returns its exact canonical payload")
var json_snapshot: Variant = JSON.parse_string(JSON.stringify(snapshot))
var json_restored := DefenseServiceType.new()
_expect(json_snapshot is Dictionary and json_restored.restore_snapshot(json_snapshot), "JSON-serialized Defense snapshot restores")
var expected_json := JSON.stringify(snapshot)
var actual_json := JSON.stringify(json_restored.create_snapshot())
_expect(actual_json == expected_json, "JSON transport preserves the canonical Defense snapshot")
# Each corrupt restore is attempted against a non-empty live aggregate. A
# rejection must preserve the previously restored snapshot byte-for-value.
var corruptions: Array[Dictionary] = []
var duplicate := snapshot.duplicate(true)
duplicate[&"raids"].append(duplicate[&"raids"][0].duplicate(true))
corruptions.append({&"name": "duplicate Raid ID", &"snapshot": duplicate})
var bad_remaining := snapshot.duplicate(true)
bad_remaining[&"raids"][0][&"waves"][0][&"remaining_threat"] = 9_999
corruptions.append({&"name": "remaining threat above maximum", &"snapshot": bad_remaining})
var bad_wave_state := snapshot.duplicate(true)
bad_wave_state[&"raids"][0][&"waves"][0][&"state"] = "TELEPORTING"
corruptions.append({&"name": "unknown wave state", &"snapshot": bad_wave_state})
var bad_clock := snapshot.duplicate(true)
bad_clock[&"current_minute"] = -1
corruptions.append({&"name": "negative Defense clock", &"snapshot": bad_clock})
var future_clock := snapshot.duplicate(true)
future_clock[&"current_minute"] = 0
future_clock[&"raids"][0][&"state_changed_at"] = 1
corruptions.append({&"name": "Raid timestamp beyond clock", &"snapshot": future_clock})
var bad_health := snapshot.duplicate(true)
bad_health[&"raids"][0][&"combatants"][0][&"health"] = -1
corruptions.append({&"name": "negative combatant health", &"snapshot": bad_health})
var bad_asset_entry := snapshot.duplicate(true)
bad_asset_entry[&"raids"][0][&"assets"][0][&"entry_points"] = ["not_declared"]
corruptions.append({&"name": "unknown asset entry", &"snapshot": bad_asset_entry})
var bad_attacker_wave_entry := snapshot.duplicate(true)
bad_attacker_wave_entry[&"raids"][0][&"waves"][0][&"entry_points"] = ["west"]
corruptions.append({
&"name": "attacker entry omitted by assigned wave",
&"snapshot": bad_attacker_wave_entry,
})
var bad_skill := snapshot.duplicate(true)
bad_skill[&"raids"][0][&"queued_skills"][0][&"potency"] = 101
corruptions.append({&"name": "invalid queued skill potency", &"snapshot": bad_skill})
for corruption: Dictionary in corruptions:
var before := restored.create_snapshot()
_expect(not restored.restore_snapshot(corruption[&"snapshot"]), "corrupt snapshot rejects %s" % corruption[&"name"])
_expect(restored.create_snapshot() == before, "rejected %s is atomic" % corruption[&"name"])
_expect(not restored.last_error.is_empty(), "corrupt restore exposes a diagnostic error")
_expect(restored.validate_invariants()[&"ok"], "all corrupt snapshot attempts preserve live invariants")
func _test_cross_time_snapshot_rejection() -> void:
var source := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
_prepare_basic(source, &"time_snapshot")
_must(source.resolve_next_round(&"time_snapshot", &"time_round_1", 5), "time snapshot first round resolves")
_must(source.resolve_next_round(&"time_snapshot", &"time_round_2", 10), "time snapshot second round resolves")
var valid := source.create_snapshot()
var restored := DefenseServiceType.new()
_expect(restored.restore_snapshot(valid), "valid cross-time snapshot restores")
var corruptions: Array[Dictionary] = []
var start_after_state := valid.duplicate(true)
start_after_state[&"raids"][0][&"started_at"] = 11
corruptions.append({&"name": "start after state change", &"snapshot": start_after_state})
var first_too_early := valid.duplicate(true)
first_too_early[&"raids"][0][&"resolved_rounds"][0][&"payload"]["absolute_minute"] = 4
corruptions.append({&"name": "first round before start interval", &"snapshot": first_too_early})
var same_minute_rounds := valid.duplicate(true)
same_minute_rounds[&"raids"][0][&"resolved_rounds"][1][&"payload"]["absolute_minute"] = 5
corruptions.append({&"name": "two rounds in one minute", &"snapshot": same_minute_rounds})
var resolution_after_state := valid.duplicate(true)
resolution_after_state[&"raids"][0][&"resolved_rounds"][1][&"payload"]["absolute_minute"] = 11
corruptions.append({&"name": "resolution after state change", &"snapshot": resolution_after_state})
var missing_history := valid.duplicate(true)
missing_history[&"raids"][0][&"resolved_rounds"].remove_at(0)
corruptions.append({&"name": "missing round history", &"snapshot": missing_history})
var invalid_operation := valid.duplicate(true)
invalid_operation[&"raids"][0][&"resolved_rounds"][0][&"payload"]["operation_kind"] = "DUEL"
corruptions.append({&"name": "invalid explicit operation kind", &"snapshot": invalid_operation})
var clock_before_state := valid.duplicate(true)
clock_before_state[&"current_minute"] = 9
corruptions.append({&"name": "clock before latest state", &"snapshot": clock_before_state})
var live_finished := valid.duplicate(true)
live_finished[&"raids"][0][&"finished_at"] = 10
corruptions.append({&"name": "live Raid with finish time", &"snapshot": live_finished})
for corruption: Dictionary in corruptions:
var before := restored.create_snapshot()
_expect(not restored.restore_snapshot(corruption[&"snapshot"]), "cross-time snapshot rejects %s" % corruption[&"name"])
_expect(restored.create_snapshot() == before, "rejected %s remains atomic" % corruption[&"name"])
var terminal := DefenseServiceType.new()
_prepare_basic(terminal, &"terminal_time")
_must(terminal.cancel_raid(&"terminal_time", 2, &"TEST_CANCEL", &"terminal_cancel"), "terminal time fixture cancels")
var bad_finish := terminal.create_snapshot()
bad_finish[&"raids"][0][&"finished_at"] = 1
var before_terminal_restore := restored.create_snapshot()
_expect(not restored.restore_snapshot(bad_finish), "terminal snapshot rejects finish time different from state change")
_expect(restored.create_snapshot() == before_terminal_restore, "terminal finish-time rejection is atomic")
var bad_terminal_resolution_time := terminal.create_snapshot()
bad_terminal_resolution_time[&"raids"][0][&"resolved_rounds"][0][&"payload"]["absolute_minute"] = 1
_expect(not restored.restore_snapshot(bad_terminal_resolution_time), "terminal command resolution must occur at the Raid finish time")
_expect(restored.create_snapshot() == before_terminal_restore, "terminal resolution-time rejection is atomic")
var wrong_terminal_operation := terminal.create_snapshot()
wrong_terminal_operation[&"raids"][0][&"resolved_rounds"][0][&"payload"]["operation_kind"] = "RETREAT"
_expect(not restored.restore_snapshot(wrong_terminal_operation), "terminal operation kind must agree with its result and Raid state")
_expect(restored.create_snapshot() == before_terminal_restore, "terminal operation mismatch rejection is atomic")
_expect(restored.validate_invariants()[&"ok"], "cross-time corruptions preserve restored invariants")
func _test_input_order_determinism() -> void:
var forward := _ordered_fixture(false)
var reverse := _ordered_fixture(true)
_expect(forward.create_snapshot() == reverse.create_snapshot(), "canonical snapshot is independent of registration and collection input order")
var forward_round = forward.resolve_next_round(&"ordered", &"ordered_round", 5)
var reverse_round = reverse.resolve_next_round(&"ordered", &"ordered_round", 5)
_expect(forward_round.is_success() and reverse_round.is_success(), "both input-order variants resolve")
_expect(forward_round.details == reverse_round.details, "round payload is input-order deterministic")
_expect(forward.create_snapshot() == reverse.create_snapshot(), "post-round aggregate is input-order deterministic")
_expect(forward_round.details[&"defense_damage"][&"target_id"] == &"a_door", "equal-priority asset targeting uses lexical stable ID")
_expect(forward_round.details[&"attacker_damage"][0][&"target_id"] == &"a_attacker", "explicit attacker damage uses lexical stable ID")
_expect(forward.validate_invariants()[&"ok"] and reverse.validate_invariants()[&"ok"], "both deterministic orderings preserve invariants")
func _definition(overrides: Dictionary = {}) -> Dictionary:
var definition := {
&"source_id": &"fixture",
&"entry_points": [&"gate"],
&"objective_ids": [&"core"],
&"waves": [{
&"wave_id": &"wave_1",
&"threat_strength": 30,
&"retreat_threshold": 0,
&"max_rounds": 12,
}],
&"max_duration_minutes": 120,
&"round_minutes": 5,
&"retreat_policy": &"HOLD",
&"defender_retreat_threshold": 20,
&"terrain_bonus": 0,
&"command_bonus": 0,
&"surprise_penalty": 0,
&"multiple_entry_penalty": 5,
}
for key: Variant in overrides.keys():
definition[key] = overrides[key]
return definition
func _fixed_roll(value: int) -> Callable:
return func(_key: StringName, _context: Dictionary) -> int: return value
func _prepare_basic(service: DefenseService, raid_id: StringName) -> void:
_must(service.create_raid(raid_id, 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "basic Raid is created: %s" % raid_id)
_must(service.register_combatant(raid_id, &"guard", &"DEFENDER", 20, 100), "basic guard is registered: %s" % raid_id)
_must(service.register_asset(raid_id, &"core", &"OBJECTIVE", 1_000, 10), "basic objective is registered: %s" % raid_id)
_must(service.start_raid(raid_id, 0), "basic Raid starts: %s" % raid_id)
func _attack_mix_service(raid_id: StringName, include_explicit: bool) -> DefenseService:
var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
var aggregate_strength := 10 if include_explicit else 12
_must(service.create_raid(raid_id, 0, _definition({
&"waves": [{&"wave_id": &"wave_1", &"threat_strength": aggregate_strength, &"retreat_threshold": 0, &"max_rounds": 10}],
})), "attack mix Raid is created: %s" % raid_id)
_must(service.register_combatant(raid_id, &"guard", &"DEFENDER", 100, 100), "attack mix guard is registered: %s" % raid_id)
if include_explicit:
_must(service.register_combatant(raid_id, &"visible_raider", &"ATTACKER", 2, 2, {&"wave_id": &"wave_1"}), "visible attacker is registered")
_must(service.register_asset(raid_id, &"core", &"OBJECTIVE", 1_000, 0), "attack mix objective is registered: %s" % raid_id)
_must(service.start_raid(raid_id, 0), "attack mix Raid starts: %s" % raid_id)
return service
func _ordered_fixture(reverse: bool) -> DefenseService:
var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50))
var entries: Array[StringName] = [&"east", &"west"]
if reverse:
entries.reverse()
_must(service.create_raid(&"ordered", 0, _definition({
&"entry_points": entries,
&"waves": [{
&"wave_id": &"wave_1", &"threat_strength": 40,
&"entry_points": entries.duplicate(), &"objective_ids": [&"core"],
&"retreat_threshold": 0, &"max_rounds": 10,
}],
})), "ordered Raid definition is created")
var combatants := [
[&"z_guard", &"DEFENDER", 10, 100, {}],
[&"a_guard", &"DEFENDER", 20, 100, {}],
[&"z_attacker", &"ATTACKER", 5, 20, {&"wave_id": &"wave_1"}],
[&"a_attacker", &"ATTACKER", 5, 20, {&"wave_id": &"wave_1"}],
]
var assets := [
[&"core", &"OBJECTIVE", 1_000, 10, {}],
[&"z_door", &"DOOR", 50, 5, {&"target_order": 10}],
[&"a_door", &"DOOR", 50, 5, {&"target_order": 10}],
]
if reverse:
combatants.reverse()
assets.reverse()
for row: Array in combatants:
_must(service.register_combatant(&"ordered", row[0], row[1], row[2], row[3], row[4]), "ordered combatant registers: %s" % row[0])
for row: Array in assets:
_must(service.register_asset(&"ordered", row[0], row[1], row[2], row[3], row[4]), "ordered asset registers: %s" % row[0])
_must(service.start_raid(&"ordered", 0), "ordered Raid starts")
return service
func _must(result: CommandResult, message: String) -> bool:
return _expect(result != null and result.is_success(), message)
func _expect(condition: bool, message: String) -> bool:
_checks += 1
if condition:
return true
_failures += 1
push_error(" CHECK FAILED: %s" % message)
return false
+1
View File
@@ -0,0 +1 @@
uid://ceq33fn7vgm51
+542
View File
@@ -0,0 +1,542 @@
extends SceneTree
const PLAYER_CASH := &"PLAYER_CASH"
const AUTHORITY_FINE_EXPENSE := &"AUTHORITY_FINE_EXPENSE"
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S5: integrated Authority runtime cycle")
_test_incident_visibility_and_daily_evaluation()
_test_resolution_replay_unreachable_exit_and_long_horizon()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _reset_runtime(seed: int, hour := 6, minute := 0) -> void:
_cleanup()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, hour, minute)
_state.reset()
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
_clock.set_paused(true)
_expect(report[&"valid"], "runtime initializes with the Authority aggregate")
_expect(
_runtime.authority_service.current_minute == _clock.absolute_minute,
"Authority time begins aligned with SimulationKernel"
)
_expect(
_runtime.economy_service.has_account(AUTHORITY_FINE_EXPENSE),
"Authority fine expense account exists at initialization"
)
func _cleanup() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _test_incident_visibility_and_daily_evaluation() -> void:
_reset_runtime(5_501, 5, 59)
var reported := _submit(
&"report_authority_incident",
{
&"incident_id": &"incident.runtime.shared",
&"incident_type": &"illegal_facility",
&"detector_id": &"detector.noise",
&"options": {
&"severity": 80,
&"heat_delta": 100,
&"facility_id": &"facility.hidden",
},
},
&"s5.incident.first"
)
var duplicate := _submit(
&"report_authority_incident",
{
&"incident_id": &"incident.runtime.shared",
&"incident_type": &"conflicting_payload",
&"detector_id": &"detector.witness",
&"options": {&"severity": 100, &"heat_delta": 100},
},
&"s5.incident.second-detector"
)
var incident: AuthorityIncident = _runtime.authority_service.get_incident(&"incident.runtime.shared")
_expect(reported.is_success(), "incident report executes through Runtime Command")
_expect(
duplicate.is_success() and duplicate.is_replay,
"incident_id deduplicates reports from independent detectors"
)
_expect(
_runtime.authority_service.get_authority_state().heat == 100,
"duplicate detector cannot apply Heat twice"
)
_expect(
incident.detector_ids == [&"detector.noise", &"detector.witness"]
and incident.incident_type == &"illegal_facility",
"dedup retains sorted detector audit data and the first canonical payload"
)
_expect(
_count_domain_events(&"INCIDENT_CREATED", incident.incident_id) == 1
and _count_domain_events(&"INCIDENT_DETECTOR_ADDED", incident.incident_id) == 1,
"Authority signals project once into DomainEventStream"
)
var wall := Vector3i(1, 0, WorldService.LAYER_FRONT)
var evidence_cell := Vector3i(2, 0, WorldService.LAYER_FRONT)
_expect(_runtime.world_service.set_walkable(wall, false), "LOS fixture wall is installed")
_expect(_runtime.rebuild_path_graph_from_world(), "Path graph rebuilds after wall installation")
var registered := _submit(
&"register_authority_evidence",
{
&"evidence_id": &"evidence.runtime.wall",
&"source": &"visual_trace",
&"severity": 35,
&"options": {
&"incident_id": incident.incident_id,
&"facility_id": &"facility.hidden",
&"location_cell": evidence_cell,
&"expires_at": 2_000,
},
},
&"s5.evidence.register"
)
var blocked := _submit(
&"discover_authority_evidence",
{
&"evidence_id": &"evidence.runtime.wall",
&"inspector_id": &"inspector.los",
&"inspector_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"context": {},
},
&"s5.evidence.blocked"
)
_expect(registered.is_success(), "latent evidence registers through Runtime Command")
_expect(
not blocked.is_success() and blocked.details.get(&"reason", &"") == &"LINE_OF_SIGHT_BLOCKED",
"WorldService wall blocks automatic evidence discovery"
)
_expect(
_runtime.authority_service.get_authority_state().evidence_points == 0,
"wall-blocked evidence contributes no Authority score"
)
_expect(_runtime.world_service.set_walkable(wall, true), "LOS fixture wall is removed")
_expect(_runtime.rebuild_path_graph_from_world(), "Path graph rebuilds after opening sight line")
var discovered := _submit(
&"discover_authority_evidence",
{
&"evidence_id": &"evidence.runtime.wall",
&"inspector_id": &"inspector.los",
&"inspector_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"context": {&"illegal_confirmed": true},
},
&"s5.evidence.visible"
)
_expect(
discovered.is_success() and discovered.details[&"discovery_method"] == &"LINE_OF_SIGHT",
"evidence becomes visible only after the World callback confirms sight"
)
_expect(
_runtime.authority_service.get_authority_state().evidence_points == 35,
"visible evidence contributes its deterministic Incident score"
)
var authority_rng: DeterministicRng = _runtime.get_rng_stream(&"authority")
var expected_rng := DeterministicRng.from_root_seed(0, &"authority") as DeterministicRng
_expect(expected_rng.restore(authority_rng.snapshot()), "Authority RNG snapshot clones for prediction")
var expected_roll := expected_rng.randi_range(0, 99)
var before_six: Dictionary = _runtime.create_snapshot()
_clock.advance_one_minute()
var evaluation := _evaluation_for_day(0)
_expect(
_clock.absolute_minute == 360 and _runtime.authority_service.current_minute == 360,
"INCIDENTS_AND_EVENTS advances Authority time at 06:00"
)
_expect(
int(evaluation.get(&"roll", -1)) == expected_roll
and bool(evaluation.get(&"selected", false)) == (expected_roll < int(evaluation.get(&"chance", 0))),
"06:00 inspection decision consumes only the deterministic Authority RNG stream"
)
_expect(
int(evaluation.get(&"chance", -1)) == 95
and not StringName(evaluation.get(&"inspection_id", &"")).is_empty(),
"Heat and active-case terms deterministically schedule the selected inspection"
)
_expect(
_runtime.get_rng_stream(&"authority").get_counter() == expected_rng.get_counter(),
"06:00 evaluation advances the Authority RNG counter exactly once"
)
_expect(
_count_domain_events(&"AUTHORITY_INSPECTION_EVALUATED") == 1
and _count_domain_events(&"INSPECTION_SCHEDULED") == 1,
"automatic inspection selection emits its Domain Events once"
)
var first_six_snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(before_six), "pre-06:00 Runtime snapshot restores atomically")
_clock.advance_one_minute()
_expect(
_runtime.create_snapshot() == first_six_snapshot,
"restored 06:00 Authority selection reproduces the full Runtime snapshot"
)
var stable_before_corruption: Dictionary = _runtime.create_snapshot()
var corrupt := stable_before_corruption.duplicate(true)
corrupt[&"systems"][&"runtime"][&"authority"][&"current_minute"] = 359
_expect(
not _runtime.restore_snapshot(corrupt),
"restore rejects an Authority/SimulationKernel minute mismatch"
)
_expect(
_runtime.create_snapshot() == stable_before_corruption,
"rejected Authority restore leaves the live Runtime snapshot unchanged"
)
_expect(
_runtime.authority_service.validate_invariants()[&"ok"],
"integrated incident, evidence and inspection selection preserve Authority invariants"
)
func _test_resolution_replay_unreachable_exit_and_long_horizon() -> void:
_reset_runtime(5_502)
_expect(_submit(
&"report_authority_incident",
{
&"incident_id": &"incident.runtime.fine",
&"incident_type": &"contraband",
&"detector_id": &"detector.audit",
&"options": {&"severity": 80, &"heat_delta": 20},
},
&"s5.fine.incident"
).is_success(), "fine fixture Incident registers")
_expect(_submit(
&"register_authority_evidence",
{
&"evidence_id": &"evidence.runtime.fine",
&"source": &"physical_record",
&"severity": 60,
&"options": {
&"incident_id": &"incident.runtime.fine",
&"facility_id": &"facility.illegal",
&"location_cell": Vector3i(2, 0, WorldService.LAYER_FRONT),
},
},
&"s5.fine.evidence"
).is_success(), "fine fixture Evidence registers")
_expect(_submit(
&"schedule_authority_inspection",
{&"options": {
&"inspection_id": &"inspection.runtime.fine",
&"inspector_id": &"inspector.runtime.fine",
&"entrance_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
}},
&"s5.fine.schedule"
).is_success(), "inspection schedules through Runtime Command")
_expect(_submit(
&"start_authority_inspection",
{&"inspection_id": &"inspection.runtime.fine"},
&"s5.fine.start"
).is_success(), "inspection starts through Runtime Command")
_expect(_submit(
&"arrive_authority_inspection",
{&"inspection_id": &"inspection.runtime.fine"},
&"s5.fine.arrive"
).is_success(), "inspector checks in through Runtime Command")
_expect(_submit(
&"plan_authority_inspection",
{
&"inspection_id": &"inspection.runtime.fine",
&"candidates": [{
&"waypoint_id": &"waypoint.illegal.room",
&"cell": Vector3i(2, 0, WorldService.LAYER_FRONT),
&"suspect_score": 100,
&"evidence_potential": 100,
&"travel_minutes": 2,
&"inspection_minutes": 3,
}],
&"time_budget_minutes": 30,
},
&"s5.fine.plan"
).is_success(), "deterministic inspection patrol is planned")
var moved := _submit(
&"advance_authority_inspection",
{&"inspection_id": &"inspection.runtime.fine", &"context": {}},
&"s5.fine.advance"
)
_expect(
moved.is_success() and moved.details.get(&"reachable", false),
"PathService callback advances the inspector to one reachable waypoint"
)
var mid_inspection: Dictionary = _runtime.create_snapshot()
var mid_digest := SimulationDigest.compute(mid_inspection)
var first_final := _run_resolution_timeline()
_expect(first_final.get(&"ok", false), "inspection resolution timeline completes")
var first_final_snapshot: Dictionary = _runtime.create_snapshot()
var first_final_digest := SimulationDigest.compute(first_final_snapshot)
_expect(_runtime.restore_snapshot(mid_inspection), "mid-inspection Runtime snapshot restores")
_expect(
SimulationDigest.compute(_runtime.create_snapshot()) == mid_digest,
"mid-inspection restore reproduces its exact digest"
)
var replay_final := _run_resolution_timeline()
_expect(replay_final.get(&"ok", false), "restored inspection timeline completes again")
_expect(
SimulationDigest.compute(_runtime.create_snapshot()) == first_final_digest
and _runtime.create_snapshot() == first_final_snapshot,
"mid-inspection replay reproduces Authority, ledger and Domain Events exactly"
)
_expect(_run_unreachable_inspection(), "unreachable patrol target follows the safe exit path")
_expect(
_runtime.authority_service.validate_invariants()[&"ok"]
and _runtime.economy_service.validate_ledger()[&"ok"],
"resolved and unreachable inspections preserve Authority and ledger invariants"
)
var horizon_snapshot: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(1_440)
var first_horizon_digest: String = _runtime.latest_digest
var first_horizon_snapshot: Dictionary = _runtime.create_snapshot()
_expect(
_runtime.authority_service.current_minute == _clock.absolute_minute,
"Authority stays aligned through a 24-hour simulation horizon"
)
_expect(_runtime.restore_snapshot(horizon_snapshot), "24-hour Authority replay origin restores")
_clock.advance_minutes(1_440)
_expect(
_runtime.latest_digest == first_horizon_digest
and _runtime.create_snapshot() == first_horizon_snapshot,
"24-hour Runtime replay reproduces daily Heat and inspection evaluation exactly"
)
_expect(
_runtime.authority_service.validate_invariants()[&"ok"]
and _runtime.economy_service.validate_ledger()[&"ok"],
"long replay ends with valid Authority and Economy aggregates"
)
func _run_resolution_timeline() -> Dictionary:
var inspected := _submit(
&"inspect_authority_waypoint",
{
&"inspection_id": &"inspection.runtime.fine",
&"evidence_ids": [&"evidence.runtime.fine"],
&"context": {&"illegal_confirmed": true},
},
&"s5.fine.inspect"
)
var reviewed := _submit(
&"advance_authority_inspection",
{&"inspection_id": &"inspection.runtime.fine", &"context": {}},
&"s5.fine.review"
)
var authority_before_failure: Dictionary = _runtime.authority_service.create_snapshot()
var economy_before_failure: Dictionary = _runtime.economy_service.create_snapshot()
var rejected := _submit(
&"resolve_authority_inspection",
{
&"inspection_id": &"inspection.runtime.fine",
&"resolution_id": &"resolution.runtime.unfunded",
&"options": {&"fine_per_point": 100},
},
&"s5.fine.resolve.unfunded"
)
var rejection_atomic: bool = not rejected.is_success() \
and _runtime.authority_service.create_snapshot() == authority_before_failure \
and _runtime.economy_service.create_snapshot() == economy_before_failure
var resolved := _submit(
&"resolve_authority_inspection",
{
&"inspection_id": &"inspection.runtime.fine",
&"resolution_id": &"resolution.runtime.once",
&"options": {&"fine_per_point": 10},
},
&"s5.fine.resolve.once"
)
var duplicate := _submit(
&"resolve_authority_inspection",
{
&"inspection_id": &"inspection.runtime.fine",
&"resolution_id": &"resolution.runtime.once",
&"options": {&"fine_per_point": 999},
},
&"s5.fine.resolve.retry"
)
var departed := _submit(
&"depart_authority_inspection",
{&"inspection_id": &"inspection.runtime.fine"},
&"s5.fine.depart"
)
var fine_entries: Array = _runtime.economy_service.get_entries().filter(
func(entry: LedgerEntry) -> bool: return entry.category == &"AUTHORITY_FINE"
)
var incident: AuthorityIncident = _runtime.authority_service.get_incident(&"incident.runtime.fine")
var inspection: AuthorityInspection = _runtime.authority_service.get_inspection(&"inspection.runtime.fine")
_expect(inspected.is_success(), "reached waypoint discovers only the supplied Evidence IDs")
_expect(
reviewed.is_success()
and reviewed.details.get(&"new_state", &"") == &"REVIEWING"
and inspection != null,
"completed patrol enters REVIEWING before resolution"
)
_expect(rejection_atomic, "unfunded Authority fine leaves Authority and Economy snapshots unchanged")
_expect(
resolved.is_success() and resolved.details.get(&"fine_amount", -1) == 600
and resolved.details.get(&"sanction", &"") == &"TEMPORARY_CLOSURE",
"evidence score projects the deterministic fine and sanction"
)
_expect(
duplicate.is_success() and duplicate.is_replay
and duplicate.details.get(&"fine_amount", -1) == 600,
"duplicate resolution_id replays the canonical outcome"
)
_expect(
fine_entries.size() == 1
and _runtime.economy_service.get_balance(AUTHORITY_FINE_EXPENSE) == 600
and _runtime.economy_service.get_balance(PLAYER_CASH) == 4_400
and _state.get_resource(&"gold") == 4_400,
"fine posts exactly once to the ledger and synchronized Gold mirror"
)
_expect(
incident != null and incident.state == AuthorityIncident.State.RESOLVED
and departed.is_success() and inspection.state == AuthorityInspection.State.CLOSED,
"successful resolution closes its Incident and inspection lifecycle"
)
_expect(
_count_domain_events(&"INSPECTION_RESOLVED", &"inspection.runtime.fine") == 1,
"resolution replay emits no duplicate INSPECTION_RESOLVED event"
)
return {
&"ok": inspected.is_success()
and reviewed.is_success()
and rejection_atomic
and resolved.is_success()
and duplicate.is_success()
and departed.is_success(),
}
func _run_unreachable_inspection() -> bool:
var inspection_id := &"inspection.runtime.blocked"
var target := Vector3i(0, 1, WorldService.LAYER_FRONT)
var scheduled := _submit(
&"schedule_authority_inspection",
{&"options": {
&"inspection_id": inspection_id,
&"inspector_id": &"inspector.runtime.blocked",
&"entrance_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
}},
&"s5.blocked.schedule"
)
var started := _submit(
&"start_authority_inspection",
{&"inspection_id": inspection_id},
&"s5.blocked.start"
)
var arrived := _submit(
&"arrive_authority_inspection",
{&"inspection_id": inspection_id},
&"s5.blocked.arrive"
)
var planned := _submit(
&"plan_authority_inspection",
{
&"inspection_id": inspection_id,
&"candidates": [{
&"waypoint_id": &"waypoint.sealed",
&"cell": target,
&"suspect_score": 90,
&"evidence_potential": 90,
&"travel_minutes": 5,
&"inspection_minutes": 5,
}],
},
&"s5.blocked.plan"
)
var blocked := _submit(
&"advance_authority_inspection",
{&"inspection_id": inspection_id, &"context": {}},
&"s5.blocked.advance"
)
var inspection: AuthorityInspection = _runtime.authority_service.get_inspection(inspection_id)
var safe_departure: bool = blocked.is_success() \
and not bool(blocked.details.get(&"reachable", true)) \
and inspection != null \
and inspection.state == AuthorityInspection.State.DEPARTING \
and inspection.exit_reason == &"PATH_UNREACHABLE" \
and inspection.visited_waypoint_ids.is_empty() \
and inspection.inaccessible_waypoint_ids == [&"waypoint.sealed"] \
and not inspection.result_reason.is_empty() \
and _count_domain_events(&"INSPECTION_PATH_BLOCKED", inspection_id) == 1
var departed := _submit(
&"depart_authority_inspection",
{&"inspection_id": inspection_id},
&"s5.blocked.depart"
)
return scheduled.is_success() \
and started.is_success() \
and arrived.is_success() \
and planned.is_success() \
and safe_departure \
and departed.is_success() \
and inspection.state == AuthorityInspection.State.CLOSED
func _submit(command_type: StringName, payload: Dictionary, idempotency_key: StringName) -> CommandResult:
return _runtime.submit_command(
command_type,
payload,
&"authority_controller",
&"",
80,
-1,
idempotency_key
)
func _evaluation_for_day(day_index: int) -> Dictionary:
var snapshot: Dictionary = _runtime.authority_service.create_snapshot()
for value: Variant in snapshot[&"evaluated_inspection_days"]:
if value is Dictionary and int(value.get(&"day_index", -1)) == day_index:
return (value[&"result"] as Dictionary).duplicate(true)
return {}
func _count_domain_events(event_type: StringName, entity_id: StringName = &"") -> int:
var count := 0
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type != event_type:
continue
if entity_id.is_empty() or entity_id in event.entity_ids:
count += 1
return count
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://dxr8o0wmgfy0l
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://ct3xctiqfuurj
+86
View File
@@ -0,0 +1,86 @@
extends SceneTree
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S6: performance budgets")
var profiler := SimulationPerformanceBudget.new(4)
_expect(not profiler.record_sample(&"", 10)[&"ok"], "empty metric ID is rejected")
_expect(not profiler.record_sample(&"path_query", -1)[&"ok"], "negative duration is rejected")
_expect(not profiler.set_budget(&"bad", 0), "non-positive budget is rejected")
_expect(profiler.set_budget(&"custom", 100, SimulationPerformanceBudget.Aggregation.P95), "custom budget registers")
for value: int in [10, 20, 30, 40, 50]:
profiler.record_sample(&"custom", value)
var custom := profiler.get_report(&"custom")
_expect(custom[&"window_sample_count"] == 4, "sample window remains bounded")
_expect(custom[&"lifetime_sample_count"] == 5, "lifetime count survives window eviction")
_expect(custom[&"average_us"] == 30, "lifetime average uses every recorded sample")
_expect(custom[&"maximum_us"] == 50, "window maximum is exact")
_expect(custom[&"p95_us"] == 50, "nearest-rank P95 is deterministic")
_expect(not custom[&"budget_exceeded"], "measurement below limit passes")
var exceeded: Array[StringName] = []
profiler.budget_exceeded.connect(func(metric_id: StringName, _report: Dictionary) -> void:
exceeded.append(metric_id)
)
profiler.record_sample(&"custom", 101)
_expect(profiler.get_report(&"custom")[&"budget_exceeded"], "P95 above limit fails")
_expect(exceeded == [&"custom"], "budget violation emits its exact metric once")
profiler.record_simulation_tick(3_000, 1)
profiler.record_simulation_tick(11_000, 4)
_expect(profiler.get_report(&"simulation_x1")[&"limit_us"] == 4_000, "x1 uses the MVP average budget")
_expect(profiler.get_report(&"simulation_x4")[&"limit_us"] == 12_000, "x4 uses the MVP P95 budget")
_expect(not profiler.get_report(&"simulation_x4")[&"budget_exceeded"], "x4 sample within budget passes")
profiler.record_sample(&"path_query", 2_001)
_expect(profiler.get_report(&"path_query")[&"budget_exceeded"], "path P95 enforces two milliseconds")
var reports := profiler.get_all_reports()
var ids: Array[String] = []
for report: Dictionary in reports:
ids.append(String(report[&"metric_id"]))
var sorted_ids := ids.duplicate()
sorted_ids.sort()
_expect(ids == sorted_ids, "all reports are returned in stable metric order")
profiler.clear(&"custom")
_expect(profiler.get_report(&"custom")[&"lifetime_sample_count"] == 0, "single metric can be cleared")
profiler.clear()
_expect(profiler.get_report(&"path_query")[&"lifetime_sample_count"] == 0, "all diagnostic samples can be cleared")
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
clock.process_real_delta(clock.REAL_SECONDS_PER_GAME_MINUTE)
_expect(
clock.performance_budget.get_report(&"simulation_x1")[&"lifetime_sample_count"] == 1,
"real-delta Kernel ticks feed the diagnostic profiler"
)
_expect(not clock.get_performance_reports().is_empty(), "Kernel exposes read-only performance reports")
clock.reset(1, 6, 0)
clock.set_speed(4)
var accelerated_ticks = clock.process_real_delta(clock.REAL_SECONDS_PER_GAME_MINUTE)
_expect(accelerated_ticks == 4, "x4 process frame advances the expected four game-minute ticks")
_expect(clock.performance_budget.get_report(&"simulation_x4")[&"lifetime_sample_count"] == 1, "x4 performance measures the aggregate frame once rather than four isolated ticks")
_expect(clock.performance_budget.get_report(&"main_thread_stall")[&"lifetime_sample_count"] == 1, "active simulation frames feed the main-thread stall budget")
clock.free()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
else:
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://c4bgfjyaobl8i
+697
View File
@@ -0,0 +1,697 @@
extends SceneTree
const TEST_PREFIX := "user://saves/s6_runtime_lifecycle"
const PRODUCTION_AUTOSAVE_PATH := "user://saves/autosave"
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S6: runtime save lifecycle and recovery safety")
await _test_autoload_isolation_and_main_opt_in()
_test_disabled_policy_and_path_timing()
_test_queued_checkpoint_at_tick_boundary()
_test_daily_checkpoint_dedup_generations_and_failure_preservation()
_test_exact_round_trip_recovery_pause_and_performance()
_test_domain_invalid_formal_falls_back_to_backup()
_test_multiple_domain_invalid_candidates_fall_back_to_autosave()
_test_all_domain_invalid_candidates_preserve_complete_live_state()
_test_content_filter_cannot_be_weakened()
_test_whitelisted_recovery_repairs()
_test_non_whitelisted_corruption_is_rejected_atomically()
_cleanup_runtime()
_cleanup_test_saves()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_autoload_isolation_and_main_opt_in() -> void:
var app_clock := root.get_node("GameClock")
var app_state := root.get_node("GameState")
var app_runtime := root.get_node("SimulationRuntime")
app_runtime.configure_save_policy({&"enabled": false})
var default_generation := SaveService.get_autosave_path(
PRODUCTION_AUTOSAVE_PATH,
0
)
var before := _package_fingerprint(default_generation)
app_clock.reset(2, 5, 59)
app_state.reset()
app_runtime.initialize(app_clock, app_state, 60_001)
_expect(not app_runtime.get_save_policy()[&"enabled"], "autoload test runtime starts with disk autosave disabled")
app_clock.advance_one_minute()
_expect(_package_fingerprint(default_generation) == before, "disabled autoload reaches 06:00 without touching the production autosave")
var main := load("res://scenes/main/main.tscn").instantiate() as Control
root.add_child(main)
await process_frame
_expect(
app_runtime.get_save_policy()[&"enabled"]
and app_runtime.get_save_policy()[&"autosave_path"] == PRODUCTION_AUTOSAVE_PATH,
"playable main scene explicitly opts into the production autosave policy"
)
app_runtime.configure_save_policy({&"enabled": false})
main.queue_free()
await process_frame
func _test_disabled_policy_and_path_timing() -> void:
_reset_runtime(60_002, 1, 5, 59)
var path := TEST_PREFIX + "_disabled"
SaveService.delete_save(path)
_expect(
not _runtime.get_save_policy()[&"enabled"]
and _runtime.get_save_policy()[&"autosave_path"] == PRODUCTION_AUTOSAVE_PATH,
"direct Runtime instances remain isolated until explicitly enabled"
)
_clock.advance_one_minute()
_expect(
not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(SaveService.get_autosave_path(path, 0))),
"disabled policy writes no implicit autosave"
)
var before_count := int(
_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"]
)
_runtime.path_service.find_path(
Vector3i(-2, 0, WorldService.LAYER_FRONT),
Vector3i(2, 0, WorldService.LAYER_BACK),
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK],
[&"staff"]
)
var after_count := int(
_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"]
)
_expect(after_count == before_count + 1, "one public path query records one Runtime performance sample")
var snapshot = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(snapshot), "Runtime restores before reconnecting the Path timing boundary")
before_count = int(_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"])
_runtime.path_service.find_path(Vector3i(-1, 0, 0), Vector3i(1, 0, 0))
after_count = int(_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"])
_expect(after_count == before_count + 1, "restore reconnects Path timing without duplicate signal delivery")
func _test_queued_checkpoint_at_tick_boundary() -> void:
_reset_runtime(60_003)
var path := TEST_PREFIX + "_queued"
SaveService.delete_save(path)
var queued = _runtime.queue_manual_checkpoint(path, {&"display_name": "Queued Runtime"})
_expect(queued[&"ok"] and _runtime.get_pending_checkpoint_count() == 1, "manual checkpoint is queued for the next fixed tick boundary")
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(path)), "queued checkpoint does not write before its boundary")
_clock.advance_one_minute()
var loaded := SaveService.load_snapshot(path)
_expect(loaded[&"ok"] and _runtime.get_pending_checkpoint_count() == 0, "queued checkpoint writes exactly once at PUBLISH_SNAPSHOT")
_expect(int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == 361, "queued package captures the completed minute boundary")
_expect(_has_domain_event(&"SAVE_CHECKPOINT_SUCCEEDED"), "successful manual checkpoint publishes a DomainEvent")
SaveService.delete_save(path)
func _test_daily_checkpoint_dedup_generations_and_failure_preservation() -> void:
_reset_runtime(60_004, 1, 5, 59)
var path := TEST_PREFIX + "_daily"
SaveService.delete_save(path)
var configured = _runtime.configure_save_policy({
&"enabled": true,
&"daily_checkpoint_enabled": true,
&"autosave_path": path,
&"autosave_generations": 3,
})
_expect(configured[&"ok"], "daily Runtime save policy accepts a safe explicit slot")
_clock.advance_one_minute()
_expect(_runtime.get_last_daily_checkpoint_day() == 1, "day 1 checkpoint runs at exactly 06:00")
var generation_zero := SaveService.load_snapshot(SaveService.get_autosave_path(path, 0))
_expect(generation_zero[&"ok"] and int(generation_zero[&"snapshot"][&"clock"][&"absolute_minute"]) == 360, "first daily checkpoint becomes autosave generation zero")
_runtime.call("_process_save_checkpoints", 360)
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(SaveService.get_autosave_path(path, 1))), "same-day 06:00 callback is deduplicated")
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
var expected_minutes := [3240, 1800, 360]
var all_generations_match := true
for generation in range(3):
var loaded := SaveService.load_snapshot(SaveService.get_autosave_path(path, generation))
all_generations_match = all_generations_match \
and loaded[&"ok"] \
and int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == expected_minutes[generation]
_expect(all_generations_match, "daily checkpoints rotate three deterministic newest-first generations")
var newest_world := ProjectSettings.globalize_path(
SaveService.get_autosave_path(path, 0)
).path_join(SaveService.WORLD_FILE)
var newest_before := FileAccess.get_md5(newest_world)
var pending_path := ProjectSettings.globalize_path(path + ".autosave.pending")
DirAccess.make_dir_recursive_absolute(pending_path)
var sentinel := FileAccess.open(pending_path.path_join("not_a_package.txt"), FileAccess.WRITE)
sentinel.store_string("block promotion")
sentinel = null
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
_expect(
not _runtime.latest_save_operation_result[&"ok"]
and _runtime.get_last_daily_checkpoint_day() == 3,
"failed day 4 checkpoint remains eligible for retry instead of being marked complete"
)
_expect(FileAccess.get_md5(newest_world) == newest_before, "failed autosave leaves the previous newest generation byte-identical")
_expect(_has_domain_event(&"AUTOSAVE_CHECKPOINT_FAILED"), "autosave failure publishes a structured DomainEvent")
DirAccess.remove_absolute(pending_path.path_join("not_a_package.txt"))
DirAccess.remove_absolute(pending_path)
SaveService.delete_save(path)
func _test_exact_round_trip_recovery_pause_and_performance() -> void:
_reset_runtime(60_005, 3, 7, 15)
var path := TEST_PREFIX + "_round_trip"
SaveService.delete_save(path)
_clock.set_speed(4)
var expected_snapshot = _runtime.create_snapshot()
var expected_digest := SimulationDigest.compute(expected_snapshot)
var first_save = _runtime.save_to_path(path, {&"display_name": "Runtime Exact"})
_expect(first_save[&"ok"] and not _clock.paused, "save pauses I/O temporarily and restores the running clock")
_state.add_resource(&"faith", 77)
_clock.advance_minutes(5)
var loaded = _runtime.load_from_path(path)
_expect(loaded[&"ok"] and _runtime.latest_digest == expected_digest, "ordinary Runtime load restores the exact persisted snapshot")
_expect(_clock.absolute_minute == int(expected_snapshot[&"clock"][&"absolute_minute"]) and not _clock.paused, "load preserves the saved clock minute and pause state")
var minute_before_process = _clock.absolute_minute
_expect(_clock.process_real_delta(10.0) == 0 and _clock.absolute_minute == minute_before_process, "first process frame after load never advances an extra minute")
var second_save = _runtime.save_to_path(path, {&"display_name": "Runtime New Formal"})
_expect(second_save[&"ok"], "second formal Runtime save creates a verified prior-generation backup")
_corrupt_world(path)
_state.add_resource(&"faith", 5)
var inventory = _runtime.inspect_recovery_candidates(path)
_expect(inventory[&"ok"] and inventory[&"selected"][&"id"] == &"backup", "explicit candidate inventory identifies the verified backup")
var recovered = _runtime.load_with_recovery(path)
_expect(recovered[&"ok"] and recovered[&"selected"][&"id"] == &"backup", "Runtime recovery skips a corrupt formal package")
_expect(_runtime.latest_digest == expected_digest, "recovery loads the exact prior formal snapshot without promotion")
_expect(recovered[&"recovery_report"][&"restore_committed"] and _has_domain_event(&"SAVE_RECOVERY_LOADED"), "successful recovery returns a structured report and publishes a DomainEvent")
var explicit = _runtime.load_recovery_candidate(path, &"backup")
_expect(explicit[&"ok"] and explicit[&"read_only"], "caller can explicitly load one named recovery candidate")
_expect(int(_clock.performance_budget.get_report(&"save")[&"lifetime_sample_count"]) >= 2, "Runtime records actual save operation timing")
_expect(int(_clock.performance_budget.get_report(&"load")[&"lifetime_sample_count"]) >= 3, "Runtime records load, inventory and recovery timing")
SaveService.delete_save(path)
func _test_domain_invalid_formal_falls_back_to_backup() -> void:
_reset_runtime(60_009, 2, 8, 0)
var path := TEST_PREFIX + "_domain_backup"
SaveService.delete_save(path)
var backup_snapshot: Dictionary = _runtime.create_snapshot()
var backup_digest := SimulationDigest.compute(backup_snapshot)
_expect(
SaveService.save_snapshot(path, backup_snapshot)[&"ok"],
"domain fallback fixture writes a valid prior formal package"
)
var invalid_formal: Dictionary = backup_snapshot.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 1
_expect(
SaveService.save_snapshot(path, invalid_formal)[&"ok"],
"checksum-valid but ledger-invalid formal package is preserved as a package"
)
_state.add_resource(&"faith", 17)
var strict_guard := _authoritative_live_guard()
var strict_load: Dictionary = _runtime.load_from_path(path)
_expect(
not strict_load[&"ok"] and strict_load[&"code"] == &"RESTORE_FAILED",
"ordinary load remains strict and does not fall through to a valid backup"
)
_expect(
_authoritative_live_guard() == strict_guard,
"strict ordinary-load rejection preserves authoritative gameplay state"
)
var explicit_formal: Dictionary = _runtime.load_recovery_candidate(path, &"formal")
_expect(
not explicit_formal[&"ok"] and explicit_formal[&"code"] == &"RESTORE_FAILED",
"explicit candidate load remains strict to the requested domain-invalid package"
)
var recovered: Dictionary = _runtime.load_with_recovery(path)
_expect(
recovered[&"ok"] and recovered[&"selected"][&"id"] == &"backup",
"Runtime rejects a domain-invalid formal package and automatically selects its valid backup"
)
_expect(
recovered[&"candidates"][0][&"package_status"] == &"VALID"
and recovered[&"candidates"][0][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][0][&"domain_rejection_code"] == &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED"
and recovered[&"candidates"][0][&"failed_attempt_live_state_preserved"],
"formal candidate report separates successful package verification from domain rejection"
)
_expect(
recovered[&"candidates"][1][&"domain_status"] == &"VALID"
and recovered[&"candidates"][1][&"selected"],
"backup candidate report identifies the first domain-valid committed candidate"
)
_expect(
_runtime.latest_digest == backup_digest,
"domain fallback commits the exact backup snapshot"
)
_expect(
recovered[&"package_candidate_report"][&"selected"][&"id"] == &"formal"
and recovered[&"package_recovery_report"][&"source"][&"candidate_id"] == &"backup"
and recovered[&"restore_safety_report"][&"recovery_mode"],
"result preserves package-level selection evidence and final Runtime restore-safety evidence"
)
SaveService.delete_save(path)
func _test_multiple_domain_invalid_candidates_fall_back_to_autosave() -> void:
_reset_runtime(60_010, 2, 9, 0)
var path := TEST_PREFIX + "_domain_autosave"
SaveService.delete_save(path)
var valid_autosave: Dictionary = _runtime.create_snapshot()
var valid_digest := SimulationDigest.compute(valid_autosave)
var invalid_backup: Dictionary = valid_autosave.duplicate(true)
invalid_backup[&"game_state"][&"resources"][&"gold"] = int(
invalid_backup[&"game_state"][&"resources"][&"gold"]
) + 1
var invalid_formal: Dictionary = valid_autosave.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 2
_expect(
SaveService.save_snapshot(path, invalid_backup)[&"ok"]
and SaveService.save_snapshot(path, invalid_formal)[&"ok"],
"multiple domain-invalid formal generations remain checksum-valid recovery candidates"
)
_expect(
SaveService.save_autosave(path, valid_autosave, {}, 3)[&"ok"],
"valid autosave is written behind both invalid formal generations"
)
var package_guard := {
&"formal": _package_fingerprint(path),
&"backup": _package_fingerprint(path + ".bak"),
&"autosave": _package_fingerprint(SaveService.get_autosave_path(path, 0)),
}
_state.add_resource(&"faith", 23)
var recovered: Dictionary = _runtime.load_with_recovery(path)
_expect(
recovered[&"ok"] and recovered[&"selected"][&"id"] == &"autosave_0",
"Runtime walks past two domain-invalid candidates to the newest valid autosave"
)
_expect(
recovered[&"candidates"][0][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][1][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][0][&"failed_attempt_live_state_preserved"]
and recovered[&"candidates"][1][&"failed_attempt_live_state_preserved"]
and recovered[&"candidates"][4][&"domain_status"] == &"VALID",
"candidate attempts retain both ordered domain rejections and the autosave success"
)
_expect(
recovered[&"recovery_report"][&"domain_rejections"].size() == 2
and recovered[&"recovery_report"][&"selected_candidate_id"] == &"autosave_0",
"combined recovery report records every rejected domain candidate and final selection"
)
_expect(_runtime.latest_digest == valid_digest, "autosave fallback restores the exact valid snapshot")
_expect(
package_guard == {
&"formal": _package_fingerprint(path),
&"backup": _package_fingerprint(path + ".bak"),
&"autosave": _package_fingerprint(SaveService.get_autosave_path(path, 0)),
},
"domain fallback remains read-only and never promotes or rewrites any package"
)
SaveService.delete_save(path)
func _test_all_domain_invalid_candidates_preserve_complete_live_state() -> void:
_reset_runtime(60_011, 2, 10, 0)
var path := TEST_PREFIX + "_all_domain_invalid"
SaveService.delete_save(path)
var base: Dictionary = _runtime.create_snapshot()
var invalid_backup: Dictionary = base.duplicate(true)
invalid_backup[&"game_state"][&"resources"][&"gold"] = int(
invalid_backup[&"game_state"][&"resources"][&"gold"]
) + 1
var invalid_formal: Dictionary = base.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 2
var invalid_autosave: Dictionary = base.duplicate(true)
invalid_autosave[&"game_state"][&"resources"][&"gold"] = int(
invalid_autosave[&"game_state"][&"resources"][&"gold"]
) + 3
_expect(
SaveService.save_snapshot(path, invalid_backup)[&"ok"]
and SaveService.save_snapshot(path, invalid_formal)[&"ok"]
and SaveService.save_autosave(path, invalid_autosave, {}, 3)[&"ok"],
"all-invalid fixture exposes three checksum-valid but domain-invalid candidates"
)
_state.add_resource(&"faith", 31)
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.DISABLE,
EventDefinition.ContentRating.GENERAL,
[&"local_atomic_filter"]
)
_clock.set_speed(4)
_clock.set_paused(true)
_expect(
_runtime.queue_manual_checkpoint(TEST_PREFIX + "_never_written")[&"ok"],
"all-invalid atomicity fixture includes a pending Runtime checkpoint"
)
var live_guard := _complete_live_guard()
var rejected: Dictionary = _runtime.load_with_recovery(path)
_expect(
not rejected[&"ok"]
and rejected[&"code"] == &"RESTORE_FAILED"
and rejected[&"recovery_report"][&"rejection_reason"] == &"NO_DOMAIN_VALID_RECOVERY_CANDIDATE",
"all domain-invalid candidates return one explicit terminal recovery failure"
)
_expect(
rejected[&"recovery_report"][&"domain_rejections"].size() == 3
and rejected[&"selected"].is_empty(),
"terminal failure reports all three domain rejections and no selected candidate"
)
_expect(
_complete_live_guard() == live_guard and rejected[&"live_state_preserved"],
"all-candidate failure preserves the complete live snapshot, filter, pause, policy, and pending checkpoint"
)
_expect(
rejected[&"restore_safety_report"] == rejected[&"recovery_report"]
and rejected[&"package_candidate_report"].has(&"candidates"),
"terminal failure preserves both package inventory and Runtime restore-safety reports"
)
SaveService.delete_save(path)
func _test_content_filter_cannot_be_weakened() -> void:
_reset_runtime(60_006)
var weak_path := TEST_PREFIX + "_weak_filter"
var exact_path := TEST_PREFIX + "_exact_filter"
SaveService.delete_save(weak_path)
SaveService.delete_save(exact_path)
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.NONE,
EventDefinition.ContentRating.RESTRICTED,
[&"saved_tag"]
)
_expect(_runtime.save_to_path(weak_path)[&"ok"], "weaker saved content profile fixture is valid")
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.DISABLE,
EventDefinition.ContentRating.GENERAL,
[&"local_tag"]
)
var loaded = _runtime.load_from_path(weak_path)
var profile = _runtime.event_service.get_content_filter_profile()
_expect(
loaded[&"ok"]
and profile[&"filter_mode"] == EventDefinition.FilterBehavior.DISABLE
and profile[&"maximum_content_rating"] == EventDefinition.ContentRating.GENERAL,
"loading a weaker save cannot reduce behavior severity or raise the rating ceiling"
)
_expect(
profile[&"disabled_content_tags"] == [&"local_tag", &"saved_tag"],
"load safety unions local and saved disabled content tags"
)
var filter_report: Dictionary = loaded[&"restore_safety_report"][&"content_filter"]
_expect(filter_report[&"saved_profile_was_weaker"] and filter_report[&"strict_profile_preserved"], "load result explains that the stricter local filter won")
var exact_snapshot = _runtime.create_snapshot()
var exact_digest := SimulationDigest.compute(exact_snapshot)
_expect(_runtime.save_to_path(exact_path)[&"ok"], "effective strict profile saves normally")
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.NONE,
EventDefinition.ContentRating.RESTRICTED,
[]
)
var exact_loaded = _runtime.load_from_path(exact_path)
_expect(exact_loaded[&"ok"] and _runtime.latest_digest == exact_digest, "ordinary content-profile round trip remains snapshot-exact")
SaveService.delete_save(weak_path)
SaveService.delete_save(exact_path)
func _test_whitelisted_recovery_repairs() -> void:
_reset_runtime(60_007)
var path := TEST_PREFIX + "_allowed_repairs"
SaveService.delete_save(path)
var snapshot = _runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
var staff := StaffRuntime.new().configure(&"repair_staff", &"worker", {
&"state": StaffRuntime.State.MOVING_TO_TASK,
&"current_task_id": &"repair_task",
})
var task := StaffTask.new().configure(&"repair_task", &"cleanup", 50, _clock.absolute_minute, {
&"state": StaffTask.State.RESERVED,
&"assigned_staff_id": &"missing_staff",
})
runtime_data[&"staff"][&"staff"].append(staff.create_snapshot())
runtime_data[&"staff"][&"tasks"].append(task.create_snapshot())
_append_staff_path_crosslink(runtime_data, &"repair_staff", Vector3i.ZERO)
var terminal := EventInstance.new().configure(
&"terminal_foreground",
&"relationship.threshold",
&"relationship.threshold",
[&"player"],
100,
_clock.absolute_minute,
-1,
true,
&""
)
terminal.state = EventInstance.State.COMPLETED
runtime_data[&"events"][&"instances"].append(terminal.create_snapshot())
runtime_data[&"events"][&"foreground_instance_id"] = "terminal_foreground"
runtime_data[&"events"][&"actor_locks"] = {"player": "terminal_foreground"}
_expect(SaveService.save_snapshot(path, snapshot)[&"ok"], "checksum-valid orphan repair fixture writes as a package")
var recovered = _runtime.load_with_recovery(path)
_expect(recovered[&"ok"], "recovery accepts only the whitelisted Task and Event Lock damage")
var repaired_task = _runtime.staff_service.get_task(&"repair_task")
var repaired_staff = _runtime.staff_service.get_staff(&"repair_staff")
_expect(
repaired_task != null and repaired_task.state == StaffTask.State.OPEN and repaired_task.assigned_staff_id.is_empty()
and repaired_staff != null and repaired_staff.current_task_id.is_empty(),
"StaffService audit reopens the orphan Task and releases its stale Staff pointer"
)
_expect(_runtime.event_service.get_actor_locks().is_empty() and _runtime.event_service.get_foreground_instance() == null, "terminal foreground recovery releases only its stale Event Locks")
var report: Dictionary = recovered[&"recovery_report"]
_expect(
report[&"repairs"][&"orphan_tasks"][&"reopened_task_ids"] == [&"repair_task"]
and report[&"repairs"][&"orphan_tasks"][&"released_staff_ids"] == [&"repair_staff"],
"repair report names every repaired Task and Staff exactly"
)
_expect(
report[&"repairs"][&"orphan_event_locks"][&"released_actor_ids"] == [&"player"]
and report[&"repairs"][&"orphan_event_locks"][&"released_foreground_instance_id"] == &"terminal_foreground",
"repair report names the released Actor Lock and terminal foreground exactly"
)
_expect(report[&"automatic_repairs_applied"] and not report[&"forbidden_repairs_applied"], "recovery report confirms its authoritative whitelist boundary")
SaveService.delete_save(path)
func _test_non_whitelisted_corruption_is_rejected_atomically() -> void:
_reset_runtime(60_008)
var built = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"reception", &"origin": Vector3i(-6, 0, 0), &"rotation": 0},
&"player", &"", 100, -1, &"s6.invalid.fixture.facility"
)
_expect(built.is_success(), "invalid recovery fixtures start from a valid constructed facility")
_clock.advance_minutes(120)
var base = _runtime.create_snapshot()
var missing_facility = base.duplicate(true)
missing_facility[&"systems"][&"runtime"][&"facilities"][&"instances"] = []
_test_rejected_candidate(
TEST_PREFIX + "_reject_missing_facility",
missing_facility,
"recovery never deletes or invents a missing operational facility"
)
var ledger_mutation = base.duplicate(true)
ledger_mutation[&"game_state"][&"resources"][&"gold"] = int(
ledger_mutation[&"game_state"][&"resources"][&"gold"]
) + 1
_test_rejected_candidate(
TEST_PREFIX + "_reject_ledger",
ledger_mutation,
"recovery never synthesizes Gold or rewrites the ledger projection"
)
var route_mismatch = base.duplicate(true)
var invalid_relationship := RelationshipState.new().configure(
&"invalid_route",
&"actor_a",
&"actor_b",
{
&"route_lock": RelationshipState.RouteLock.EXCLUSIVE_LOCKED,
&"route_id": &"",
}
)
route_mismatch[&"systems"][&"runtime"][&"relationships"][&"relationships"].append(
invalid_relationship.create_snapshot()
)
_test_rejected_candidate(
TEST_PREFIX + "_reject_route",
route_mismatch,
"recovery never guesses an exclusive Relationship route lock"
)
var unreleasable = base.duplicate(true)
var bad_staff := StaffRuntime.new().configure(&"blocked_staff", &"worker", {
&"state": StaffRuntime.State.MOVING_TO_TASK,
&"current_task_id": &"blocked_task",
})
var bad_task := StaffTask.new().configure(&"blocked_task", &"cleanup", 50, _clock.absolute_minute, {
&"state": StaffTask.State.RESERVED,
&"assigned_staff_id": &"missing_staff",
&"reservation_ids": [&"missing_reservation"],
})
unreleasable[&"systems"][&"runtime"][&"staff"][&"staff"].append(bad_staff.create_snapshot())
unreleasable[&"systems"][&"runtime"][&"staff"][&"tasks"].append(bad_task.create_snapshot())
_append_staff_path_crosslink(
unreleasable[&"systems"][&"runtime"],
&"blocked_staff",
Vector3i.ZERO
)
_test_rejected_candidate(
TEST_PREFIX + "_reject_reservation",
unreleasable,
"recovery rejects an orphan Task whose inventory Reservation cannot be released"
)
func _test_rejected_candidate(path: String, snapshot: Dictionary, label: String) -> void:
SaveService.delete_save(path)
_expect(SaveService.save_snapshot(path, snapshot)[&"ok"], "%s fixture is checksum-valid" % label)
var guard := _authoritative_live_guard()
var result = _runtime.load_with_recovery(path)
_expect(not result[&"ok"] and result[&"code"] == &"RESTORE_FAILED", label)
_expect(_authoritative_live_guard() == guard, "%s and leaves authoritative live state unchanged" % label)
_expect(
result[&"recovery_report"][&"restore_committed"] == false
and not result[&"recovery_report"][&"forbidden_repairs_applied"],
"%s reports atomic rejection without forbidden repair" % label
)
SaveService.delete_save(path)
func _append_staff_path_crosslink(
runtime_data: Dictionary,
staff_id: StringName,
cell: Vector3i
) -> void:
var actor_id := StringName("staff:%s" % staff_id)
runtime_data[&"path"][&"actor_cells"].append({
&"actor_id": actor_id,
&"cell": cell,
})
runtime_data[&"path"][&"actor_progress"].append({
&"actor_id": actor_id,
&"last_cell": cell,
&"last_progress_at": _clock.absolute_minute,
&"last_observed_at": _clock.absolute_minute,
&"stuck_reported_at": -1,
})
runtime_data[&"movement_credits"].append({
&"actor_id": String(actor_id),
&"credit": 0,
&"updated_at": _clock.absolute_minute,
})
for traffic: Dictionary in runtime_data[&"path"][&"cell_traffic"]:
if traffic[&"cell"] == cell:
traffic[&"current_density"] = int(traffic[&"current_density"]) + 1
break
func _authoritative_live_guard() -> Dictionary:
return {
&"clock": _clock.clock_snapshot(),
&"game_state": _state.create_snapshot(),
&"economy": _runtime.economy_service.create_snapshot(),
&"inventory": _runtime.inventory_service.create_snapshot(),
&"facilities": _runtime.facility_service.create_snapshot(),
&"construction": _runtime.construction_service.create_snapshot(),
&"staff": _runtime.staff_service.create_snapshot(),
&"relationships": _runtime.relationship_service.create_snapshot(),
&"events": _runtime.event_service.create_snapshot(),
}
func _complete_live_guard() -> Dictionary:
return {
&"snapshot": _runtime.create_snapshot(),
&"clock_paused": _clock.paused,
&"save_policy": _runtime.get_save_policy(),
&"pending_checkpoint_count": _runtime.get_pending_checkpoint_count(),
&"last_daily_checkpoint_day": _runtime.get_last_daily_checkpoint_day(),
&"content_filter": _runtime.event_service.get_content_filter_profile(),
}
func _reset_runtime(seed: int, day := 1, hour := 6, minute := 0) -> void:
_cleanup_runtime()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(day, hour, minute)
_state.reset()
var report = _runtime.initialize(_clock, _state, seed)
_expect(report[&"valid"], "Runtime fixture initializes deterministic definitions")
func _cleanup_runtime() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _cleanup_test_saves() -> void:
for suffix: String in [
"_disabled", "_queued", "_daily", "_round_trip", "_weak_filter",
"_exact_filter", "_allowed_repairs", "_reject_missing_facility",
"_reject_ledger", "_reject_route", "_reject_reservation",
"_domain_backup", "_domain_autosave", "_all_domain_invalid",
"_never_written",
]:
SaveService.delete_save(TEST_PREFIX + suffix)
func _has_domain_event(event_type: StringName) -> bool:
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == event_type:
return true
return false
func _package_fingerprint(save_path: String) -> Dictionary:
var absolute_path := ProjectSettings.globalize_path(save_path)
var result := {&"directory_exists": DirAccess.dir_exists_absolute(absolute_path)}
for file_name: String in [SaveService.MANIFEST_FILE, SaveService.WORLD_FILE, SaveService.THUMBNAIL_FILE]:
var file_path := absolute_path.path_join(file_name)
result[file_name] = FileAccess.get_md5(file_path) if FileAccess.file_exists(file_path) else ""
return result
func _corrupt_world(save_path: String) -> void:
var world_path := ProjectSettings.globalize_path(save_path).path_join(SaveService.WORLD_FILE)
var bytes := FileAccess.get_file_as_bytes(world_path)
if bytes.is_empty():
return
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0x5a
var file := FileAccess.open(world_path, FileAccess.WRITE)
file.store_buffer(bytes)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bamjaic1m17jn
+584
View File
@@ -0,0 +1,584 @@
extends SceneTree
const SaveServiceType = preload("res://scripts/core/save_service.gd")
const MigrationRegistryType = preload("res://scripts/core/save_migration_registry.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S6: save rotation, recovery and migration")
_test_path_safety()
_test_package_internal_symlink_load_rejection()
_test_package_contract_identity_and_timing()
_test_autosave_rotation_and_formal_atomicity()
_test_interrupted_formal_save_recovery()
_test_interrupted_autosave_rotation_recovery()
_test_autosave_retention_shrink_prunes_verified_generations()
_test_recovery_priority_checksum_and_read_only_boundary()
_test_migration_registry_continuity_and_purity()
_test_current_format_snapshot_validation()
_test_save_package_migration_and_pre_migration_backup()
_test_migration_failure_preserves_source()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_path_safety() -> void:
var snapshot := _snapshot(10, &"safe")
var traversal := SaveServiceType.save_snapshot("user://../s6_escape", snapshot)
_expect(not traversal[&"ok"] and traversal[&"code"] == &"UNSAFE_PATH", "parent traversal is rejected before any write")
var relative := SaveServiceType.save_snapshot("relative/s6_save", snapshot)
_expect(not relative[&"ok"] and relative[&"code"] == &"INVALID_PATH", "ambiguous relative save paths are rejected")
var external := SaveServiceType.save_snapshot("/tmp/the_tree_s6_escape", snapshot)
_expect(not external[&"ok"] and external[&"code"] == &"UNSAFE_PATH", "absolute paths outside the Godot user directory are rejected")
_expect(not SaveServiceType.delete_save("user://"), "delete refuses the broad user data root")
_expect(not SaveServiceType.delete_save("/"), "delete refuses the filesystem root")
var non_package_path := ProjectSettings.globalize_path("user://saves/s6_non_package_parent")
DirAccess.make_dir_recursive_absolute(non_package_path)
var keep_file := FileAccess.open(non_package_path.path_join("keep.txt"), FileAccess.WRITE)
keep_file.store_string("must remain")
keep_file = null
_expect(not SaveServiceType.delete_save("user://saves/s6_non_package_parent"), "delete refuses an ordinary parent directory that is not a save package")
_expect(FileAccess.file_exists(non_package_path.path_join("keep.txt")), "refused parent deletion preserves unrelated user data")
_expect(not SaveServiceType.save_snapshot("user://saves/s6_non_package_parent", snapshot)[&"ok"], "save refuses to rotate an ordinary directory into a package backup")
DirAccess.remove_absolute(non_package_path.path_join("keep.txt"))
DirAccess.remove_absolute(non_package_path)
var external_path := "/tmp/the_tree_s6_symlink_%d" % OS.get_process_id()
var link_path := ProjectSettings.globalize_path("user://saves/s6_symlink_parent")
DirAccess.make_dir_recursive_absolute(external_path)
var sentinel := FileAccess.open(external_path.path_join("outside.txt"), FileAccess.WRITE)
sentinel.store_string("outside")
sentinel = null
DirAccess.remove_absolute(link_path)
var link_exit := OS.execute("ln", PackedStringArray(["-s", external_path, link_path]))
_expect(link_exit == 0, "symlink escape fixture is available")
if link_exit == 0:
var linked_save := SaveServiceType.save_snapshot("user://saves/s6_symlink_parent/escaped", snapshot)
_expect(not linked_save[&"ok"] and linked_save[&"code"] == &"UNSAFE_PATH", "write refuses a save directory reached through a symbolic link")
_expect(not SaveServiceType.delete_save("user://saves/s6_symlink_parent/escaped"), "delete refuses a directory reached through a symbolic link")
_expect(FileAccess.get_file_as_string(external_path.path_join("outside.txt")) == "outside", "symlink rejection never touches data outside user storage")
DirAccess.remove_absolute(link_path)
var recursive_save_path := "user://saves/s6_recursive_symlink"
SaveServiceType.delete_save(recursive_save_path)
SaveServiceType.save_snapshot(recursive_save_path, snapshot)
var embedded_link := ProjectSettings.globalize_path(recursive_save_path).path_join("external_link")
var embedded_link_exit := OS.execute("ln", PackedStringArray(["-s", external_path, embedded_link]))
_expect(embedded_link_exit == 0, "recursive symlink fixture is available")
if embedded_link_exit == 0:
var recursive_write := SaveServiceType.save_snapshot(recursive_save_path, snapshot)
_expect(not recursive_write[&"ok"] and recursive_write[&"code"] == &"UNSAFE_PATH", "overwrite refuses a symbolic link nested anywhere inside the package")
_expect(not SaveServiceType.delete_save(recursive_save_path), "recursive delete refuses a package containing a symbolic link")
_expect(FileAccess.file_exists(external_path.path_join("outside.txt")), "recursive symlink guard preserves its external target")
DirAccess.remove_absolute(embedded_link)
SaveServiceType.delete_save(recursive_save_path)
DirAccess.remove_absolute(external_path.path_join("outside.txt"))
DirAccess.remove_absolute(external_path)
func _test_package_internal_symlink_load_rejection() -> void:
var save_path := "user://saves/s6_internal_world_link"
var external_package := "user://saves/s6_external_valid_world"
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(external_package)
var shared_snapshot := _snapshot(44, &"external_valid")
_expect(SaveServiceType.save_snapshot(save_path, shared_snapshot)[&"ok"], "internal-link fixture target package is valid before substitution")
_expect(SaveServiceType.save_snapshot(external_package, shared_snapshot)[&"ok"], "external world fixture is a separately verified package")
var target_world := ProjectSettings.globalize_path(save_path).path_join(SaveServiceType.WORLD_FILE)
var external_world := ProjectSettings.globalize_path(external_package).path_join(SaveServiceType.WORLD_FILE)
DirAccess.remove_absolute(target_world)
var link_exit := OS.execute("ln", PackedStringArray(["-s", external_world, target_world]))
_expect(link_exit == 0, "known package-file symlink fixture is available")
if link_exit == 0:
var direct := SaveServiceType.load_snapshot(save_path)
_expect(not direct[&"ok"] and direct[&"code"] == &"UNSAFE_PATH", "direct load rejects a world.bin symlink even when its external bytes are valid")
var explicit := SaveServiceType.load_recovery_candidate(save_path, &"formal")
_expect(not explicit[&"ok"] and explicit[&"code"] == &"UNSAFE_PATH", "explicit recovery-candidate load rejects an internal package symlink")
var inspected := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(not inspected[&"ok"] and inspected[&"code"] == &"UNSAFE_PATH", "recovery inspection rejects an internal package symlink before opening it")
_expect(FileAccess.file_exists(external_world), "load rejection leaves the external valid world untouched")
DirAccess.remove_absolute(target_world)
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(external_package)
func _test_package_contract_identity_and_timing() -> void:
var save_path := "user://saves/s6_contract"
SaveServiceType.delete_save(save_path)
SaveServiceType.clear_performance_reports()
var first := SaveServiceType.save_snapshot(save_path, _snapshot(60, &"first"))
_expect(first[&"ok"] and int(first[&"duration_us"]) >= 0, "save result reports actual elapsed time")
var package_path := ProjectSettings.globalize_path(save_path)
var manifest: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE)))
_expect(str(manifest["save_id"]) == "s6_contract" and str(manifest["display_name"]) == "s6_contract", "default identity comes from the logical slot rather than its temporary directory")
var contract_keys := ["format_version", "game_version", "save_id", "display_name", "created_at_real", "updated_at_real", "absolute_minute", "day", "seed", "checksum_world", "content_pack_ids", "enabled_filter_profile"]
var all_contract_keys := true
for key: String in contract_keys:
all_contract_keys = all_contract_keys and manifest.has(key)
_expect(all_contract_keys, "manifest generates every required package-contract field")
var thumbnail_bytes := FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.THUMBNAIL_FILE))
var thumbnail := Image.new()
_expect(not thumbnail_bytes.is_empty() and thumbnail.load_png_from_buffer(thumbnail_bytes) == OK, "save package always contains a valid thumbnail PNG")
var created_at := str(manifest["created_at_real"])
var second := SaveServiceType.save_snapshot(save_path, _snapshot(61, &"second"), {
&"display_name": "Chosen Name",
&"content_pack_ids": ["base"],
&"custom_note": "preserved",
})
_expect(second[&"ok"], "package overwrite accepts supplied metadata")
var overwritten := SaveServiceType.load_snapshot(save_path)
var second_manifest: Dictionary = overwritten[&"manifest"]
_expect(str(second_manifest["save_id"]) == "s6_contract" and str(second_manifest["display_name"]) == "Chosen Name", "overwrite preserves prior ID while honoring an explicit display name")
_expect(str(second_manifest["created_at_real"]) == created_at and second_manifest["content_pack_ids"] == ["base"] and str(second_manifest["custom_note"]) == "preserved", "overwrite preserves creation identity and custom contract metadata")
_expect(FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.THUMBNAIL_FILE)) == thumbnail_bytes, "overwrite preserves the previous thumbnail when none is supplied")
var third := SaveServiceType.save_snapshot(save_path, _snapshot(62, &"third"))
_expect(third[&"ok"] and str(third[&"manifest"]["display_name"]) == "Chosen Name", "later overwrite retains prior display identity by default")
_expect(SaveServiceType.get_performance_report(&"save")[&"lifetime_sample_count"] == 3, "every completed save attempt feeds the save performance budget")
_expect(SaveServiceType.get_performance_report(&"load")[&"lifetime_sample_count"] == 1 and int(overwritten[&"duration_us"]) >= 0, "load operations feed the load performance budget and report elapsed time")
SaveServiceType.delete_save(save_path)
func _test_autosave_rotation_and_formal_atomicity() -> void:
var save_path := "user://saves/s6_rotation"
SaveServiceType.delete_save(save_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(10, &"formal_old"))[&"ok"], "initial formal save succeeds")
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(20, &"formal_current"))[&"ok"], "formal save keeps a verified backup")
for minute in [101, 102, 103, 104]:
var result := SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("auto_%d" % minute)))
_expect(result[&"ok"] and result[&"formal_save_preserved"], "autosave %d rotates without replacing the formal save" % minute)
var expected_minutes := [104, 103, 102]
for generation in range(3):
var loaded := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, generation))
_expect(loaded[&"ok"], "autosave generation %d remains checksum-valid" % generation)
if loaded[&"ok"]:
_expect(int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == expected_minutes[generation], "generation %d has deterministic newest-first order" % generation)
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(save_path + ".autosave.3")), "rotation retains exactly three generations")
var formal_world_path := ProjectSettings.globalize_path(save_path).path_join(SaveServiceType.WORLD_FILE)
var formal_before := FileAccess.get_file_as_bytes(formal_world_path)
var invalid := _snapshot(999, &"invalid")
invalid[&"schema_version"] = 999
var failed := SaveServiceType.save_autosave(save_path, invalid)
_expect(not failed[&"ok"] and failed[&"code"] == &"INVALID_SNAPSHOT", "invalid autosave candidate fails before rotation")
_expect(failed[&"formal_save_preserved"] and FileAccess.get_file_as_bytes(formal_world_path) == formal_before, "failed autosave leaves the formal package byte-identical")
var formal_loaded := SaveServiceType.load_snapshot(save_path)
_expect(formal_loaded[&"ok"] and _marker(formal_loaded[&"snapshot"]) == &"formal_current", "formal save remains loadable after autosave failure")
var newest := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 0))
_expect(newest[&"ok"] and _marker(newest[&"snapshot"]) == &"auto_104", "failed candidate does not disturb the previous newest autosave")
SaveServiceType.delete_save(save_path)
func _test_interrupted_formal_save_recovery() -> void:
var save_path := "user://saves/s6_formal_transaction"
var candidate_path := "user://saves/s6_formal_transaction_candidate"
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(candidate_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(10, &"durable_old"))[&"ok"], "formal crash fixture writes its durable predecessor")
_expect(SaveServiceType.save_snapshot(candidate_path, _snapshot(20, &"durable_new"))[&"ok"], "formal crash fixture writes a verified replacement candidate")
var target_absolute := ProjectSettings.globalize_path(save_path)
var candidate_absolute := ProjectSettings.globalize_path(candidate_path)
_copy_fixture_tree(candidate_absolute, target_absolute + ".tmp")
DirAccess.rename_absolute(target_absolute, target_absolute + ".bak")
_write_transaction_marker(target_absolute)
var promoted := SaveServiceType.load_snapshot(save_path)
_expect(promoted[&"ok"] and promoted[&"interrupted_save_reconciled"], "load reconciles a crash between formal-to-backup and candidate promotion")
_expect(_marker(promoted[&"snapshot"]) == &"durable_new", "reconciliation promotes only the verified temporary package")
var preserved_backup := SaveServiceType.load_snapshot(save_path + ".bak")
_expect(preserved_backup[&"ok"] and _marker(preserved_backup[&"snapshot"]) == &"durable_old", "candidate promotion retains the verified prior formal backup")
_expect(not FileAccess.file_exists(target_absolute + SaveServiceType.SAVE_TRANSACTION_SUFFIX), "successful formal reconciliation clears its durable marker")
SaveServiceType.delete_save(save_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(30, &"rollback_old"))[&"ok"], "corrupt-candidate fixture writes its verified backup source")
_copy_fixture_tree(candidate_absolute, target_absolute + ".tmp")
_corrupt_world(save_path + ".tmp")
DirAccess.rename_absolute(target_absolute, target_absolute + ".bak")
_write_transaction_marker(target_absolute)
var rolled_back := SaveServiceType.load_snapshot(save_path)
_expect(rolled_back[&"ok"] and _marker(rolled_back[&"snapshot"]) == &"rollback_old", "corrupt temporary candidate is never promoted and verified backup is restored")
_expect(not DirAccess.dir_exists_absolute(target_absolute + ".tmp"), "verified backup recovery discards only the marker-owned corrupt temporary package")
_expect(not FileAccess.file_exists(target_absolute + SaveServiceType.SAVE_TRANSACTION_SUFFIX), "verified backup recovery clears the completed transaction marker")
SaveServiceType.delete_save(save_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(40, &"backup_only"))[&"ok"], "backup-only crash fixture writes a verified formal source")
DirAccess.rename_absolute(target_absolute, target_absolute + ".bak")
_write_transaction_marker(target_absolute)
var inspected := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(inspected[&"ok"] and inspected[&"reconciliation"][&"formal_save"][&"action"] == &"restored_verified_backup", "candidate inspection restores a verified backup when no temporary candidate exists")
_expect(_marker(inspected[&"snapshot"]) == &"backup_only" and inspected[&"selected"][&"id"] == &"formal", "backup-only reconciliation recreates the formal package before candidate selection")
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(candidate_path)
func _test_interrupted_autosave_rotation_recovery() -> void:
var save_path := "user://saves/s6_rotation_recovery"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(5, &"formal"))
for minute in [10, 20, 30]:
SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("auto_%d" % minute)))
var target_path := ProjectSettings.globalize_path(save_path)
var pending_path := target_path + ".autosave.pending"
_copy_fixture_tree(target_path + SaveServiceType.AUTOSAVE_SUFFIX + "0", pending_path)
for generation in range(3):
var generation_path := target_path + SaveServiceType.AUTOSAVE_SUFFIX + str(generation)
DirAccess.rename_absolute(generation_path, generation_path + ".rotate_tmp")
var after_rollback := SaveServiceType.save_autosave(save_path, _snapshot(40, &"auto_40"))
_expect(after_rollback[&"ok"] and after_rollback[&"interrupted_rotation_reconciled"], "next autosave rolls back an interrupted pre-promotion rotation before proceeding")
var expected_after_rollback := [40, 30, 20]
for generation in range(3):
var loaded := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, generation))
_expect(int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == expected_after_rollback[generation], "rollback recovery preserves generation %d order" % generation)
# Simulate a crash after the new generation zero was promoted but before old
# generations were shifted out of their transaction holds.
for generation in range(3):
var generation_path := target_path + SaveServiceType.AUTOSAVE_SUFFIX + str(generation)
DirAccess.rename_absolute(generation_path, generation_path + ".rotate_tmp")
_copy_fixture_tree(target_path, target_path + SaveServiceType.AUTOSAVE_SUFFIX + "0")
var recovery := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(recovery[&"ok"] and recovery[&"reconciliation"][&"autosave"][&"action"] == &"completed", "recovery inspection completes an interrupted post-promotion rotation")
var completed_zero := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 0))
var completed_one := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 1))
var completed_two := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 2))
_expect(_marker(completed_zero[&"snapshot"]) == &"formal", "post-promotion recovery keeps the already-promoted newest package")
_expect(_marker(completed_one[&"snapshot"]) == &"auto_40" and _marker(completed_two[&"snapshot"]) == &"auto_30", "post-promotion recovery completes the remaining generation shifts exactly once")
_expect(not DirAccess.dir_exists_absolute(target_path + SaveServiceType.AUTOSAVE_SUFFIX + "0.rotate_tmp"), "rotation recovery removes durable hold markers after verification")
SaveServiceType.delete_save(save_path)
func _test_autosave_retention_shrink_prunes_verified_generations() -> void:
var save_path := "user://saves/s6_retention_shrink"
SaveServiceType.delete_save(save_path)
for minute in [10, 20, 30, 40, 50]:
_expect(SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("wide_%d" % minute)), {}, 4)[&"ok"], "four-generation autosave fixture accepts minute %d" % minute)
var shrink := SaveServiceType.save_autosave(save_path, _snapshot(60, &"narrow_60"), {}, 2)
_expect(shrink[&"ok"] and shrink[&"pruned_generations"] == [2, 3], "successful 4-to-2 retention change reports every pruned verified generation")
var newest := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 0))
var previous := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 1))
_expect(_marker(newest[&"snapshot"]) == &"narrow_60" and _marker(previous[&"snapshot"]) == &"wide_50", "retention shrink preserves deterministic newest-first generations")
var target_absolute := ProjectSettings.globalize_path(save_path)
_expect(not DirAccess.dir_exists_absolute(target_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "2") and not DirAccess.dir_exists_absolute(target_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "3"), "retention shrink removes all verified generations above the new limit")
SaveServiceType.delete_save(save_path)
var conflict_path := "user://saves/s6_retention_conflict"
SaveServiceType.delete_save(conflict_path)
_expect(SaveServiceType.save_autosave(conflict_path, _snapshot(10, &"conflict_base"), {}, 4)[&"ok"], "retention conflict fixture starts with a valid owned generation")
var conflict_absolute := ProjectSettings.globalize_path(conflict_path)
var foreign_generation := conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "3"
DirAccess.make_dir_recursive_absolute(foreign_generation)
var sentinel := FileAccess.open(foreign_generation.path_join("foreign.txt"), FileAccess.WRITE)
sentinel.store_string("preserve")
sentinel = null
var newest_before := FileAccess.get_file_as_bytes((conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "0").path_join(SaveServiceType.WORLD_FILE))
var foreign_rejected := SaveServiceType.save_autosave(conflict_path, _snapshot(20, &"must_not_commit"), {}, 2)
_expect(not foreign_rejected[&"ok"] and foreign_rejected[&"code"] == &"AUTOSAVE_PRUNE_CONFLICT", "retention shrink refuses a foreign non-package generation")
_expect(FileAccess.get_file_as_string(foreign_generation.path_join("foreign.txt")) == "preserve" and FileAccess.get_file_as_bytes((conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "0").path_join(SaveServiceType.WORLD_FILE)) == newest_before, "foreign prune conflict preserves both foreign data and the prior newest autosave")
DirAccess.remove_absolute(foreign_generation.path_join("foreign.txt"))
DirAccess.remove_absolute(foreign_generation)
var external_path := ProjectSettings.globalize_path("user://saves/s6_retention_external")
DirAccess.make_dir_recursive_absolute(external_path)
var external_sentinel := FileAccess.open(external_path.path_join("outside.txt"), FileAccess.WRITE)
external_sentinel.store_string("outside")
external_sentinel = null
var linked_generation := conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "3"
var link_exit := OS.execute("ln", PackedStringArray(["-s", external_path, linked_generation]))
_expect(link_exit == 0, "retention prune symlink fixture is available")
if link_exit == 0:
var linked_rejected := SaveServiceType.save_autosave(conflict_path, _snapshot(30, &"must_not_follow"), {}, 2)
_expect(not linked_rejected[&"ok"] and linked_rejected[&"code"] == &"UNSAFE_PATH", "retention shrink refuses a symlink generation before prune")
_expect(FileAccess.get_file_as_string(external_path.path_join("outside.txt")) == "outside", "prune rejection never follows or deletes the symlink target")
DirAccess.remove_absolute(linked_generation)
SaveServiceType.delete_save(conflict_path)
DirAccess.remove_absolute(external_path.path_join("outside.txt"))
DirAccess.remove_absolute(external_path)
func _test_recovery_priority_checksum_and_read_only_boundary() -> void:
var save_path := "user://saves/s6_recovery"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(10, &"backup"))
SaveServiceType.save_snapshot(save_path, _snapshot(20, &"formal"))
for minute in [101, 102, 103, 104]:
SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("auto_%d" % minute)))
var all_valid := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(all_valid[&"ok"] and all_valid[&"selected"][&"id"] == &"formal", "recovery prefers a valid formal save")
var ids: Array[StringName] = []
for candidate: Dictionary in all_valid[&"candidates"]:
ids.append(StringName(candidate[&"id"]))
_expect(ids == [&"formal", &"backup", &"migration_original", &"pre_migration_backup", &"autosave_0", &"autosave_1", &"autosave_2"], "recovery candidate order explicitly includes migration safety packages")
_corrupt_world(save_path)
var formal_corrupt := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(formal_corrupt[&"ok"] and formal_corrupt[&"selected"][&"id"] == &"backup", "checksum failure in formal save selects the verified formal backup")
_expect(formal_corrupt[&"candidates"][0][&"code"] == &"CHECKSUM_MISMATCH", "corrupt formal world is reported as checksum mismatch")
_corrupt_world(save_path + ".bak")
_corrupt_world(SaveServiceType.get_autosave_path(save_path, 0))
var observed_manifest_path := ProjectSettings.globalize_path(SaveServiceType.get_autosave_path(save_path, 1)).path_join(SaveServiceType.MANIFEST_FILE)
var manifest_before := FileAccess.get_file_as_string(observed_manifest_path)
var fallback := SaveServiceType.load_with_recovery(save_path)
var manifest_after := FileAccess.get_file_as_string(observed_manifest_path)
_expect(fallback[&"ok"] and fallback[&"selected"][&"id"] == &"autosave_1", "corrupt latest candidates fall back to the next valid autosave generation")
_expect(_marker(fallback[&"snapshot"]) == &"auto_103", "fallback returns the exact older generation snapshot")
_expect(manifest_before == manifest_after, "recovery inspection is read-only and does not promote or rewrite candidates")
var explicit := SaveServiceType.load_recovery_candidate(save_path, &"autosave", 2)
_expect(explicit[&"ok"] and explicit[&"read_only"] and _marker(explicit[&"snapshot"]) == &"auto_102", "caller can explicitly select a valid recovery generation")
var report: Dictionary = explicit[&"recovery_report"]
_expect(not report[&"automatic_repairs_applied"] and report[&"repair_policy"] == &"REPORT_ONLY_AUTHORITATIVE_SERVICE_REQUIRED", "recovery report never performs implicit domain repairs")
_expect(report[&"findings"].has(&"orphan_tasks") and report[&"findings"].has(&"orphan_event_locks"), "recovery report exposes Task and Event-lock handoff boundaries")
SaveServiceType.delete_save(save_path)
func _test_migration_registry_continuity_and_purity() -> void:
var registry := MigrationRegistryType.new()
_expect(registry.register_step(1, &"add_first", Callable(self, "_migration_add_first"))[&"ok"], "adjacent migration step 1 -> 2 registers")
_expect(registry.register_step(2, &"add_second", Callable(self, "_migration_add_second"))[&"ok"], "adjacent migration step 2 -> 3 registers")
var source := {&"nested": {&"history": []}, &"stable": 7}
var source_guard := source.duplicate(true)
var migrated := registry.migrate(source, 1, 3)
_expect(migrated[&"ok"], "N -> N+2 migration succeeds through continuous adjacent steps")
_expect(migrated[&"applied_migration_ids"] == [&"add_first", &"add_second"], "migration registry applies every step in exact version order")
_expect(migrated[&"data"][&"nested"][&"history"] == [&"one", &"two"], "second step observes the first step output")
_expect(source == source_guard and source[&"nested"][&"history"].is_empty(), "migration callbacks cannot mutate the caller-owned source")
var missing := MigrationRegistryType.new()
missing.register_step(1, &"only_first", Callable(self, "_migration_add_first"))
var missing_result := missing.migrate(source, 1, 3)
_expect(not missing_result[&"ok"] and missing_result[&"code"] == &"MIGRATION_STEP_MISSING", "registry refuses to skip a missing N+1 -> N+2 step")
_expect(missing_result[&"required_migration_id"] == &"format_2_to_3", "missing step returns its stable required migration ID")
_expect(missing_result[&"applied_migration_ids"] == [&"only_first"], "failure report preserves the exact completed-step audit trail")
var failing := MigrationRegistryType.new()
failing.register_step(1, &"known_failure", Callable(self, "_migration_reject"))
var failed := failing.migrate(source, 1, 2)
_expect(not failed[&"ok"] and failed[&"failed_migration_id"] == &"known_failure", "failed migration reports the exact registered migration ID")
_expect(failed[&"code"] == &"FIXTURE_REJECTED" and source == source_guard, "failed migration propagates its reason and preserves source purity")
func _test_current_format_snapshot_validation() -> void:
var save_path := "user://saves/s6_current_invalid"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(180, &"current_invalid"))
_make_legacy_format_zero(save_path, SaveServiceType.FORMAT_VERSION)
var direct_load := SaveServiceType.load_snapshot(save_path)
_expect(not direct_load[&"ok"] and direct_load[&"code"] == &"INVALID_SNAPSHOT", "current-format load still enforces the current snapshot schema")
var migration_attempt := SaveServiceType.migrate_save(save_path, MigrationRegistryType.new(), SaveServiceType.FORMAT_VERSION)
_expect(not migration_attempt[&"ok"] and migration_attempt[&"code"] == &"INVALID_SNAPSHOT", "no-op migration cannot bypass current snapshot validation")
SaveServiceType.delete_save(save_path)
func _test_save_package_migration_and_pre_migration_backup() -> void:
var save_path := "user://saves/s6_migration"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(200, &"legacy"), {&"display_name": "Legacy fixture"})
_make_legacy_format_zero(save_path)
var package_path := ProjectSettings.globalize_path(save_path)
var world_before := FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.WORLD_FILE))
var manifest_before := FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE))
var unsupported := SaveServiceType.load_snapshot(save_path)
_expect(not unsupported[&"ok"] and unsupported[&"code"] == &"UNSUPPORTED_FORMAT", "legacy package is not loaded before explicit migration")
var registry := MigrationRegistryType.new()
registry.register_step(0, &"legacy_zero_to_current", Callable(self, "_migration_package_zero_to_one"))
var result := SaveServiceType.migrate_save(save_path, registry, SaveServiceType.FORMAT_VERSION)
_expect(result[&"ok"] and result[&"backup_created"], "package migration verifies and promotes a current-format candidate")
_expect(result[&"applied_migration_ids"] == [&"legacy_zero_to_current"], "package migration returns its applied migration audit trail")
var current := SaveServiceType.load_snapshot(save_path)
_expect(current[&"ok"] and current[&"snapshot"][&"systems"][&"migrated_from"] == 0, "promoted package contains the pure migration output")
_expect(str(current[&"manifest"]["migration_note"]) == "zero-to-one", "migration preserves step-authored manifest metadata")
var backup_path := ProjectSettings.globalize_path(save_path + SaveServiceType.MIGRATION_BACKUP_SUFFIX)
_expect(DirAccess.dir_exists_absolute(backup_path), "migration creates a dedicated pre-migration backup package")
_expect(FileAccess.get_file_as_bytes(backup_path.path_join(SaveServiceType.WORLD_FILE)) == world_before, "pre-migration backup preserves world bytes exactly")
_expect(FileAccess.get_file_as_string(backup_path.path_join(SaveServiceType.MANIFEST_FILE)) == manifest_before, "pre-migration backup preserves the original manifest exactly")
var recovery_before_crash := SaveServiceType.inspect_recovery_candidates(save_path)
var migration_backup_info: Dictionary = recovery_before_crash[&"candidates"][3]
_expect(migration_backup_info[&"id"] == &"pre_migration_backup" and migration_backup_info[&"package_valid"] and migration_backup_info[&"requires_migration"], "recovery inventory retains a checksum-valid legacy migration backup without treating it as runtime-loadable")
var backup_old_path := backup_path + ".old"
_copy_fixture_tree(backup_path, backup_old_path)
_corrupt_world(save_path + SaveServiceType.MIGRATION_BACKUP_SUFFIX)
var repaired_backup_inventory := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(repaired_backup_inventory[&"candidates"][3][&"package_valid"] and not DirAccess.dir_exists_absolute(backup_old_path), "recovery replaces a torn pre-migration backup from its verified durable predecessor")
# Crash boundary: original was durably held and candidate was complete, but
# candidate had not yet been promoted to the formal name.
var candidate_path := package_path + ".migration.tmp"
var original_path := package_path + ".migration.original"
_copy_fixture_tree(package_path, candidate_path)
DirAccess.rename_absolute(package_path, original_path)
var recovered := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(recovered[&"ok"] and recovered[&"reconciliation"][&"migration"][&"action"] == &"completed_promotion", "recovery completes an interrupted migration promotion from durable markers")
_expect(SaveServiceType.load_snapshot(save_path)[&"ok"] and not DirAccess.dir_exists_absolute(original_path) and not DirAccess.dir_exists_absolute(candidate_path), "migration reconciliation verifies the formal package before clearing crash markers")
DirAccess.rename_absolute(package_path, original_path)
var rolled_back := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(rolled_back[&"ok"] and rolled_back[&"reconciliation"][&"migration"][&"action"] == &"rolled_back_to_original", "recovery rolls back a held original when no promotable candidate exists")
_expect(SaveServiceType.load_snapshot(save_path)[&"ok"] and not DirAccess.dir_exists_absolute(original_path), "rollback restores the original formal name without losing the verified package")
SaveServiceType.delete_save(save_path)
func _test_migration_failure_preserves_source() -> void:
var save_path := "user://saves/s6_migration_failure"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(300, &"must_survive"))
_make_legacy_format_zero(save_path)
var package_path := ProjectSettings.globalize_path(save_path)
var world_before := FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.WORLD_FILE))
var manifest_before := FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE))
var registry := MigrationRegistryType.new()
registry.register_step(0, &"reject_legacy", Callable(self, "_migration_reject"))
var result := SaveServiceType.migrate_save(save_path, registry, SaveServiceType.FORMAT_VERSION)
_expect(not result[&"ok"] and result[&"failed_migration_id"] == &"reject_legacy", "package migration exposes the failing step ID")
_expect(FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.WORLD_FILE)) == world_before, "failed step leaves the formal world byte-identical")
_expect(FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE)) == manifest_before, "failed step leaves the formal manifest byte-identical")
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(save_path + SaveServiceType.MIGRATION_BACKUP_SUFFIX)), "failed in-memory migration creates no misleading backup or partial promotion")
SaveServiceType.delete_save(save_path)
func _migration_add_first(data: Dictionary, from_version: int, to_version: int) -> Dictionary:
data[&"nested"][&"history"].append(&"one")
data[&"first_edge"] = [from_version, to_version]
return data
func _migration_add_second(data: Dictionary, from_version: int, to_version: int) -> Dictionary:
data[&"nested"][&"history"].append(&"two")
data[&"second_edge"] = [from_version, to_version]
return {&"ok": true, &"data": data}
func _migration_reject(data: Dictionary, _from_version: int, _to_version: int) -> Dictionary:
data[&"would_have_mutated"] = true
return {&"ok": false, &"code": &"FIXTURE_REJECTED", &"message": "intentional test rejection"}
func _migration_package_zero_to_one(data: Dictionary, from_version: int, to_version: int) -> Dictionary:
var legacy_snapshot: Dictionary = data[&"snapshot"]
var restored := _snapshot(
int(legacy_snapshot[&"legacy_absolute_minute"]),
StringName(legacy_snapshot[&"legacy_marker"])
)
restored[&"systems"][&"migrated_from"] = from_version
restored[&"systems"][&"migrated_to"] = to_version
data[&"snapshot"] = restored
data[&"manifest"][&"migration_note"] = "zero-to-one"
return data
func _make_legacy_format_zero(save_path: String, package_format_version := 0) -> void:
var package_path := ProjectSettings.globalize_path(save_path)
var current := SaveServiceType.load_snapshot(save_path)
var current_snapshot: Dictionary = current[&"snapshot"]
var legacy_snapshot := {
&"legacy_schema": 0,
&"legacy_absolute_minute": int(current_snapshot[&"clock"][&"absolute_minute"]),
&"legacy_marker": _marker(current_snapshot),
}
var raw_world := var_to_bytes(legacy_snapshot)
var compressed_world := raw_world.compress(FileAccess.COMPRESSION_ZSTD)
var manifest_path := package_path.path_join(SaveServiceType.MANIFEST_FILE)
var manifest_value: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest_path))
var manifest: Dictionary = manifest_value
manifest["format_version"] = package_format_version
manifest["checksum_world"] = _sha256(compressed_world)
manifest["uncompressed_size"] = raw_world.size()
var manifest_file := FileAccess.open(manifest_path, FileAccess.WRITE)
manifest_file.store_string(JSON.stringify(manifest, "\t"))
manifest_file = null
var world_path := package_path.path_join(SaveServiceType.WORLD_FILE)
var world_file := FileAccess.open(world_path, FileAccess.WRITE)
world_file.store_buffer(SaveServiceType.MAGIC.to_ascii_buffer())
world_file.store_32(package_format_version)
world_file.store_64(raw_world.size())
world_file.store_buffer(compressed_world)
world_file = null
func _copy_fixture_tree(source_path: String, destination_path: String) -> void:
DirAccess.make_dir_recursive_absolute(destination_path)
var source := DirAccess.open(source_path)
source.list_dir_begin()
var name := source.get_next()
while not name.is_empty():
if name != "." and name != "..":
var source_child := source_path.path_join(name)
var destination_child := destination_path.path_join(name)
if source.current_is_dir():
_copy_fixture_tree(source_child, destination_child)
else:
DirAccess.copy_absolute(source_child, destination_child)
name = source.get_next()
source.list_dir_end()
func _write_transaction_marker(target_absolute: String) -> void:
var marker := FileAccess.open(target_absolute + SaveServiceType.SAVE_TRANSACTION_SUFFIX, FileAccess.WRITE)
marker.store_string(SaveServiceType.SAVE_TRANSACTION_MARKER)
marker = null
func _sha256(bytes: PackedByteArray) -> String:
var context := HashingContext.new()
context.start(HashingContext.HASH_SHA256)
context.update(bytes)
return context.finish().hex_encode()
func _corrupt_world(save_path: String) -> void:
var world_path := ProjectSettings.globalize_path(save_path).path_join(SaveServiceType.WORLD_FILE)
var bytes := FileAccess.get_file_as_bytes(world_path)
if bytes.is_empty():
return
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0x5a
var file := FileAccess.open(world_path, FileAccess.WRITE)
file.store_buffer(bytes)
func _snapshot(absolute_minute: int, marker: StringName) -> Dictionary:
return {
&"schema_version": 1,
&"clock": {
&"day": 1 + absolute_minute / 1_440,
&"hour": (absolute_minute / 60) % 24,
&"minute": absolute_minute % 60,
&"absolute_minute": absolute_minute,
&"speed": 1,
&"paused": true,
},
&"game_state": {
&"schema_version": 1,
&"revision": absolute_minute,
&"resources": {&"gold": 5_000},
&"flags": {},
},
&"systems": {
&"marker": marker,
},
}
func _marker(snapshot: Dictionary) -> StringName:
return StringName(snapshot[&"systems"][&"marker"])
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://davhixen8ndkv
+1
View File
@@ -0,0 +1 @@
uid://bm3pisyt1ry1m
+54
View File
@@ -0,0 +1,54 @@
extends SceneTree
const DEFAULT_OUTPUT_PATH := "res://assets/mockups/the-tree-runtime-ui-v6-actors.png"
func _initialize() -> void:
call_deferred("_capture")
func _capture() -> void:
var capture_size := Vector2i(
maxi(int(OS.get_environment("THE_TREE_CAPTURE_WIDTH")), 1440),
maxi(int(OS.get_environment("THE_TREE_CAPTURE_HEIGHT")), 810)
)
var output_path := OS.get_environment("THE_TREE_CAPTURE_PATH")
if output_path.is_empty():
output_path = DEFAULT_OUTPUT_PATH
var capture_viewport := SubViewport.new()
capture_viewport.size = capture_size
capture_viewport.size_2d_override = Vector2i(1440, 810)
capture_viewport.size_2d_override_stretch = true
capture_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(capture_viewport)
var clock := root.get_node("GameClock")
var state := root.get_node("GameState")
var runtime := root.get_node("SimulationRuntime")
clock.reset(1, 6, 0)
state.reset()
runtime.initialize(clock, state, 424242)
clock.set_paused(true)
var main := load("res://scenes/main/main.tscn").instantiate() as Control
capture_viewport.add_child(main)
main.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for frame: int in 8:
await process_frame
main.call("_hire_staff_candidate", &"staff_akane")
main.call("_select_build_definition", &"reception")
main.call("_on_map_cell_clicked", Vector3i(-6, 0, WorldService.LAYER_FRONT))
clock.advance_minutes(120)
main.call("_clear_active_tool")
var facilities: Array[Dictionary] = runtime.construction_service.get_facilities()
if not facilities.is_empty():
main.call("_on_facility_selected", StringName(facilities[0].get(&"instance_id", &"")))
for frame: int in 4:
await process_frame
var image := capture_viewport.get_texture().get_image()
if image == null:
push_error("Could not read the capture viewport texture.")
quit(ERR_CANT_ACQUIRE_RESOURCE)
return
var result := image.save_png(output_path)
if result != OK:
push_error("Could not save UI capture: %s" % error_string(result))
quit(result)

Some files were not shown because too many files have changed in this diff Show More