Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
223 lines
13 KiB
GDScript
223 lines
13 KiB
GDScript
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)
|