Files
OpenMaidEngine/tools/test_globals.py
2026-07-23 23:36:02 -04:00

236 lines
13 KiB
Python

#!/usr/bin/env python3
"""Standalone tests for the globals registry tooling. Run: py -3.11 -X utf8 tools/test_globals.py"""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import paths
import globals_build as G
FAILS = []
def check(cond, msg):
print((" ok " if cond else " FAIL ") + msg)
if not cond: FAILS.append(msg)
def test_load_and_lint():
entries, meta = G.load_toml(paths.VM_MAP / "globals.toml")
check(0xa57 in entries, "0xa57 present in globals.toml")
check(entries[0xa57]["category"] == "story-flag", "0xa57 is category story-flag")
check(entries[0x3234]["category"] == "story-flag", "0x3234 is category story-flag")
check({0xa57, 0xa58, 0xa59} <= set(entries), "Lily form flags A/B/C all present")
check(entries[0x4e085]["columns"] == {
"0": "current_hp", "1": "current_sp", "2": "current_fs"
}, "entity current-resource columns are curated")
check(entries[0x4e11b]["columns"]["10"] == "movement"
and entries[0x4e11b]["columns"]["13"] == "max_fs",
"entity effective-stat columns are curated")
check(entries[0x5231f]["name"] == "entity_tile_x"
and entries[0x52351]["name"] == "entity_tile_y",
"entity map-coordinate arrays are curated")
check(entries[0x4e021]["name"] == "entity_runtime_flags"
and entries[0x522ed]["name"] == "entity_faction_ids",
"entity activity and faction arrays are curated")
check(entries[0x4e693]["name"] == "entity_skill_flags",
"per-entity skill flags are curated")
check(entries[0x53e13]["name"] == "entity_carried_item_ids"
and entries[0x53e77]["name"] == "entity_carried_item_counts",
"per-entity carried-item slots are curated")
check(entries[0xeff77]["name"] == "routine_execution_state"
and entries[0xeff77]["value_domain"] == "0..3",
"movement routine result states are curated")
check(entries[0x56b20]["name"] == "entity_patrol_waypoint_indices",
"RTN_M011 waypoint state is curated")
check(entries[0xaba96]["name"] == "pathfinding_remaining_route_steps"
and entries[0xcc9f1]["name"] == "movement_search_mode",
"movement-search reachability state is curated")
check(entries[0xbf6fe]["name"] == "pathfinding_filtered_route_scores"
and entries[0xcc9f4]["name"] == "offensive_action_scope_masks",
"movement target filtering and offensive scope are curated")
check(entries[0xb8d86]["name"] == "action_range_distance_grid"
and entries[0xcc9f2]["name"] == "usable_action_min_range"
and entries[0xcc9f3]["name"] == "usable_action_max_range",
"action-range search state is curated")
check(entries[0x66713]["name"] == "acting_entity_index"
and entries[0x66714]["name"] == "target_entity_index"
and entries[0x52289]["name"] == "entity_selected_action_ids",
"AI actor, target, and selected action state are curated")
check(entries[0xcca08]["name"] == "offensive_action_attack_elements"
and entries[0xab5ba]["name"]
== "attack_element_effectiveness_percent",
"AI action-element eligibility state is curated")
check(entries[0xcc9fe]["name"] == "healing_action_scope_masks",
"AI healing-action eligibility state is curated")
check(entries[0x15a2a8]["name"] == "current_glossary_topic_id"
and entries[0x15a2a9]["name"]
== "glossary_topic_unlock_seen_decision_ids"
and entries[0x463b]["name"] == "glossary_topic_titles",
"VIINIT/VIMES glossary state is curated")
check(entries[0x15a759]["name"] == "current_enemy_encyclopedia_unit_id"
and entries[0x56b85]["name"]
== "enemy_encyclopedia_revealed_flags",
"EBINIT/EIMES enemy-encyclopedia state is curated")
check(entries[0x15a117]["name"] == "current_character_profile_id"
and entries[0x15a118]["name"] == "character_profile_unit_ids"
and entries[0x15a17c]["name"]
== "character_profile_portrait_asset_ids"
and entries[0x45d7]["name"] == "character_profile_names",
"CIINIT/CIMES character-profile state is curated")
check(entries[0x1560e7]["name"] == "current_magic_action_id"
and entries[0x45b9]["name"] == "magic_action_names"
and entries[0x1561f6]["name"]
== "magic_action_information_handler_script_ids",
"MAINIT/MAMES action state is curated")
check(entries[0x453b]["name"] == "training_action_text"
and entries[0x155bbc]["name"]
== "training_action_required_story_flag_ids"
and entries[0x155e9b]["columns"]["13"] == "max_fs"
and entries[0x156015]["columns"]["9"] == "execution_10"
and entries[0x6722]["name"] == "familiar_alignment"
and entries[0x6727]["name"] == "training_action_execution_counts",
"TRINIT/TRAIN action state is curated")
check(entries[0x152485]["name"] == "current_card_generation_list_id"
and entries[0x152486]["columns"]["2"] == "growth_weight"
and entries[0x1525b2]["name"] == "card_generation_card_ids"
and entries[0x1519f8]["name"] == "current_card_id"
and entries[0x151a5d]["columns"]["2"]
== "engine_dead_required_flag_3"
and entries[0x204f4]["name"] == "current_stage_turn",
"CDINIT/FIELD card-generation state is curated")
check(entries[0x1519f9]["name"] == "card_definition_type_ids"
and entries[0x151de1]["columns"]["2"] == "fs"
and entries[0x15222d]["name"]
== "card_definition_maximum_resource_damage"
and entries[0x152359]["name"] == "card_definition_condition_ids"
and entries[0x152421]["name"] == "card_definition_visual_asset_ids"
and entries[0x4dfbb]["name"]
== "stage_card_spendable_point_bonus",
"CDINIT2/FIELD card-effect state is curated")
check(entries[0x15288c]["name"] == "selected_battle_animation_id"
and entries[0x15288e]["columns"]["4"]
== "reserved_effect_slot_4"
and entries[0x153ffe]["name"]
== "battle_animation_effect_start_delays_ms"
and entries[0x15576e]["name"] == "battle_animation_duration_ms"
and entries[0x155b5c]["name"] == "battle_effect_visual_mode_ids"
and entries[0x155b7a]["name"] == "battle_effect_atlas_row_counts"
and entries[0x155baa]["columns"]["2"] == "pulse_3",
"BTANINIT/BTANINIT2 battle-animation state is curated")
check(entries[0x27bd]["name"] == "stage_display_names"
and entries[0x2ba5]["columns"]["5"] == "cleared_line_3"
and entries[0xe7e8d]["name"] == "stage_unlock_group_ids"
and entries[0xe8275]["name"] == "stage_main_progression_flags"
and entries[0xe865d]["columns"]["6"] == "forbidden_flag_7"
and entries[0xea1b5]["columns"]["6"] == "required_flag_7",
"STINIT2 stage text and availability state is curated")
check(entries[0xebd0d]["name"] == "stage_display_number_major"
and entries[0xec0f5]["name"] == "stage_display_number_minor"
and entries[0xed47d]["name"] == "stage_minimap_atlas_origin_y"
and entries[0xed865]["name"]
== "stage_clear_base_spendable_point_rewards",
"STINIT2 stage numbering, minimap, and point rewards are curated")
check(entries[0xee035]["columns"]
== {"0": "entry", "1": "clear", "2": "failure"}
and entries[0xeebed]["name"] == "stage_extra_dungeon_flags"
and entries[0xeefd5]["columns"]["2"] == "gold_coin_item_93"
and entries[0xefb8d]["name"] == "stage_loader_script_ids",
"STINIT2 flow, EX, coin, and loader columns are curated")
check(entries[0x15a095]["name"] == "information_tab_index"
and entries[0x15a096]["name"] == "information_message_handled"
and entries[0x15a097]["name"]
== "information_message_handler_script_ids",
"INFOMES handler registry state is curated")
check(entries[0x7db]["name"] == "modal_message_lines"
and entries[0x665d6]["name"] == "modal_message_line_count"
and entries[0x7e5]["name"] == "modal_annotation_texts"
and entries[0x665e3]["name"]
== "modal_annotation_horizontal_cells",
"MES modal-buffer state is curated")
check(entries[0x5f0ed]["name"] == "scene_decision_seen_flags",
"glossary prerequisite seen-state is curated")
check(entries[0x20543]["name"] == "tile_faction_traversal_masks",
"faction-specific tile traversal masks are curated")
check(entries[0xaba64]["name"] == "stage_object_runtime_flags",
"stage-object targeting flags are curated")
check(entries[0xb240e]["name"] == "pathfinding_movement_costs",
"movement-cost work grid is curated")
# lint clean against a permissive address universe (curated addrs are self-consistent)
errors, warnings = G.lint(entries, set(entries))
check(errors == [], f"globals.toml lints clean (errors={errors})")
def test_lint_catches_bad_vocab():
bad = {0x1: {"_addr": 0x1, "name": "x", "category": "bogus",
"source": "auto-shape", "confidence": "high",
"columns": {"not-an-index": "x", "-1": ""}}}
errors, _ = G.lint(bad, {0x1})
check(any("category" in e for e in errors), "lint flags bad category")
check(any("confidence" in e for e in errors), "lint flags auto-shape claiming high confidence")
check(any("column index" in e for e in errors), "lint flags nonnumeric column indices")
check(any("negative column" in e for e in errors), "lint flags negative column indices")
def test_merge_precedence():
curated, _ = G.load_toml(paths.VM_MAP / "globals.toml")
auto = G.load_auto(paths.BUILD / "global-var-map.json")
merged = G.merge(curated, auto)
check(merged[0xa57]["name"] == "lily_form_a", "curated 0xa57 name wins over auto label")
check(merged[0xa57]["category"] == "story-flag", "curated 0xa57 category overrides auto string-table")
check(merged[0xa57]["provenance"] == "curated", "0xa57 marked curated")
check(merged[0x9f541]["columns"]["8"] == "critical_chance",
"curated row-table column semantics survive the merge")
# an address only in the auto map falls through as provenance=auto
auto_only = next((a for a in auto.get("globals", {})
if int(a, 16) not in curated and auto["globals"][a].get("label")), None)
check(auto_only is not None and merged[int(auto_only, 16)]["provenance"] == "auto",
"auto-only address retained with provenance=auto")
def test_sys4load_labels_from_registry():
import importlib, sys4load
importlib.reload(sys4load) # re-run _load_global_labels against current build/globals.json
lbl = sys4load.GLOBAL_LABELS.get(0x3234, "")
check("chapter_mode" in lbl, f"sys4load labels 0x3234 with curated name (got {lbl!r})")
lbl2 = sys4load.GLOBAL_LABELS.get(0xa57, "")
check("lily_form_a" in lbl2, f"sys4load labels 0xa57 with curated name (got {lbl2!r})")
def test_miner_finds_known_flags():
import story_flags
cands = story_flags.mine()
check(0x3234 in cands, "miner surfaces chapter flag 0x3234")
check(set(range(1, 9)) <= set(cands[0x3234]["consts"]), "0x3234 compared against 1..8 enum")
check(cands[0x3234]["category"] == "story-flag", "0x3234 classified story-flag")
check(0xa57 in cands, "miner surfaces Lily form flag 0xa57")
check(cands[0xa57]["reach_scenes"] >= 70, "0xa57 high scene reach")
check(cands[0xa57]["category"] == "story-flag", "0xa57 classified story-flag")
def test_bootstrap_is_additive_and_idempotent():
import tempfile, pathlib, story_flags
# Minimal fixture (one curated entry) so bootstrap always has candidates to add, independent
# of how many the real globals.toml already holds.
src = ('[meta]\nnote = "fixture"\n\n[[global]]\naddress = "0xa57"\nname = "lily_form_a"\n'
'category = "story-flag"\ntype = "int"\nvalue_domain = "{0,1}"\nusage = "curated"\n'
'source = "investigation"\nconfidence = "high"\ndepends_on = []\n')
with tempfile.TemporaryDirectory() as d:
tp = pathlib.Path(d) / "globals.toml"
tp.write_text(src, encoding="utf-8")
before, _ = G.load_toml(tp)
cands = story_flags.mine()
story_flags.bootstrap(cands, toml_path=tp)
after, _ = G.load_toml(tp)
check(len(after) > len(before), "bootstrap adds new skeleton entries")
check(after[0xa57]["name"] == "lily_form_a", "bootstrap preserves curated 0xa57")
# idempotent: second run adds nothing
n1 = len(after)
story_flags.bootstrap(story_flags.mine(), toml_path=tp)
after2, _ = G.load_toml(tp)
check(len(after2) == n1, "second bootstrap is a no-op (idempotent)")
# every skeleton is auto-shape and not high-confidence
added = set(after) - set(before)
check(all(after[a]["source"] == "auto-shape" for a in added), "skeletons are source=auto-shape")
check(all(after[a]["confidence"] != "high" for a in added), "skeletons never high confidence")
if __name__ == "__main__":
test_load_and_lint()
test_lint_catches_bad_vocab()
test_merge_precedence()
test_sys4load_labels_from_registry()
test_miner_finds_known_flags()
test_bootstrap_is_additive_and_idempotent()
print(f"\n{len(FAILS)} failures")
sys.exit(1 if FAILS else 0)