diff --git a/gui/config_integration.py b/gui/config_integration.py index fd15bc8..ec1beae 100644 --- a/gui/config_integration.py +++ b/gui/config_integration.py @@ -1,283 +1,283 @@ -""" -Configuration Integration Helper -Updates the actual module files with GUI configuration settings -""" - -import re -import os -from pathlib import Path -from typing import Dict, Any - - -class ConfigIntegration: - """Helper class to integrate GUI configuration with module files.""" - - def __init__(self): - self.modules_dir = Path("modules") - - def update_rpgmaker_config(self, config: Dict[str, Any]) -> bool: - """Update rpgmakermvmz.py with configuration from GUI.""" - module_path = self.modules_dir / "rpgmakermvmz.py" - - if not module_path.exists(): - raise FileNotFoundError(f"Module file not found: {module_path}") - - try: - # Read the current module file - with open(module_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Update configuration values - updated_content = self._update_config_values(content, config) - - # Write back to file - with open(module_path, 'w', encoding='utf-8') as f: - f.write(updated_content) - - return True - - except Exception as e: - raise e - - def _update_config_values(self, content: str, config: Dict[str, Any]) -> str: - """Update configuration values in the module content.""" - lines = content.split('\n') - updated_lines = [] - - # Track which configurations we found and updated - found_configs = set() - - for line in lines: - updated_line = line - - # Look for configuration assignments - for config_key, config_value in config.items(): - # Match lines like: CONFIG_NAME = value # comment - pattern = rf'^({re.escape(config_key)})\s*=\s*.*?(#.*)?$' - match = re.match(pattern, line.strip()) - - if match: - # Preserve the comment if it exists - comment = match.group(2) if match.group(2) else "" - if comment: - comment = " " + comment - - # Create the new line with proper formatting - updated_line = f"{config_key} = {config_value}{comment}" - found_configs.add(config_key) - break - - updated_lines.append(updated_line) - - # Check if all configurations were found - missing_configs = set(config.keys()) - found_configs - if missing_configs: - print(f"Warning: Could not find these configurations in module: {missing_configs}") - - return '\n'.join(updated_lines) - - def update_env_file(self, config: Dict[str, Any], env_path: Path = None) -> bool: - """Update .env file with configuration.""" - if env_path is None: - env_path = Path(".env") - - try: - from dotenv import set_key - - for key, value in config.items(): - set_key(env_path, key, str(value)) - - return True - - except Exception as e: - print(f"Error updating .env file: {e}") - return False - - def read_current_config(self, module_path: Path = None) -> Dict[str, Any]: - """Read current configuration from module file.""" - if module_path is None: - module_path = self.modules_dir / "rpgmakermvmz.py" - - if not module_path.exists(): - return {} - - config = {} - - try: - with open(module_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Find configuration lines used by the RPG Maker config UI. - bool_pattern = ( - r'^(FIRSTLINESPEAKERS|INLINE401SPEAKERS|FACENAME101|NAMES|' - r'BRFLAG|FIXTEXTWRAP|IGNORETLTEXT|TLSYSTEMVARIABLES|' - r'TLSYSTEMSWITCHES|JOIN408|SPEAKERS408|CODE\d+)\s*=\s*(True|False)' - ) - int_pattern = r'^(CODE122_VAR_MIN|CODE122_VAR_MAX)\s*=\s*(\d+)' - - for line in content.split('\n'): - line = line.strip() - match = re.match(bool_pattern, line) - if match: - key = match.group(1) - value = match.group(2) == 'True' - config[key] = value - continue - - match = re.match(int_pattern, line) - if match: - key = match.group(1) - config[key] = int(match.group(2)) - - except Exception as e: - print(f"Error reading configuration from {module_path}: {e}") - - return config - - def read_plugin_config(self, module_path: Path = None) -> Dict[str, Any]: - """Read ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals from module file.""" - if module_path is None: - module_path = self.modules_dir / "rpgmakermvmz.py" - result: Dict[str, Any] = { - "ENABLED_PLUGINS_357": set(), - "ENABLED_PATTERNS_355655": set(), - } - if not module_path.exists(): - return result - try: - with open(module_path, "r", encoding="utf-8") as f: - content = f.read() - for var_name in ("ENABLED_PLUGINS_357", "ENABLED_PATTERNS_355655"): - # Match: VAR_NAME: set = set() or VAR_NAME: set = {"a", "b"} - m = re.search( - rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(\{{[^}}]*\}}|set\(\))', - content, - re.MULTILINE, - ) - if m: - bracket = m.group(1) - items = re.findall(r'"([^"]+)"|\'([^\']+)\'', bracket) - result[var_name] = {a or b for a, b in items} - except Exception as e: - print(f"Error reading plugin config: {e}") - return result - - def update_plugin_config( - self, - enabled_357: set, - enabled_355655: set, - module_path: Path = None, - ) -> bool: - """Write ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals to module file.""" - if module_path is None: - module_path = self.modules_dir / "rpgmakermvmz.py" - if not module_path.exists(): - raise FileNotFoundError(f"Module file not found: {module_path}") - - def _fmt(s: set) -> str: - if not s: - return "set()" - items = ", ".join(f'"{k}"' for k in sorted(s)) - return "{" + items + "}" - - try: - with open(module_path, "r", encoding="utf-8") as f: - content = f.read() - for var_name, val_set in ( - ("ENABLED_PLUGINS_357", enabled_357), - ("ENABLED_PATTERNS_355655", enabled_355655), - ): - content = re.sub( - rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(?:\{{[^}}]*\}}|set\(\))', - f'{var_name}: set = {_fmt(val_set)}', - content, - flags=re.MULTILINE | re.DOTALL, - ) - with open(module_path, "w", encoding="utf-8") as f: - f.write(content) - return True - except Exception as e: - raise e - - def validate_module_syntax(self, module_path: Path = None) -> bool: - """Validate that the module file has correct Python syntax.""" - if module_path is None: - module_path = self.modules_dir / "rpgmakermvmz.py" - - try: - with open(module_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Try to compile the code - compile(content, str(module_path), 'exec') - return True - - except SyntaxError as e: - print(f"Syntax error in {module_path}: {e}") - return False - except Exception as e: - print(f"Error validating {module_path}: {e}") - return False - - def create_config_template(self) -> Dict[str, Any]: - """Create a template configuration with default values.""" - return { - # General config - "FIRSTLINESPEAKERS": False, - "INLINE401SPEAKERS": False, - "FACENAME101": False, - "NAMES": False, - "BRFLAG": False, - "FIXTEXTWRAP": True, - "IGNORETLTEXT": False, - - # Main Codes - "CODE401": True, - "CODE405": True, - "CODE102": True, - - # Optional codes - "CODE101": False, - "CODE408": False, - - # Variable codes - "CODE122": False, - - # Other codes - "CODE355655": False, - "CODE357": False, - "CODE657": False, - "CODE356": False, - "CODE320": False, - "CODE324": False, - "CODE111": False, - "CODE108": False - } - - def get_config_descriptions(self) -> Dict[str, str]: - """Get descriptions for all configuration options.""" - return { - "FIRSTLINESPEAKERS": "If 1st line of 401 is a speaker, set to True", - "INLINE401SPEAKERS": "Detect speaker from Name\u300cdialogue\u300d inline format in 401 lines", - "FACENAME101": "Find Speakers in 101 Codes based on Face Name", - "NAMES": "Output a list of all the character names found", - "BRFLAG": "If the game uses
instead of newlines", - "FIXTEXTWRAP": "Overwrites textwrap for better formatting", - "IGNORETLTEXT": "Ignores all translated text", - - "CODE401": "Show Text - Main dialogue content", - "CODE405": "Show Text (Scrolling) - Longer dialogue", - "CODE102": "Show Choices - Player choice options", - "CODE101": "Character Names - Turn on when names exist in 101", - "CODE408": "Comments - WARNING: Can inflate costs significantly", - "CODE122": "Control Variables - Text stored in variables", - - "CODE355655": "Scripts - Text within script commands", - "CODE357": "Picture Text - Text displayed on pictures", - "CODE657": "Picture Text Extended - Extended picture text", - "CODE356": "Plugin Commands - Plugin command parameters", - "CODE320": "Change Name Input - Name input prompts", - "CODE324": "Change Nickname - Nickname changes", - "CODE111": "Conditional Branch - Conditional text", - "CODE108": "Comments - Comment blocks" - } +""" +Configuration Integration Helper +Updates the actual module files with GUI configuration settings +""" + +import re +import os +from pathlib import Path +from typing import Dict, Any + + +class ConfigIntegration: + """Helper class to integrate GUI configuration with module files.""" + + def __init__(self): + self.modules_dir = Path("modules") + + def update_rpgmaker_config(self, config: Dict[str, Any]) -> bool: + """Update rpgmakermvmz.py with configuration from GUI.""" + module_path = self.modules_dir / "rpgmakermvmz.py" + + if not module_path.exists(): + raise FileNotFoundError(f"Module file not found: {module_path}") + + try: + # Read the current module file + with open(module_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Update configuration values + updated_content = self._update_config_values(content, config) + + # Write back to file + with open(module_path, 'w', encoding='utf-8') as f: + f.write(updated_content) + + return True + + except Exception as e: + raise e + + def _update_config_values(self, content: str, config: Dict[str, Any]) -> str: + """Update configuration values in the module content.""" + lines = content.split('\n') + updated_lines = [] + + # Track which configurations we found and updated + found_configs = set() + + for line in lines: + updated_line = line + + # Look for configuration assignments + for config_key, config_value in config.items(): + # Match lines like: CONFIG_NAME = value # comment + pattern = rf'^({re.escape(config_key)})\s*=\s*.*?(#.*)?$' + match = re.match(pattern, line.strip()) + + if match: + # Preserve the comment if it exists + comment = match.group(2) if match.group(2) else "" + if comment: + comment = " " + comment + + # Create the new line with proper formatting + updated_line = f"{config_key} = {config_value}{comment}" + found_configs.add(config_key) + break + + updated_lines.append(updated_line) + + # Check if all configurations were found + missing_configs = set(config.keys()) - found_configs + if missing_configs: + print(f"Warning: Could not find these configurations in module: {missing_configs}") + + return '\n'.join(updated_lines) + + def update_env_file(self, config: Dict[str, Any], env_path: Path = None) -> bool: + """Update .env file with configuration.""" + if env_path is None: + env_path = Path(".env") + + try: + from dotenv import set_key + + for key, value in config.items(): + set_key(env_path, key, str(value)) + + return True + + except Exception as e: + print(f"Error updating .env file: {e}") + return False + + def read_current_config(self, module_path: Path = None) -> Dict[str, Any]: + """Read current configuration from module file.""" + if module_path is None: + module_path = self.modules_dir / "rpgmakermvmz.py" + + if not module_path.exists(): + return {} + + config = {} + + try: + with open(module_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Find configuration lines used by the RPG Maker config UI. + bool_pattern = ( + r'^(FIRSTLINESPEAKERS|INLINE401SPEAKERS|FACENAME101|NAMES|' + r'BRFLAG|FIXTEXTWRAP|IGNORETLTEXT|PRESERVEORIGINAL|TLSYSTEMVARIABLES|' + r'TLSYSTEMSWITCHES|JOIN408|SPEAKERS408|CODE\d+)\s*=\s*(True|False)' + ) + int_pattern = r'^(CODE122_VAR_MIN|CODE122_VAR_MAX)\s*=\s*(\d+)' + + for line in content.split('\n'): + line = line.strip() + match = re.match(bool_pattern, line) + if match: + key = match.group(1) + value = match.group(2) == 'True' + config[key] = value + continue + + match = re.match(int_pattern, line) + if match: + key = match.group(1) + config[key] = int(match.group(2)) + + except Exception as e: + print(f"Error reading configuration from {module_path}: {e}") + + return config + + def read_plugin_config(self, module_path: Path = None) -> Dict[str, Any]: + """Read ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals from module file.""" + if module_path is None: + module_path = self.modules_dir / "rpgmakermvmz.py" + result: Dict[str, Any] = { + "ENABLED_PLUGINS_357": set(), + "ENABLED_PATTERNS_355655": set(), + } + if not module_path.exists(): + return result + try: + with open(module_path, "r", encoding="utf-8") as f: + content = f.read() + for var_name in ("ENABLED_PLUGINS_357", "ENABLED_PATTERNS_355655"): + # Match: VAR_NAME: set = set() or VAR_NAME: set = {"a", "b"} + m = re.search( + rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(\{{[^}}]*\}}|set\(\))', + content, + re.MULTILINE, + ) + if m: + bracket = m.group(1) + items = re.findall(r'"([^"]+)"|\'([^\']+)\'', bracket) + result[var_name] = {a or b for a, b in items} + except Exception as e: + print(f"Error reading plugin config: {e}") + return result + + def update_plugin_config( + self, + enabled_357: set, + enabled_355655: set, + module_path: Path = None, + ) -> bool: + """Write ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals to module file.""" + if module_path is None: + module_path = self.modules_dir / "rpgmakermvmz.py" + if not module_path.exists(): + raise FileNotFoundError(f"Module file not found: {module_path}") + + def _fmt(s: set) -> str: + if not s: + return "set()" + items = ", ".join(f'"{k}"' for k in sorted(s)) + return "{" + items + "}" + + try: + with open(module_path, "r", encoding="utf-8") as f: + content = f.read() + for var_name, val_set in ( + ("ENABLED_PLUGINS_357", enabled_357), + ("ENABLED_PATTERNS_355655", enabled_355655), + ): + content = re.sub( + rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(?:\{{[^}}]*\}}|set\(\))', + f'{var_name}: set = {_fmt(val_set)}', + content, + flags=re.MULTILINE | re.DOTALL, + ) + with open(module_path, "w", encoding="utf-8") as f: + f.write(content) + return True + except Exception as e: + raise e + + def validate_module_syntax(self, module_path: Path = None) -> bool: + """Validate that the module file has correct Python syntax.""" + if module_path is None: + module_path = self.modules_dir / "rpgmakermvmz.py" + + try: + with open(module_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Try to compile the code + compile(content, str(module_path), 'exec') + return True + + except SyntaxError as e: + print(f"Syntax error in {module_path}: {e}") + return False + except Exception as e: + print(f"Error validating {module_path}: {e}") + return False + + def create_config_template(self) -> Dict[str, Any]: + """Create a template configuration with default values.""" + return { + # General config + "FIRSTLINESPEAKERS": False, + "INLINE401SPEAKERS": False, + "FACENAME101": False, + "NAMES": False, + "BRFLAG": False, + "FIXTEXTWRAP": True, + "IGNORETLTEXT": False, + + # Main Codes + "CODE401": True, + "CODE405": True, + "CODE102": True, + + # Optional codes + "CODE101": False, + "CODE408": False, + + # Variable codes + "CODE122": False, + + # Other codes + "CODE355655": False, + "CODE357": False, + "CODE657": False, + "CODE356": False, + "CODE320": False, + "CODE324": False, + "CODE111": False, + "CODE108": False + } + + def get_config_descriptions(self) -> Dict[str, str]: + """Get descriptions for all configuration options.""" + return { + "FIRSTLINESPEAKERS": "If 1st line of 401 is a speaker, set to True", + "INLINE401SPEAKERS": "Detect speaker from Name\u300cdialogue\u300d inline format in 401 lines", + "FACENAME101": "Find Speakers in 101 Codes based on Face Name", + "NAMES": "Output a list of all the character names found", + "BRFLAG": "If the game uses
instead of newlines", + "FIXTEXTWRAP": "Overwrites textwrap for better formatting", + "IGNORETLTEXT": "Ignores all translated text", + + "CODE401": "Show Text - Main dialogue content", + "CODE405": "Show Text (Scrolling) - Longer dialogue", + "CODE102": "Show Choices - Player choice options", + "CODE101": "Character Names - Turn on when names exist in 101", + "CODE408": "Comments - WARNING: Can inflate costs significantly", + "CODE122": "Control Variables - Text stored in variables", + + "CODE355655": "Scripts - Text within script commands", + "CODE357": "Picture Text - Text displayed on pictures", + "CODE657": "Picture Text Extended - Extended picture text", + "CODE356": "Plugin Commands - Plugin command parameters", + "CODE320": "Change Name Input - Name input prompts", + "CODE324": "Change Nickname - Nickname changes", + "CODE111": "Conditional Branch - Conditional text", + "CODE108": "Comments - Comment blocks" + } diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index 2f7abd5..b6a5a5e 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -130,6 +130,8 @@ BRFLAG = False FIXTEXTWRAP = True # IGNORETLTEXT: Skip Translated Text. IGNORETLTEXT = False +# PRESERVEORIGINAL: Store Japanese source in _original fields (maps + database) for re-run safety. +PRESERVEORIGINAL = False # TLSYSTEMVARIABLES: Translate System Variables. (Optional but sometimes necessary. Can break stuff.) TLSYSTEMVARIABLES = False # TLSYSTEMSWITCHES: Translate System Switches. (Optional. Translates switch names in System.json.) @@ -487,6 +489,8 @@ def _group_raw_source(codeList, group_start: int, source_parts: list[str]) -> st def _apply_original(cmd, raw_source: str) -> None: """Set scalar _original only when not already present (re-run safe).""" + if not PRESERVEORIGINAL: + return if not raw_source or not str(raw_source).strip(): return if _scalar_original(cmd) is not None: @@ -510,6 +514,8 @@ def _choice_source(cmd, index: int) -> str: def _apply_choice_original(cmd, index: int, raw_source: str) -> None: """Set _original[index] for code 102 only when that slot is empty.""" + if not PRESERVEORIGINAL: + return if not raw_source or not str(raw_source).strip(): return params = cmd.get("parameters") or [[]] @@ -581,6 +587,8 @@ def _entry_field_source(entry, field: str) -> str: def _apply_entry_field_original(entry, field: str, raw: str) -> None: """Set _original[field] only when empty and raw contains Japanese.""" + if not PRESERVEORIGINAL: + return if not isinstance(entry, dict) or not raw or not str(raw).strip(): return if not re.search(LANGREGEX, raw): @@ -622,6 +630,8 @@ def _system_scalar_source(data, field: str) -> str: def _apply_system_scalar_original(data, field: str, raw: str) -> None: """Set root _original[field] only when empty and raw contains Japanese.""" + if not PRESERVEORIGINAL: + return if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): return orig = _system_orig(data) @@ -650,6 +660,8 @@ def _system_list_source(data, list_name: str, index: int) -> str: def _apply_system_list_original(data, list_name: str, index: int, raw: str) -> None: """Set _original[list_name][str(index)] only when empty and raw contains Japanese.""" + if not PRESERVEORIGINAL: + return if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): return orig = _system_orig(data) @@ -685,6 +697,8 @@ def _system_terms_source(data, category: str, index: int) -> str: def _apply_system_terms_original(data, category: str, index: int, raw: str) -> None: """Set _original.terms[category][str(index)] only when empty and raw contains Japanese.""" + if not PRESERVEORIGINAL: + return if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): return orig = _system_orig(data) @@ -725,6 +739,8 @@ def _system_terms_message_source(data, key: str) -> str: def _apply_system_terms_message_original(data, key: str, raw: str) -> None: """Set _original.terms.messages[key] only when empty and raw contains Japanese.""" + if not PRESERVEORIGINAL: + return if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): return orig = _system_orig(data) diff --git a/set_defaults.py b/set_defaults.py index 70d2f81..7f6c24f 100644 --- a/set_defaults.py +++ b/set_defaults.py @@ -1,58 +1,59 @@ -import re -from copy import deepcopy - -# Canonical defaults used by the GUI and the `set_defaults` script. -# Stored as booleans for easier consumption by the GUI. -DEFAULTS = { - 'FIRSTLINESPEAKERS': False, - 'FACENAME101': False, - 'BRFLAG': False, - 'FIXTEXTWRAP': True, - 'IGNORETLTEXT': False, - 'JOIN408': False, - # Speakers / Dialogue / Scroll / Choices (Main Codes) - 'CODE101': True, - 'CODE401': True, - 'CODE405': True, - 'CODE102': True, - # Optional - 'CODE408': False, - # Variables - 'CODE122': False, - # Other - 'CODE355655': False, - 'CODE357': False, - 'CODE657': False, - 'CODE356': False, - 'CODE320': False, - 'CODE324': False, - 'CODE111': False, - 'CODE108': False -} - - -def get_defaults(): - """Return a shallow copy of the DEFAULTS dictionary.""" - return deepcopy(DEFAULTS) - - -def set_defaults(file_path): - """Write canonical default values into a module file. - - The module file contains assignments like `CODE401 = True`. This - function replaces those assignment lines with the canonical defaults. - """ - with open(file_path, 'r', encoding='utf-8') as file: - content = file.read() - - # Update the content with the default values (converted to Python literals) - for key, value in DEFAULTS.items(): - value_str = 'True' if value else 'False' - content = re.sub(rf'{re.escape(key)}\s*=\s*.*', f'{key} = {value_str}', content) - - with open(file_path, 'w', encoding='utf-8') as file: - file.write(content) - - -if __name__ == "__main__": - set_defaults('modules/rpgmakermvmz.py') +import re +from copy import deepcopy + +# Canonical defaults used by the GUI and the `set_defaults` script. +# Stored as booleans for easier consumption by the GUI. +DEFAULTS = { + 'FIRSTLINESPEAKERS': False, + 'FACENAME101': False, + 'BRFLAG': False, + 'FIXTEXTWRAP': True, + 'IGNORETLTEXT': False, + 'PRESERVEORIGINAL': True, + 'JOIN408': False, + # Speakers / Dialogue / Scroll / Choices (Main Codes) + 'CODE101': True, + 'CODE401': True, + 'CODE405': True, + 'CODE102': True, + # Optional + 'CODE408': False, + # Variables + 'CODE122': False, + # Other + 'CODE355655': False, + 'CODE357': False, + 'CODE657': False, + 'CODE356': False, + 'CODE320': False, + 'CODE324': False, + 'CODE111': False, + 'CODE108': False +} + + +def get_defaults(): + """Return a shallow copy of the DEFAULTS dictionary.""" + return deepcopy(DEFAULTS) + + +def set_defaults(file_path): + """Write canonical default values into a module file. + + The module file contains assignments like `CODE401 = True`. This + function replaces those assignment lines with the canonical defaults. + """ + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + + # Update the content with the default values (converted to Python literals) + for key, value in DEFAULTS.items(): + value_str = 'True' if value else 'False' + content = re.sub(rf'{re.escape(key)}\s*=\s*.*', f'{key} = {value_str}', content) + + with open(file_path, 'w', encoding='utf-8') as file: + file.write(content) + + +if __name__ == "__main__": + set_defaults('modules/rpgmakermvmz.py') diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 19af199..f10f9f7 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -1,69 +1,77 @@ -# Map `_original` fixture - -## Files - -- **`Map_original_fixture.json`** — Minimal RPG Maker MV map (17×13) with one event exercising every `_original` code path: 401, 405, 102, 101, and 122. -- **`Map_original_fixture_manifest.json`** — Expected `_original` values after translation (used by tests). - -## Event layout (event id 1, page 0) - -| Order | Code | Purpose | -|------:|------|---------| -| 1–2 | 401 | Color speaker line `\C[2]テスト子\C[0]` + following dialogue | -| 3–4 | 401 | Merged multi-line dialogue (two 401s → one batch) | -| 5–6 | 101, 405 | Empty face setup + scrolling text line | -| 7 | 102 | Three choices (middle choice has `if(...)` prefix) | -| 8–9 | 108, 408 | Choice-help marker (`選択肢ヘルプ`) + help comment line | -| 10–11 | 101, 401 | Name box with `\C[2]アリス\C[0]` + dialogue | -| 10–11 | 122 | Variable string `` `変数の中身` `` and `` `セミコロン`; `` | - -## Run the test - -Using the project venv (recommended): - -```bash -./tests/run_tests.sh -``` - -Or explicitly: - -```bash -./tests/run_tests.sh tests.test_mvmz_source_original -v -./tests/run_tests.sh tests.test_mvmz_source_original.TestFixtureMapOriginal -v -``` - -Manual equivalent (must run from project root, with venv activated): - -```bash -cd /path/to/DazedMTLTool -source .venv/bin/activate # or: source venv/bin/activate -python -m unittest tests.test_mvmz_source_original -v -``` - -Do **not** use `pytest` unless you install it yourself — this project uses the stdlib `unittest` runner. - -If you see `ModuleNotFoundError: No module named 'colorama'` (or similar), activate the venv or use `./tests/run_tests.sh` instead of system `python3`. - -Tests mock `translateAI` / `getSpeaker` — no API key required. Enable `CODE122` for the duration of the run. - -## Database `_original` fixtures - -Mini database JSON files for scalar field preservation (notes deferred to Phase 2): - -| Fixture | Parser | Fields | -|---------|--------|--------| -| `Actors_original_fixture.json` | `searchNames` | `name`, `nickname`, `profile` | -| `Items_original_fixture.json` | `searchNames` | `name`, `description` | -| `Skills_original_fixture.json` | `searchNames` | `name`, `description`, `message1` | -| `States_original_fixture.json` | `searchSS` | `name`, `description`, `message1` | -| `System_original_fixture.json` | `searchSystem` | `gameTitle`, `terms.basic[1]`, `armorTypes[1]`, `terms.messages.alwaysDash` | - -Expected `_original` shapes are in `db_original_manifest.json`. - -```bash -./tests/run_tests.sh tests.test_mvmz_db_original -v -``` - -## Manual check - -Copy the fixture into `files/` and run the tool with batch/consume or live translate, then diff against the manifest’s `expected_original` fields in `translated/Map_original_fixture.json`. +# Map `_original` fixture + +## Files + +- **`Map_original_fixture.json`** — Minimal RPG Maker MV map (17×13) with one event exercising every `_original` code path: 401, 405, 102, 101, and 122. +- **`Map_original_fixture_manifest.json`** — Expected `_original` values after translation (used by tests). + +## Event layout (event id 1, page 0) + +| Order | Code | Purpose | +|------:|------|---------| +| 1–2 | 401 | Color speaker line `\C[2]テスト子\C[0]` + following dialogue | +| 3–4 | 401 | Merged multi-line dialogue (two 401s → one batch) | +| 5–6 | 101, 405 | Empty face setup + scrolling text line | +| 7 | 102 | Three choices (middle choice has `if(...)` prefix) | +| 8–9 | 108, 408 | Choice-help marker (`選択肢ヘルプ`) + help comment line | +| 10–11 | 101, 401 | Name box with `\C[2]アリス\C[0]` + dialogue | +| 10–11 | 122 | Variable string `` `変数の中身` `` and `` `セミコロン`; `` | + +## Run the test + +Using the project venv (recommended): + +```bash +./tests/run_tests.sh +``` + +Or explicitly: + +```bash +./tests/run_tests.sh tests.test_mvmz_source_original -v +./tests/run_tests.sh tests.test_mvmz_source_original.TestFixtureMapOriginal -v +``` + +Manual equivalent (must run from project root, with venv activated): + +```bash +cd /path/to/DazedMTLTool +source .venv/bin/activate # or: source venv/bin/activate +python -m unittest tests.test_mvmz_source_original -v +``` + +Do **not** use `pytest` unless you install it yourself — this project uses the stdlib `unittest` runner. + +If you see `ModuleNotFoundError: No module named 'colorama'` (or similar), activate the venv or use `./tests/run_tests.sh` instead of system `python3`. + +Tests mock `translateAI` / `getSpeaker` — no API key required. + +### Optional `_original` config (in `modules/rpgmakermvmz.py`) + +| Flag | Scope | Default | +|------|-------|---------| +| `PRESERVEORIGINAL` | Map/event commands and database JSON | `True` | + +Set to `False` to skip writing `_original`. Existing `_original` keys are still read on re-run. + +## Database `_original` fixtures + +Mini database JSON files for scalar field preservation (notes deferred to Phase 2): + +| Fixture | Parser | Fields | +|---------|--------|--------| +| `Actors_original_fixture.json` | `searchNames` | `name`, `nickname`, `profile` | +| `Items_original_fixture.json` | `searchNames` | `name`, `description` | +| `Skills_original_fixture.json` | `searchNames` | `name`, `description`, `message1` | +| `States_original_fixture.json` | `searchSS` | `name`, `description`, `message1` | +| `System_original_fixture.json` | `searchSystem` | `gameTitle`, `terms.basic[1]`, `armorTypes[1]`, `terms.messages.alwaysDash` | + +Expected `_original` shapes are in `db_original_manifest.json`. + +```bash +./tests/run_tests.sh tests.test_mvmz_db_original -v +``` + +## Manual check + +Copy the fixture into `files/` and run the tool with batch/consume or live translate, then diff against the manifest’s `expected_original` fields in `translated/Map_original_fixture.json`. diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d141c79..c50f322 100644 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -24,7 +24,7 @@ if ! "$PYTHON" -c "import colorama, dotenv, tqdm" >/dev/null 2>&1; then fi if [[ "$#" -eq 0 ]]; then - set -- tests.test_mvmz_source_original -v + set -- discover -s tests -p 'test_mvmz_*.py' -v fi exec "$PYTHON" -m unittest "$@" diff --git a/tests/test_mvmz_db_original.py b/tests/test_mvmz_db_original.py index f1a0be7..0852505 100644 --- a/tests/test_mvmz_db_original.py +++ b/tests/test_mvmz_db_original.py @@ -1,205 +1,222 @@ -#!/usr/bin/env python3 -"""Integration tests for _original source preservation in database parsers.""" - -from __future__ import annotations - -import copy -import json -import os -import re -import sys -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -os.chdir(ROOT) -sys.path.insert(0, str(ROOT)) - -import modules.rpgmakermvmz as mvmz # noqa: E402 - -LANGREGEX = mvmz.LANGREGEX -FIXTURES = ROOT / "tests" / "fixtures" -MANIFEST = json.loads((FIXTURES / "db_original_manifest.json").read_text(encoding="utf-8")) - - -def _mock_translate(text, history, batch=False): - def tr(s): - if not isinstance(s, str): - return s - return "EN_TRANSLATED" - - if isinstance(text, list): - return [[tr(t) for t in text], [0, 0]] - return [tr(text), [0, 0]] - - -def _has_japanese(s: str) -> bool: - return bool(re.search(LANGREGEX, s or "")) - - -def _run_search_names(data, context, filename): - captured = [] - - def translate(text, history, batch=False): - captured.append(copy.deepcopy(text)) - return _mock_translate(text, history, batch) - - orig_t = mvmz.translateAI - orig_vocab = mvmz.update_vocab_section - mvmz.translateAI = translate - mvmz.update_vocab_section = lambda *args, **kwargs: None - try: - data_copy = copy.deepcopy(data) - mvmz.searchNames(data_copy, None, context, filename) - return data_copy, captured - finally: - mvmz.translateAI = orig_t - mvmz.update_vocab_section = orig_vocab - - -def _run_search_ss(state): - captured = [] - - def translate(text, history, batch=False): - captured.append(copy.deepcopy(text)) - return _mock_translate(text, history, batch) - - orig_t = mvmz.translateAI - mvmz.translateAI = translate - try: - state_copy = copy.deepcopy(state) - mvmz.searchSS(state_copy, None) - return state_copy, captured - finally: - mvmz.translateAI = orig_t - - -def _run_search_system(data): - captured = [] - - def translate(text, history, batch=False): - captured.append(copy.deepcopy(text)) - return _mock_translate(text, history, batch) - - orig_t = mvmz.translateAI - mvmz.translateAI = translate - try: - data_copy = copy.deepcopy(data) - mvmz.searchSystem(data_copy, None) - return data_copy, captured - finally: - mvmz.translateAI = orig_t - - -def _assert_batches_japanese(captured): - for payload in captured: - items = payload if isinstance(payload, list) else [payload] - for item in items: - if not isinstance(item, str) or item == "EN_TRANSLATED": - continue - if item.startswith("Taro"): - item = item[4:] - self_fail = not _has_japanese(item) - if self_fail: - raise AssertionError(f"Re-run sent non-Japanese to translateAI: {item!r}") - - -class TestActorsOriginal(unittest.TestCase): - def test_first_pass_writes_original(self): - data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8")) - result, _ = _run_search_names(data, "Actors", "Actors.json") - entry = result[MANIFEST["actors"]["entry_index"]] - expected = MANIFEST["actors"]["expected_original"] - self.assertEqual(entry.get("_original"), expected) - self.assertNotEqual(entry["name"], expected["name"]) - self.assertNotEqual(entry["nickname"], expected["nickname"]) - self.assertNotEqual(entry["profile"], expected["profile"]) - - def test_rerun_preserves_original(self): - data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8")) - result1, _ = _run_search_names(data, "Actors", "Actors.json") - orig_snapshot = copy.deepcopy(result1[1]["_original"]) - result2, captured2 = _run_search_names(result1, "Actors", "Actors.json") - self.assertEqual(result2[1]["_original"], orig_snapshot) - _assert_batches_japanese(captured2) - - -class TestItemsOriginal(unittest.TestCase): - def test_first_pass_writes_original(self): - data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8")) - result, _ = _run_search_names(data, "Items", "Items.json") - entry = result[MANIFEST["items"]["entry_index"]] - expected = MANIFEST["items"]["expected_original"] - self.assertEqual(entry.get("_original"), expected) - self.assertNotEqual(entry["name"], expected["name"]) - self.assertNotEqual(entry["description"], expected["description"]) - - def test_rerun_preserves_original(self): - data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8")) - result1, _ = _run_search_names(data, "Items", "Items.json") - orig_snapshot = copy.deepcopy(result1[1]["_original"]) - result2, captured2 = _run_search_names(result1, "Items", "Items.json") - self.assertEqual(result2[1]["_original"], orig_snapshot) - _assert_batches_japanese(captured2) - - -class TestSkillsOriginal(unittest.TestCase): - def test_first_pass_writes_original(self): - data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8")) - result, _ = _run_search_names(data, "Skills", "Skills.json") - entry = result[MANIFEST["skills"]["entry_index"]] - expected = MANIFEST["skills"]["expected_original"] - self.assertEqual(entry.get("_original"), expected) - for field, jp in expected.items(): - self.assertNotEqual(entry[field], jp) - - def test_rerun_preserves_original(self): - data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8")) - result1, _ = _run_search_names(data, "Skills", "Skills.json") - orig_snapshot = copy.deepcopy(result1[1]["_original"]) - result2, captured2 = _run_search_names(result1, "Skills", "Items.json") - self.assertEqual(result2[1]["_original"], orig_snapshot) - _assert_batches_japanese(captured2) - - -class TestStatesOriginal(unittest.TestCase): - def test_first_pass_writes_original(self): - data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8")) - state = data[MANIFEST["states"]["entry_index"]] - result, _ = _run_search_ss(state) - expected = MANIFEST["states"]["expected_original"] - self.assertEqual(result.get("_original"), expected) - for field, jp in expected.items(): - self.assertNotEqual(result[field], jp) - - def test_rerun_preserves_original(self): - data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8")) - state = data[MANIFEST["states"]["entry_index"]] - result1, _ = _run_search_ss(state) - orig_snapshot = copy.deepcopy(result1["_original"]) - result2, captured2 = _run_search_ss(result1) - self.assertEqual(result2["_original"], orig_snapshot) - _assert_batches_japanese(captured2) - - -class TestSystemOriginal(unittest.TestCase): - def test_first_pass_writes_original(self): - data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8")) - result, _ = _run_search_system(data) - expected = MANIFEST["system"]["expected_original"] - self.assertEqual(result.get("_original"), expected) - self.assertNotEqual(result["gameTitle"], expected["gameTitle"]) - self.assertNotEqual(result["terms"]["basic"][1], expected["terms"]["basic"]["1"]) - self.assertNotEqual(result["armorTypes"][1], expected["armorTypes"]["1"]) - - def test_rerun_preserves_original(self): - data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8")) - result1, _ = _run_search_system(data) - orig_snapshot = copy.deepcopy(result1["_original"]) - result2, captured2 = _run_search_system(result1) - self.assertEqual(result2["_original"], orig_snapshot) - _assert_batches_japanese(captured2) - - -if __name__ == "__main__": - unittest.main(verbosity=2) +#!/usr/bin/env python3 +"""Integration tests for _original source preservation in database parsers.""" + +from __future__ import annotations + +import copy +import json +import os +import re +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +os.chdir(ROOT) +sys.path.insert(0, str(ROOT)) + +import modules.rpgmakermvmz as mvmz # noqa: E402 + +LANGREGEX = mvmz.LANGREGEX +FIXTURES = ROOT / "tests" / "fixtures" +MANIFEST = json.loads((FIXTURES / "db_original_manifest.json").read_text(encoding="utf-8")) + + +def _mock_translate(text, history, batch=False): + def tr(s): + if not isinstance(s, str): + return s + return "EN_TRANSLATED" + + if isinstance(text, list): + return [[tr(t) for t in text], [0, 0]] + return [tr(text), [0, 0]] + + +def _has_japanese(s: str) -> bool: + return bool(re.search(LANGREGEX, s or "")) + + +def _run_search_names(data, context, filename, *, preserve_original=True): + captured = [] + + def translate(text, history, batch=False): + captured.append(copy.deepcopy(text)) + return _mock_translate(text, history, batch) + + orig_t = mvmz.translateAI + orig_vocab = mvmz.update_vocab_section + orig_preserve = mvmz.PRESERVEORIGINAL + mvmz.translateAI = translate + mvmz.update_vocab_section = lambda *args, **kwargs: None + mvmz.PRESERVEORIGINAL = preserve_original + try: + data_copy = copy.deepcopy(data) + mvmz.searchNames(data_copy, None, context, filename) + return data_copy, captured + finally: + mvmz.translateAI = orig_t + mvmz.update_vocab_section = orig_vocab + mvmz.PRESERVEORIGINAL = orig_preserve + + +def _run_search_ss(state): + captured = [] + + def translate(text, history, batch=False): + captured.append(copy.deepcopy(text)) + return _mock_translate(text, history, batch) + + orig_t = mvmz.translateAI + orig_preserve = mvmz.PRESERVEORIGINAL + mvmz.translateAI = translate + mvmz.PRESERVEORIGINAL = True + try: + state_copy = copy.deepcopy(state) + mvmz.searchSS(state_copy, None) + return state_copy, captured + finally: + mvmz.translateAI = orig_t + mvmz.PRESERVEORIGINAL = orig_preserve + + +def _run_search_system(data): + captured = [] + + def translate(text, history, batch=False): + captured.append(copy.deepcopy(text)) + return _mock_translate(text, history, batch) + + orig_t = mvmz.translateAI + orig_preserve = mvmz.PRESERVEORIGINAL + mvmz.translateAI = translate + mvmz.PRESERVEORIGINAL = True + try: + data_copy = copy.deepcopy(data) + mvmz.searchSystem(data_copy, None) + return data_copy, captured + finally: + mvmz.translateAI = orig_t + mvmz.PRESERVEORIGINAL = orig_preserve + + +def _assert_batches_japanese(captured): + for payload in captured: + items = payload if isinstance(payload, list) else [payload] + for item in items: + if not isinstance(item, str) or item == "EN_TRANSLATED": + continue + if item.startswith("Taro"): + item = item[4:] + self_fail = not _has_japanese(item) + if self_fail: + raise AssertionError(f"Re-run sent non-Japanese to translateAI: {item!r}") + + +class TestActorsOriginal(unittest.TestCase): + def test_first_pass_writes_original(self): + data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8")) + result, _ = _run_search_names(data, "Actors", "Actors.json") + entry = result[MANIFEST["actors"]["entry_index"]] + expected = MANIFEST["actors"]["expected_original"] + self.assertEqual(entry.get("_original"), expected) + self.assertNotEqual(entry["name"], expected["name"]) + self.assertNotEqual(entry["nickname"], expected["nickname"]) + self.assertNotEqual(entry["profile"], expected["profile"]) + + def test_rerun_preserves_original(self): + data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8")) + result1, _ = _run_search_names(data, "Actors", "Actors.json") + orig_snapshot = copy.deepcopy(result1[1]["_original"]) + result2, captured2 = _run_search_names(result1, "Actors", "Actors.json") + self.assertEqual(result2[1]["_original"], orig_snapshot) + _assert_batches_japanese(captured2) + + +class TestItemsOriginal(unittest.TestCase): + def test_first_pass_writes_original(self): + data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8")) + result, _ = _run_search_names(data, "Items", "Items.json") + entry = result[MANIFEST["items"]["entry_index"]] + expected = MANIFEST["items"]["expected_original"] + self.assertEqual(entry.get("_original"), expected) + self.assertNotEqual(entry["name"], expected["name"]) + self.assertNotEqual(entry["description"], expected["description"]) + + def test_rerun_preserves_original(self): + data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8")) + result1, _ = _run_search_names(data, "Items", "Items.json") + orig_snapshot = copy.deepcopy(result1[1]["_original"]) + result2, captured2 = _run_search_names(result1, "Items", "Items.json") + self.assertEqual(result2[1]["_original"], orig_snapshot) + _assert_batches_japanese(captured2) + + +class TestSkillsOriginal(unittest.TestCase): + def test_first_pass_writes_original(self): + data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8")) + result, _ = _run_search_names(data, "Skills", "Skills.json") + entry = result[MANIFEST["skills"]["entry_index"]] + expected = MANIFEST["skills"]["expected_original"] + self.assertEqual(entry.get("_original"), expected) + for field, jp in expected.items(): + self.assertNotEqual(entry[field], jp) + + def test_rerun_preserves_original(self): + data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8")) + result1, _ = _run_search_names(data, "Skills", "Skills.json") + orig_snapshot = copy.deepcopy(result1[1]["_original"]) + result2, captured2 = _run_search_names(result1, "Skills", "Items.json") + self.assertEqual(result2[1]["_original"], orig_snapshot) + _assert_batches_japanese(captured2) + + +class TestStatesOriginal(unittest.TestCase): + def test_first_pass_writes_original(self): + data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8")) + state = data[MANIFEST["states"]["entry_index"]] + result, _ = _run_search_ss(state) + expected = MANIFEST["states"]["expected_original"] + self.assertEqual(result.get("_original"), expected) + for field, jp in expected.items(): + self.assertNotEqual(result[field], jp) + + def test_rerun_preserves_original(self): + data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8")) + state = data[MANIFEST["states"]["entry_index"]] + result1, _ = _run_search_ss(state) + orig_snapshot = copy.deepcopy(result1["_original"]) + result2, captured2 = _run_search_ss(result1) + self.assertEqual(result2["_original"], orig_snapshot) + _assert_batches_japanese(captured2) + + +class TestSystemOriginal(unittest.TestCase): + def test_first_pass_writes_original(self): + data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8")) + result, _ = _run_search_system(data) + expected = MANIFEST["system"]["expected_original"] + self.assertEqual(result.get("_original"), expected) + self.assertNotEqual(result["gameTitle"], expected["gameTitle"]) + self.assertNotEqual(result["terms"]["basic"][1], expected["terms"]["basic"]["1"]) + self.assertNotEqual(result["armorTypes"][1], expected["armorTypes"]["1"]) + + def test_rerun_preserves_original(self): + data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8")) + result1, _ = _run_search_system(data) + orig_snapshot = copy.deepcopy(result1["_original"]) + result2, captured2 = _run_search_system(result1) + self.assertEqual(result2["_original"], orig_snapshot) + _assert_batches_japanese(captured2) + + +class TestPreserveOriginalDisabled(unittest.TestCase): + def test_db_preserve_disabled_skips_original(self): + data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8")) + result, _ = _run_search_names(data, "Items", "Items.json", preserve_original=False) + entry = result[MANIFEST["items"]["entry_index"]] + self.assertNotIn("_original", entry) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_mvmz_source_original.py b/tests/test_mvmz_source_original.py index 5cd7309..b3bc091 100644 --- a/tests/test_mvmz_source_original.py +++ b/tests/test_mvmz_source_original.py @@ -1,363 +1,391 @@ -#!/usr/bin/env python3 -"""Integration tests for _original source preservation in rpgmakermvmz searchCodes.""" - -from __future__ import annotations - -import copy -import json -import os -import re -import sys -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -os.chdir(ROOT) -sys.path.insert(0, str(ROOT)) - -import modules.rpgmakermvmz as mvmz # noqa: E402 - -LANGREGEX = mvmz.LANGREGEX - - -def _mock_translate(text, history, batch=False): - def tr(s): - if not isinstance(s, str): - return s - m = re.match(r"^(\[[^\]]+\]:\s?)", s) - if m: - return m.group(1) + "EN_TRANSLATED" - return "EN_TRANSLATED" - - if isinstance(text, list): - return [[tr(t) for t in text], [0, 0]] - return [tr(text), [0, 0]] - - -def _mock_speaker(name): - return [f"Speaker_{name}", [0, 0]] - - -FIXTURE_MAP = ROOT / "tests" / "fixtures" / "Map_original_fixture.json" -FIXTURE_MANIFEST = ROOT / "tests" / "fixtures" / "Map_original_fixture_manifest.json" -CASE_MARKER_RE = re.compile(r"# CASE:(\S+)") - - -def _load_fixture_page(): - data = json.loads(FIXTURE_MAP.read_text(encoding="utf-8-sig")) - event = next(e for e in data["events"] if e and e.get("id") == 1) - return {"list": copy.deepcopy(event["pages"][0]["list"])} - - -def _case_commands(page_list): - """Map manifest case id -> command immediately following its 108 marker.""" - cases = {} - pending = None - for cmd in page_list: - if not cmd: - continue - if cmd.get("code") == 108: - m = CASE_MARKER_RE.search(str(cmd.get("parameters", [""])[0])) - pending = m.group(1) if m else None - continue - if pending: - cases[pending] = cmd - pending = None - return cases - - -def _load_fixture_manifest(): - return json.loads(FIXTURE_MANIFEST.read_text(encoding="utf-8")) - - -def _load_map_excerpt(): - """Small real snippets from Map002 event 17 + Map001 102 + synthetic 101/122.""" - map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig")) - ev17 = next(e for e in map2["events"] if e and e.get("id") == 17) - real = copy.deepcopy(ev17["pages"][0]["list"][7:14]) # 101 + 401 dialogue block - - map1 = json.loads((ROOT / "files" / "Map001.json").read_text(encoding="utf-8-sig")) - choice_cmd = None - for ev in map1["events"]: - if not ev: - continue - for pg in ev.get("pages") or []: - if not pg: - continue - for cmd in pg.get("list") or []: - if cmd and cmd.get("code") == 102: - choice_cmd = copy.deepcopy(cmd) - break - if choice_cmd: - break - if choice_cmd: - break - assert choice_cmd is not None, "Map001 should contain a 102 choice command" - - synthetic = [ - {"code": 101, "indent": 0, "parameters": ["", 0, 0, 2, "\\C[2]アリス\\C[0]"]}, - {"code": 401, "indent": 0, "parameters": ["こんにちは、世界。"]}, - { - "code": 122, - "indent": 0, - "parameters": [101, 101, 0, 0, "`変数テスト`"], - }, - ] - - return {"list": real + [choice_cmd] + synthetic} - - -def _resolve_case_command(page_list, entry, marked_cases=None): - """Find manifest case command by CASE marker or by code + expected _original.""" - marked_cases = marked_cases if marked_cases is not None else _case_commands(page_list) - cid = entry["id"] - cmd = marked_cases.get(cid) - if cmd is not None and cmd.get("code") == entry.get("code"): - return cmd - exp = entry.get("expected_original") - code = entry.get("code") - if code is not None and exp is not None: - for candidate in page_list: - if candidate and candidate.get("code") == code and candidate.get("_original") == exp: - return candidate - return cmd - - -def _run_search_codes(page): - """Full Pass 1 -> mock translate -> Pass 2 cycle.""" - captured = [] - - def translate(text, history, batch=False): - captured.append(copy.deepcopy(text)) - return _mock_translate(text, history, batch) - - def speaker(name): - return _mock_speaker(name) - - orig_t = mvmz.translateAI - orig_s = mvmz.getSpeaker - orig_122 = mvmz.CODE122 - orig_408 = mvmz.CODE408 - mvmz.translateAI = translate - mvmz.getSpeaker = speaker - mvmz.CODE122 = True - mvmz.CODE408 = True - try: - page_copy = copy.deepcopy(page) - mvmz.searchCodes(page_copy, None, [], "TestMap.json") - return page_copy, captured - finally: - mvmz.translateAI = orig_t - mvmz.getSpeaker = orig_s - mvmz.CODE122 = orig_122 - mvmz.CODE408 = orig_408 - - -def _find_commands(page, code): - return [cmd for cmd in page["list"] if cmd and cmd.get("code") == code] - - -def _has_japanese(s: str) -> bool: - return bool(re.search(LANGREGEX, s or "")) - - -class TestMVMZSourceOriginal(unittest.TestCase): - def test_first_pass_writes_original(self): - page, _ = _run_search_codes(_load_map_excerpt()) - - cmds401 = _find_commands(page, 401) - with_orig = [c for c in cmds401 if c.get("_original")] - self.assertGreater(len(with_orig), 0, "401 dialogue should have _original") - for c in with_orig: - self.assertTrue(_has_japanese(c["_original"])) - self.assertNotEqual(c["parameters"][0], c["_original"]) - - c102 = _find_commands(page, 102)[0] - self.assertIsInstance(c102.get("_original"), list) - self.assertEqual(len(c102["_original"]), len(c102["parameters"][0])) - for i, orig in enumerate(c102["_original"]): - if orig: - self.assertTrue(_has_japanese(orig), f"choice {i} _original should be Japanese") - self.assertNotEqual(c102["parameters"][0][i], orig) - - c101 = next(c for c in _find_commands(page, 101) if len(c.get("parameters", [])) > 4) - self.assertIn("_original", c101) - self.assertIn("アリス", c101["_original"]) - self.assertIn("\\C[2]", c101["_original"]) - self.assertIn("Speaker_", c101["parameters"][4]) - - c122 = _find_commands(page, 122)[0] - self.assertEqual(c122["_original"], "変数テスト") - self.assertIn("EN_TRANSLATED", c122["parameters"][4]) - - def test_rerun_uses_original_not_display_text(self): - page1, captured1 = _run_search_codes(_load_map_excerpt()) - originals_snapshot = json.dumps( - {i: cmd.get("_original") for i, cmd in enumerate(page1["list"]) if cmd}, - ensure_ascii=False, - ) - - page2, captured2 = _run_search_codes(page1) - originals_after = json.dumps( - {i: cmd.get("_original") for i, cmd in enumerate(page2["list"]) if cmd}, - ensure_ascii=False, - ) - self.assertEqual(originals_snapshot, originals_after, "_original must not change on re-run") - - # Every batch sent to translateAI on re-run should still contain Japanese - for payload in captured2: - items = payload if isinstance(payload, list) else [payload] - for item in items: - if not isinstance(item, str): - continue - if item == "EN_TRANSLATED": - continue - if _has_japanese(item): - continue - if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item): - continue - self.fail(f"Re-run sent non-Japanese to translateAI: {item!r}") - - def test_map002_micro_page_real_data(self): - """Translate a tiny slice of Map002 event 17 only (7 commands).""" - map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig")) - ev17 = next(e for e in map2["events"] if e and e.get("id") == 17) - micro = {"list": copy.deepcopy(ev17["pages"][0]["list"][8:11])} # 3x401 only - - page, _ = _run_search_codes(micro) - c401 = _find_commands(page, 401) - self.assertGreaterEqual(len(c401), 1) - with_orig = [c for c in c401 if c.get("_original")] - self.assertGreaterEqual(len(with_orig), 1) - for c in with_orig: - self.assertTrue(_has_japanese(c["_original"])) - - def test_speaker_color_line_full_original(self): - """Standalone \\C[n]Name\\C[n] speaker 401 lines keep the full string in _original.""" - page = { - "list": [ - {"code": 401, "indent": 0, "parameters": ["\\C[2]エルーシャ\\C[0]"]}, - {"code": 401, "indent": 0, "parameters": ["「テストセリフ」"]}, - ] - } - page, _ = _run_search_codes(page) - speaker_cmd = page["list"][0] - self.assertEqual(speaker_cmd.get("_original"), "\\C[2]エルーシャ\\C[0]") - self.assertIn("\\C[2]", speaker_cmd["parameters"][0]) - self.assertIn("Speaker_エルーシャ", speaker_cmd["parameters"][0]) - - # Re-run: _original unchanged, getSpeaker still receives Japanese name - speakers_seen = [] - - def speaker(name): - speakers_seen.append(name) - return _mock_speaker(name) - - orig_t, orig_s = mvmz.translateAI, mvmz.getSpeaker - mvmz.getSpeaker = speaker - mvmz.translateAI = lambda text, history, batch=False: _mock_translate(text, history, batch) - try: - mvmz.searchCodes(page, None, [], "TestMap.json") - finally: - mvmz.getSpeaker = orig_s - mvmz.translateAI = orig_t - self.assertEqual(speaker_cmd["_original"], "\\C[2]エルーシャ\\C[0]") - self.assertIn("エルーシャ", speakers_seen) - - def test_405_split_rerun_uses_anchor_original_only(self): - """English 405 siblings after a split must not pollute re-run source.""" - page = { - "list": [ - { - "code": 405, - "indent": 0, - "parameters": ["EN_LINE_1"], - "_original": "第一行\n第二行", - }, - {"code": 405, "indent": 0, "parameters": ["EN_LINE_2"]}, - ] - } - _, captured = _run_search_codes(page) - self.assertGreater(len(captured), 0) - payloads = captured if isinstance(captured[0], str) else captured - for payload in payloads: - if not isinstance(payload, str): - continue - self.assertIn("第一行", payload) - self.assertNotIn("EN_LINE_1", payload) - self.assertNotIn("EN_LINE_2", payload) - - - def test_408_choice_help_original(self): - page = { - "list": [ - {"code": 108, "indent": 0, "parameters": ["選択肢ヘルプ"]}, - {"code": 408, "indent": 0, "parameters": ["これは選択肢のヘルプです。"]}, - ] - } - page, captured = _run_search_codes(page) - cmd = _find_commands(page, 408)[0] - self.assertEqual(cmd.get("_original"), "これは選択肢のヘルプです。") - self.assertNotEqual(cmd["parameters"][0], cmd["_original"]) - self.assertGreater(len(captured), 0) - - page2, captured2 = _run_search_codes(page) - self.assertEqual(cmd["_original"], _find_commands(page2, 408)[0]["_original"]) - for payload in captured2: - items = payload if isinstance(payload, list) else [payload] - for item in items: - if not isinstance(item, str) or item == "EN_TRANSLATED": - continue - self.assertTrue(_has_japanese(item), f"408 re-run sent non-Japanese: {item!r}") - - -class TestFixtureMapOriginal(unittest.TestCase): - """Full fixture map covering every _original preservation code path.""" - - def test_fixture_all_cases_original(self): - page, _ = _run_search_codes(_load_fixture_page()) - marked = _case_commands(page["list"]) - manifest = _load_fixture_manifest() - - for entry in manifest["cases"]: - cid = entry["id"] - cmd = _resolve_case_command(page["list"], entry, marked) - expected = entry["expected_original"] - with self.subTest(case=cid): - self.assertIsNotNone(cmd, f"could not resolve fixture case {cid}") - self.assertEqual(cmd.get("_original"), expected, entry.get("summary", cid)) - if isinstance(expected, str): - self.assertTrue(_has_japanese(expected)) - if cmd.get("parameters"): - display = cmd["parameters"][0] - if isinstance(display, str): - self.assertNotEqual(display, expected) - - def test_fixture_rerun_preserves_original(self): - page1, _ = _run_search_codes(_load_fixture_page()) - page2, captured2 = _run_search_codes(page1) - manifest = _load_fixture_manifest() - - for entry in manifest["cases"]: - cid = entry["id"] - cmd1 = _resolve_case_command(page1["list"], entry) - cmd2 = _resolve_case_command(page2["list"], entry) - with self.subTest(case=cid): - self.assertIsNotNone(cmd1) - self.assertIsNotNone(cmd2) - self.assertEqual(cmd1.get("_original"), cmd2.get("_original")) - - for payload in captured2: - items = payload if isinstance(payload, list) else [payload] - for item in items: - if not isinstance(item, str) or item == "EN_TRANSLATED": - continue - if _has_japanese(item): - continue - if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item): - continue - self.fail(f"Fixture re-run sent non-Japanese to translateAI: {item!r}") - - -if __name__ == "__main__": - unittest.main(verbosity=2) +#!/usr/bin/env python3 +"""Integration tests for _original source preservation in rpgmakermvmz searchCodes.""" + +from __future__ import annotations + +import copy +import json +import os +import re +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +os.chdir(ROOT) +sys.path.insert(0, str(ROOT)) + +import modules.rpgmakermvmz as mvmz # noqa: E402 + +LANGREGEX = mvmz.LANGREGEX + + +def _mock_translate(text, history, batch=False): + def tr(s): + if not isinstance(s, str): + return s + m = re.match(r"^(\[[^\]]+\]:\s?)", s) + if m: + return m.group(1) + "EN_TRANSLATED" + return "EN_TRANSLATED" + + if isinstance(text, list): + return [[tr(t) for t in text], [0, 0]] + return [tr(text), [0, 0]] + + +def _mock_speaker(name): + return [f"Speaker_{name}", [0, 0]] + + +FIXTURE_MAP = ROOT / "tests" / "fixtures" / "Map_original_fixture.json" +FIXTURE_MANIFEST = ROOT / "tests" / "fixtures" / "Map_original_fixture_manifest.json" +CASE_MARKER_RE = re.compile(r"# CASE:(\S+)") + + +def _load_fixture_page(): + data = json.loads(FIXTURE_MAP.read_text(encoding="utf-8-sig")) + event = next(e for e in data["events"] if e and e.get("id") == 1) + return {"list": copy.deepcopy(event["pages"][0]["list"])} + + +def _case_commands(page_list): + """Map manifest case id -> command immediately following its 108 marker.""" + cases = {} + pending = None + for cmd in page_list: + if not cmd: + continue + if cmd.get("code") == 108: + m = CASE_MARKER_RE.search(str(cmd.get("parameters", [""])[0])) + pending = m.group(1) if m else None + continue + if pending: + cases[pending] = cmd + pending = None + return cases + + +def _load_fixture_manifest(): + return json.loads(FIXTURE_MANIFEST.read_text(encoding="utf-8")) + + +def _load_map_excerpt(): + """Small real snippets from Map002 event 17 + Map001 102 + synthetic 101/122.""" + map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig")) + ev17 = next(e for e in map2["events"] if e and e.get("id") == 17) + real = copy.deepcopy(ev17["pages"][0]["list"][7:14]) # 101 + 401 dialogue block + + map1 = json.loads((ROOT / "files" / "Map001.json").read_text(encoding="utf-8-sig")) + choice_cmd = None + for ev in map1["events"]: + if not ev: + continue + for pg in ev.get("pages") or []: + if not pg: + continue + for cmd in pg.get("list") or []: + if cmd and cmd.get("code") == 102: + choice_cmd = copy.deepcopy(cmd) + break + if choice_cmd: + break + if choice_cmd: + break + assert choice_cmd is not None, "Map001 should contain a 102 choice command" + + synthetic = [ + {"code": 101, "indent": 0, "parameters": ["", 0, 0, 2, "\\C[2]アリス\\C[0]"]}, + {"code": 401, "indent": 0, "parameters": ["こんにちは、世界。"]}, + { + "code": 122, + "indent": 0, + "parameters": [101, 101, 0, 0, "`変数テスト`"], + }, + ] + + return {"list": real + [choice_cmd] + synthetic} + + +def _resolve_case_command(page_list, entry, marked_cases=None): + """Find manifest case command by CASE marker or by code + expected _original.""" + marked_cases = marked_cases if marked_cases is not None else _case_commands(page_list) + cid = entry["id"] + cmd = marked_cases.get(cid) + if cmd is not None and cmd.get("code") == entry.get("code"): + return cmd + exp = entry.get("expected_original") + code = entry.get("code") + if code is not None and exp is not None: + for candidate in page_list: + if candidate and candidate.get("code") == code and candidate.get("_original") == exp: + return candidate + return cmd + + +def _run_search_codes(page, *, preserve_original=True): + """Full Pass 1 -> mock translate -> Pass 2 cycle.""" + captured = [] + + def translate(text, history, batch=False): + captured.append(copy.deepcopy(text)) + return _mock_translate(text, history, batch) + + def speaker(name): + return _mock_speaker(name) + + orig_t = mvmz.translateAI + orig_s = mvmz.getSpeaker + orig_122 = mvmz.CODE122 + orig_408 = mvmz.CODE408 + orig_101 = mvmz.CODE101 + orig_401 = mvmz.CODE401 + orig_405 = mvmz.CODE405 + orig_102 = mvmz.CODE102 + orig_preserve = mvmz.PRESERVEORIGINAL + mvmz.translateAI = translate + mvmz.getSpeaker = speaker + mvmz.CODE122 = True + mvmz.CODE408 = True + mvmz.CODE101 = True + mvmz.CODE401 = True + mvmz.CODE405 = True + mvmz.CODE102 = True + mvmz.PRESERVEORIGINAL = preserve_original + try: + page_copy = copy.deepcopy(page) + mvmz.searchCodes(page_copy, None, [], "TestMap.json") + return page_copy, captured + finally: + mvmz.translateAI = orig_t + mvmz.getSpeaker = orig_s + mvmz.CODE122 = orig_122 + mvmz.CODE408 = orig_408 + mvmz.CODE101 = orig_101 + mvmz.CODE401 = orig_401 + mvmz.CODE405 = orig_405 + mvmz.CODE102 = orig_102 + mvmz.PRESERVEORIGINAL = orig_preserve + + +def _find_commands(page, code): + return [cmd for cmd in page["list"] if cmd and cmd.get("code") == code] + + +def _has_japanese(s: str) -> bool: + return bool(re.search(LANGREGEX, s or "")) + + +class TestMVMZSourceOriginal(unittest.TestCase): + def test_first_pass_writes_original(self): + page, _ = _run_search_codes(_load_map_excerpt()) + + cmds401 = _find_commands(page, 401) + with_orig = [c for c in cmds401 if c.get("_original")] + self.assertGreater(len(with_orig), 0, "401 dialogue should have _original") + for c in with_orig: + self.assertTrue(_has_japanese(c["_original"])) + self.assertNotEqual(c["parameters"][0], c["_original"]) + + c102 = _find_commands(page, 102)[0] + self.assertIsInstance(c102.get("_original"), list) + self.assertEqual(len(c102["_original"]), len(c102["parameters"][0])) + for i, orig in enumerate(c102["_original"]): + if orig: + self.assertTrue(_has_japanese(orig), f"choice {i} _original should be Japanese") + self.assertNotEqual(c102["parameters"][0][i], orig) + + c101 = next(c for c in _find_commands(page, 101) if len(c.get("parameters", [])) > 4) + self.assertIn("_original", c101) + self.assertIn("アリス", c101["_original"]) + self.assertIn("\\C[2]", c101["_original"]) + self.assertIn("Speaker_", c101["parameters"][4]) + + c122 = _find_commands(page, 122)[0] + self.assertEqual(c122["_original"], "変数テスト") + self.assertIn("EN_TRANSLATED", c122["parameters"][4]) + + def test_rerun_uses_original_not_display_text(self): + page1, captured1 = _run_search_codes(_load_map_excerpt()) + originals_snapshot = json.dumps( + {i: cmd.get("_original") for i, cmd in enumerate(page1["list"]) if cmd}, + ensure_ascii=False, + ) + + page2, captured2 = _run_search_codes(page1) + originals_after = json.dumps( + {i: cmd.get("_original") for i, cmd in enumerate(page2["list"]) if cmd}, + ensure_ascii=False, + ) + self.assertEqual(originals_snapshot, originals_after, "_original must not change on re-run") + + # Every batch sent to translateAI on re-run should still contain Japanese + for payload in captured2: + items = payload if isinstance(payload, list) else [payload] + for item in items: + if not isinstance(item, str): + continue + if item == "EN_TRANSLATED": + continue + if _has_japanese(item): + continue + if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item): + continue + self.fail(f"Re-run sent non-Japanese to translateAI: {item!r}") + + def test_map002_micro_page_real_data(self): + """Translate a tiny slice of Map002 event 17 only (7 commands).""" + map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig")) + ev17 = next(e for e in map2["events"] if e and e.get("id") == 17) + micro = {"list": copy.deepcopy(ev17["pages"][0]["list"][8:11])} # 3x401 only + + page, _ = _run_search_codes(micro) + c401 = _find_commands(page, 401) + self.assertGreaterEqual(len(c401), 1) + with_orig = [c for c in c401 if c.get("_original")] + self.assertGreaterEqual(len(with_orig), 1) + for c in with_orig: + self.assertTrue(_has_japanese(c["_original"])) + + def test_speaker_color_line_full_original(self): + """Standalone \\C[n]Name\\C[n] speaker 401 lines keep the full string in _original.""" + page = { + "list": [ + {"code": 401, "indent": 0, "parameters": ["\\C[2]エルーシャ\\C[0]"]}, + {"code": 401, "indent": 0, "parameters": ["「テストセリフ」"]}, + ] + } + page, _ = _run_search_codes(page) + speaker_cmd = page["list"][0] + self.assertEqual(speaker_cmd.get("_original"), "\\C[2]エルーシャ\\C[0]") + self.assertIn("\\C[2]", speaker_cmd["parameters"][0]) + self.assertIn("Speaker_エルーシャ", speaker_cmd["parameters"][0]) + + # Re-run: _original unchanged, getSpeaker still receives Japanese name + speakers_seen = [] + + def speaker(name): + speakers_seen.append(name) + return _mock_speaker(name) + + orig_t, orig_s = mvmz.translateAI, mvmz.getSpeaker + orig_401 = mvmz.CODE401 + mvmz.getSpeaker = speaker + mvmz.CODE401 = True + mvmz.translateAI = lambda text, history, batch=False: _mock_translate(text, history, batch) + try: + mvmz.searchCodes(page, None, [], "TestMap.json") + finally: + mvmz.getSpeaker = orig_s + mvmz.translateAI = orig_t + mvmz.CODE401 = orig_401 + self.assertEqual(speaker_cmd["_original"], "\\C[2]エルーシャ\\C[0]") + self.assertIn("エルーシャ", speakers_seen) + + def test_405_split_rerun_uses_anchor_original_only(self): + """English 405 siblings after a split must not pollute re-run source.""" + page = { + "list": [ + { + "code": 405, + "indent": 0, + "parameters": ["EN_LINE_1"], + "_original": "第一行\n第二行", + }, + {"code": 405, "indent": 0, "parameters": ["EN_LINE_2"]}, + ] + } + _, captured = _run_search_codes(page) + self.assertGreater(len(captured), 0) + payloads = captured if isinstance(captured[0], str) else captured + for payload in payloads: + if not isinstance(payload, str): + continue + self.assertIn("第一行", payload) + self.assertNotIn("EN_LINE_1", payload) + self.assertNotIn("EN_LINE_2", payload) + + + def test_408_choice_help_original(self): + page = { + "list": [ + {"code": 108, "indent": 0, "parameters": ["選択肢ヘルプ"]}, + {"code": 408, "indent": 0, "parameters": ["これは選択肢のヘルプです。"]}, + ] + } + page, captured = _run_search_codes(page) + cmd = _find_commands(page, 408)[0] + self.assertEqual(cmd.get("_original"), "これは選択肢のヘルプです。") + self.assertNotEqual(cmd["parameters"][0], cmd["_original"]) + self.assertGreater(len(captured), 0) + + page2, captured2 = _run_search_codes(page) + self.assertEqual(cmd["_original"], _find_commands(page2, 408)[0]["_original"]) + for payload in captured2: + items = payload if isinstance(payload, list) else [payload] + for item in items: + if not isinstance(item, str) or item == "EN_TRANSLATED": + continue + self.assertTrue(_has_japanese(item), f"408 re-run sent non-Japanese: {item!r}") + + +class TestFixtureMapOriginal(unittest.TestCase): + """Full fixture map covering every _original preservation code path.""" + + def test_fixture_all_cases_original(self): + page, _ = _run_search_codes(_load_fixture_page()) + marked = _case_commands(page["list"]) + manifest = _load_fixture_manifest() + + for entry in manifest["cases"]: + cid = entry["id"] + cmd = _resolve_case_command(page["list"], entry, marked) + expected = entry["expected_original"] + with self.subTest(case=cid): + self.assertIsNotNone(cmd, f"could not resolve fixture case {cid}") + self.assertEqual(cmd.get("_original"), expected, entry.get("summary", cid)) + if isinstance(expected, str): + self.assertTrue(_has_japanese(expected)) + if cmd.get("parameters"): + display = cmd["parameters"][0] + if isinstance(display, str): + self.assertNotEqual(display, expected) + + def test_fixture_rerun_preserves_original(self): + page1, _ = _run_search_codes(_load_fixture_page()) + page2, captured2 = _run_search_codes(page1) + manifest = _load_fixture_manifest() + + for entry in manifest["cases"]: + cid = entry["id"] + cmd1 = _resolve_case_command(page1["list"], entry) + cmd2 = _resolve_case_command(page2["list"], entry) + with self.subTest(case=cid): + self.assertIsNotNone(cmd1) + self.assertIsNotNone(cmd2) + self.assertEqual(cmd1.get("_original"), cmd2.get("_original")) + + for payload in captured2: + items = payload if isinstance(payload, list) else [payload] + for item in items: + if not isinstance(item, str) or item == "EN_TRANSLATED": + continue + if _has_japanese(item): + continue + if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item): + continue + self.fail(f"Fixture re-run sent non-Japanese to translateAI: {item!r}") + + def test_preserve_original_codes_disabled(self): + """PRESERVEORIGINAL=False must not write _original on map commands.""" + page = { + "list": [ + {"code": 401, "indent": 0, "parameters": ["こんにちは"]}, + ] + } + page, _ = _run_search_codes(page, preserve_original=False) + self.assertNotIn("_original", page["list"][0]) + + +if __name__ == "__main__": + unittest.main(verbosity=2)