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"]) == 21, "twenty-one 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_uma", "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(not heroine[&"mechanical_effects"].has(&"assigned_floor_patience_loss_multiplier_bp"), "unapproved patience substitute is absent from the original Anri definition") _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_revenue_multiplier_bp"] == 12000, "original Anri floor revenue bonus is +20 percent in integer basis points") heroine[&"display_name"] = "tampered" heroine[&"mechanical_effects"][&"assigned_floor_revenue_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_revenue_multiplier_bp"] == 12000, "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_revenue_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)