Files
TheTree_mock/tests/mvp/test_mvp_content_catalog.gd
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
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.
2026-09-30 23:52:24 +09:00

344 lines
19 KiB
GDScript

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)