extends SceneTree ## Regression for catalog manifest ownership, invalidation, legacy saves and restore. class CountingCatalog: extends MvpContentCatalog var factory_calls := 0 func build_all_guest_definitions() -> Array[GuestDefinition]: factory_calls += 1 return super.build_all_guest_definitions() var checks := 0 var failures := 0 var runtime: Variant var clock: Variant var state: Variant func _initialize() -> void: call_deferred("run") func expect(ok: bool, label: String) -> void: checks += 1 if not ok: failures += 1 push_error("MANIFEST: " + label) func manifest(catalog: MvpContentCatalog) -> Dictionary: return runtime.call("_create_mvp_content_manifest", catalog) # Independent original construction, including the existing full factory. # No ID list, original definition field, or class/species value is omitted. func original_manifest(catalog: MvpContentCatalog) -> Dictionary: var guest_ids: Array[String] = [] for definition: GuestDefinition in catalog.build_all_guest_definitions(): guest_ids.append(String(definition.guest_type_id)) return { &"manifest_version": runtime.MVP_CONTENT_MANIFEST_VERSION, &"schema_version": MvpContentCatalog.SCHEMA_VERSION, &"guest_class_ids": strings(catalog.get_guest_class_ids()), &"species_ids": strings(catalog.get_species_ids()), &"staff_candidate_ids": strings(catalog.get_staff_candidate_ids()), &"unique_character_ids": strings(catalog.get_unique_character_ids()), &"guest_definition_ids": guest_ids, } func strings(values: Array[StringName]) -> Array[String]: var result: Array[String] = [] for value: StringName in values: result.append(String(value)) return result func documents() -> Dictionary: var result: Dictionary = {} for content_type: StringName in MvpContentCatalog.DEFAULT_PATHS: var file := FileAccess.open(MvpContentCatalog.DEFAULT_PATHS[content_type], FileAccess.READ) if file == null: return {} result[content_type] = JSON.parse_string(file.get_as_text()) file.close() return result func same(left: Variant, right: Variant) -> bool: return runtime.call("_strict_variants_equal", left, right) func check_manifest_shape(expected: Dictionary, actual: Dictionary, label: String) -> void: expect(actual.keys() == expected.keys(), label + " insertion order of manifest keys") for key: Variant in actual: expect(typeof(key) == TYPE_STRING_NAME, label + " key Variant type " + String(key)) if expected[key] is Array: var values: Array = actual[key] expect(values.get_typed_builtin() == TYPE_STRING, label + " declared typed string Array " + String(key)) var bytes := SimulationDigest.canonical_bytes(actual) expect(not bytes.is_empty() and bytes == SimulationDigest.canonical_bytes(expected), label + " nonempty canonical bytes") var digest := SimulationDigest.compute(actual) expect(not digest.is_empty() and digest == SimulationDigest.compute(expected), label + " nonempty exact digest") 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 run() -> void: 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() expect(runtime.initialize(clock,state,840_101).get(&"valid", false), "canonical initialization") clock.set_paused(true) var cached: bool = runtime.has_method("_clear_mvp_content_manifest_cache") expect(cached, "production manifest cache is installed") var catalog := CountingCatalog.new() expect(catalog.rebuild().get(&"valid", false), "counting fixture uses unchanged real catalog") runtime.mvp_content_catalog = catalog var expected := original_manifest(catalog) catalog.factory_calls = 0 var first := manifest(catalog) expect(same(expected, first), "first read preserves all values and generated ID ordering") check_manifest_shape(expected, first, "first fill") first[&"manifest_version"] = -1 first[&"staff_candidate_ids"].clear() first[&"guest_definition_ids"][0] = "poisoned" var second := manifest(catalog) expect(same(expected, second), "first returned manifest never aliases live/private data") check_manifest_shape(expected, second, "second read") expect(catalog.factory_calls == (1 if cached else 2), "candidate fills once; reference rebuilds each read") second[&"species_ids"].clear() expect(same(expected, manifest(catalog)), "cache hit has independently owned nested arrays") var entry := catalog.get_entry(&"guest_classes", &"cleric") entry[&"need_rates"][&"hunger"] = 99 var ids := catalog.get_species_ids() ids.clear() var generated := catalog.build_guest_definition(&"cleric", &"human") generated.guest_type_id = &"poisoned_returned_definition" expect(same(expected, manifest(catalog)), "entry/ID/factory getters cannot mutate cached membership") var prior_generation: int = int(catalog.call("get_manifest_generation")) if cached else -1 var builds_before := catalog.factory_calls var docs := documents() docs[&"guest_classes"]["entries"][0]["base_patience"] = 61 expect(catalog.rebuild_from_documents(docs).get(&"valid", false), "same IDs with changed real fields rebuild validly") if cached: expect(int(catalog.call("get_manifest_generation")) > prior_generation, "rebuild advances generation even when IDs do not change") expect(same(expected, manifest(catalog)) and catalog.factory_calls == builds_before+1, "changed definitions force original factory despite unchanged membership") docs[&"guest_classes"]["entries"][0]["unknown_definition_field"] = 1 expect(not catalog.rebuild_from_documents(docs).get(&"valid", true), "unknown original fields remain fail-closed") var empty_expected := original_manifest(catalog) expect(same(empty_expected, manifest(catalog)), "invalid rebuild cannot expose earlier valid manifest") expect(catalog.rebuild().get(&"valid", false), "valid recovery uses normal file rebuild") expect(same(expected, manifest(catalog)), "valid recovery invalidates empty generation") expect(not catalog.rebuild({&"unknown_content_type":"res://missing.json"}).get(&"valid", true), "invalid path set is rejected") expect(same(original_manifest(catalog), manifest(catalog)), "failed path validation invalidates manifest") expect(catalog.rebuild().get(&"valid", false), "recover after path validation failure") var missing_paths: Dictionary = MvpContentCatalog.DEFAULT_PATHS.duplicate(true) missing_paths[&"guest_classes"] = "res://missing_manifest_cache_content.json" expect(not catalog.rebuild(missing_paths).get(&"valid", true), "read failure remains fail-closed") expect(same(original_manifest(catalog), manifest(catalog)), "read failure cannot retain prior successful generation") expect(catalog.rebuild().get(&"valid", false), "recover after read failure") var replacement := CountingCatalog.new() expect(replacement.rebuild().get(&"valid", false), "new catalog identity is valid") runtime.mvp_content_catalog = replacement expect(same(expected, manifest(replacement)) and replacement.factory_calls == 1, "different identity cannot hit previous template") var same_generation := CountingCatalog.new() expect(same_generation.rebuild().get(&"valid", false), "second fresh identity has equivalent content") if cached: expect(replacement.call("get_manifest_generation") == same_generation.call("get_manifest_generation"), "identity test deliberately uses equal generations") runtime.mvp_content_catalog = same_generation expect(same(expected, manifest(same_generation)) and same_generation.factory_calls == 1, "equal generation on a new identity must still rebuild") replacement = same_generation # Temporary restore catalogs must be fully built without changing live cache. var temporary := CountingCatalog.new() temporary.rebuild() var live_calls := replacement.factory_calls manifest(temporary) manifest(temporary) expect(temporary.factory_calls == 2, "temporary catalog remains uncached for explicit restore validation") expect(same(expected, manifest(replacement)), "temporary validation preserves current content") if cached: expect(replacement.factory_calls == live_calls, "temporary validation does not replace live cache") var old := expected.duplicate(true) old[&"manifest_version"] = 1 old[&"staff_candidate_ids"].erase("staff_uma") var old_data := {&"catalog_manifest":old, &"hired_staff_candidate_ids":[], &"staff":{&"staff":[]}} if cached: runtime.call("_clear_mvp_content_manifest_cache") expect(runtime.call("_saved_content_manifest_matches", replacement, old_data), "sole prior chapel manifest remains accepted on first fill") expect(same(expected, manifest(replacement)), "legacy first-fill edits cannot poison current v2 template") expect(runtime.call("_saved_content_manifest_matches", replacement, old_data), "legacy prior manifest remains accepted on a hit") expect(same(expected, manifest(replacement)), "legacy hit edits leave staff_uma and manifest version intact") var guard_before := complete_live_guard() var before: Dictionary = guard_before[&"snapshot"] var tampered := before.duplicate(true) for definition: Dictionary in tampered[&"systems"][&"runtime"][&"guests"][&"definitions"]: if String(definition[&"guest_type_id"]) == String(expected[&"guest_definition_ids"][0]): definition[&"wealth_min"] = int(definition[&"wealth_min"])+1 break var catalog_before: MvpContentCatalog = runtime.mvp_content_catalog expect(not runtime.restore_snapshot(tampered), "cached manifest does not bypass full GuestDefinition value validation") expect(runtime.mvp_content_catalog == catalog_before and same(complete_live_guard(), guard_before), "rejected restore preserves identity and all full save-lifecycle guard fields") expect(runtime.restore_snapshot(before), "canonical save restores unchanged") expect(runtime.mvp_content_catalog != catalog_before and same(runtime.create_snapshot(), before), "successful restore replaces identity while preserving complete saved state") var custom: GuestDefinition = runtime.guest_service.create_definition(&"manifest_late_guest", {&"adult_verified":true, &"wealth_min":10, &"wealth_max":20}) expect(custom != null, "late custom registration remains supported") if custom != null: custom.base_patience = 42 expect(runtime.guest_service.get_definition(&"manifest_late_guest").base_patience == 42, "live GuestService definition semantics remain intact") expect(same(expected, manifest(runtime.mvp_content_catalog)), "custom registered definitions do not replace catalog membership") var public_snapshot: Dictionary = runtime.create_snapshot() public_snapshot[&"systems"][&"runtime"][&"catalog_manifest"][&"guest_definition_ids"].clear() expect(same(expected, runtime.create_snapshot()[&"systems"][&"runtime"][&"catalog_manifest"]), "public full snapshot cannot mutate next snapshot manifest") runtime.call("_publish_snapshot", clock.absolute_minute) expect(same(runtime.latest_snapshot, runtime.create_snapshot()), "publication still eagerly contains all current components") var published_digest: String = runtime.latest_digest expect(published_digest == SimulationDigest.compute(runtime.latest_snapshot), "latest_digest remains exact") var replay_start: Dictionary = runtime.create_snapshot() clock.advance_minutes(7) var replay_end: Dictionary = runtime.create_snapshot() var retained_start := replay_start.duplicate(true) expect(runtime.restore_snapshot(replay_start), "retained checkpoint restores for tick replay") clock.advance_minutes(7) var repeated: Dictionary = runtime.create_snapshot() expect(same(replay_start, retained_start), "live ticks and restore never mutate retained public checkpoint") expect(same(replay_end, repeated), "full tick replay preserves every stored event/history/counter and component") expect(not SimulationDigest.canonical_bytes(repeated).is_empty() and SimulationDigest.canonical_bytes(repeated) == SimulationDigest.canonical_bytes(replay_end), "tick replay compares complete nonempty canonical bytes without event exclusions") if cached: var previous_identity: MvpContentCatalog = runtime.mvp_content_catalog expect(runtime.initialize(clock,state,840_102).get(&"valid", false), "repeat initialization remains valid") expect(runtime.mvp_content_catalog != previous_identity and same(expected, manifest(runtime.mvp_content_catalog)), "initialization clears old identity while preserving manifest values") runtime.free() clock.free() state.free() print("MANIFEST_CACHE_CORRECTNESS checks=%d failures=%d candidate=%s" % [checks, failures, cached]) quit(failures)