DazedTL/tests/test_mvmz_speaker_vocab.py
DazedAnon 2e7f816ecd fix(rpgmaker): enforce glossary spellings for speaker names
- Prefer curated character names over cached or generated translations
- Preserve name spellings inside compound labels and parsed speakers
- Reload vocab before runs and refine default event-code handling
- Add regression coverage for glossary precedence and matching
2026-07-25 14:01:04 -05:00

131 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Regression tests for deterministic RPG Maker speaker glossary handling."""
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
import modules.rpgmakermvmz as mvmz
from util.translation import buildMatchedVocabText, parseVocabWithCategories
VOCAB = (
"# Game Characters\n"
"ユウ (Yuu) - Male protagonist.\n"
"ハルカ (Haruka) - Female heroine.\n\n"
"# Terms\n"
"教室 (Classroom)\n"
)
class MVMZSpeakerVocabTests(unittest.TestCase):
def setUp(self):
self.original_vocab = mvmz.VOCAB
self.original_config_vocab = mvmz.TRANSLATION_CONFIG.vocab
self.original_names = mvmz.NAMESLIST
self.original_collected = mvmz.SPEAKER_COLLECTED
self.original_parse_mode = mvmz.SPEAKER_PARSE_MODE
self.original_preflight = mvmz.PREFLIGHT_COUNT_MODE
mvmz.VOCAB = VOCAB
mvmz.NAMESLIST = []
mvmz.SPEAKER_COLLECTED = []
mvmz.SPEAKER_PARSE_MODE = False
mvmz.PREFLIGHT_COUNT_MODE = False
with mvmz._speakerCacheLock:
mvmz._speakerCache.clear()
def tearDown(self):
mvmz.VOCAB = self.original_vocab
mvmz.TRANSLATION_CONFIG.vocab = self.original_config_vocab
mvmz.NAMESLIST = self.original_names
mvmz.SPEAKER_COLLECTED = self.original_collected
mvmz.SPEAKER_PARSE_MODE = self.original_parse_mode
mvmz.PREFLIGHT_COUNT_MODE = self.original_preflight
with mvmz._speakerCacheLock:
mvmz._speakerCache.clear()
def test_exact_vocab_name_bypasses_model_and_stale_cache(self):
with mvmz._speakerCacheLock:
mvmz._speakerCache["ユウ"] = "Yu"
with patch.object(mvmz, "translateAI") as translate:
result = mvmz.getSpeaker("ユウ")
self.assertEqual(result, ["Yuu", [0, 0]])
translate.assert_not_called()
def test_character_name_is_substituted_inside_compound_label(self):
def translate(text, _context, _batch=False):
self.assertEqual(text, "Yuuイベント5")
return ["Yuu Event 5", [3, 2]]
with patch.object(mvmz, "translateAI", side_effect=translate):
result = mvmz.getSpeaker("ユウイベント5")
self.assertEqual(result, ["Yuu Event 5", [3, 2]])
def test_game_characters_override_generated_speaker_spelling(self):
mvmz.VOCAB = (
"# Game Characters\nユウ (Yuu) - Male protagonist.\n\n"
"# Speakers\nユウ (Yu)\n"
)
with patch.object(mvmz, "translateAI") as translate:
result = mvmz.getSpeaker("ユウ")
self.assertEqual(result, ["Yuu", [0, 0]])
translate.assert_not_called()
def test_reload_vocab_updates_translation_config(self):
with TemporaryDirectory() as tmp:
vocab_path = Path(tmp) / "vocab.txt"
vocab_path.write_text("# Game Characters\nユウ (You)\n", encoding="utf-8")
with patch.object(mvmz, "VOCAB_PATH", vocab_path):
mvmz._reload_vocab()
self.assertIn("ユウ (You)", mvmz.VOCAB)
self.assertEqual(mvmz.TRANSLATION_CONFIG.vocab, mvmz.VOCAB)
def test_parse_speakers_reuses_curated_name(self):
mvmz.SPEAKER_PARSE_MODE = True
mvmz.SPEAKER_COLLECTED = ["ユウ"]
with TemporaryDirectory() as tmp:
vocab_path = Path(tmp) / "vocab.txt"
vocab_path.write_text(VOCAB, encoding="utf-8")
with (
patch.object(mvmz, "VOCAB_PATH", vocab_path),
patch.object(mvmz, "translateAI") as translate,
):
mvmz.finalizeSpeakerParse()
written = vocab_path.read_text(encoding="utf-8")
translate.assert_not_called()
self.assertIn("# Speakers\nユウ (Yuu)", written)
class CharacterCompoundMatchingTests(unittest.TestCase):
def test_character_entry_matches_inside_katakana_compound(self):
pairs = parseVocabWithCategories(VOCAB)
matched = buildMatchedVocabText(pairs, "ユウイベント5")
self.assertIn("ユウ (Yuu)", matched)
def test_non_character_short_term_keeps_script_boundaries(self):
pairs = parseVocabWithCategories("# Terms\nキス (Kiss)\n")
matched = buildMatchedVocabText(pairs, "テキスト")
self.assertEqual(matched, "")
def test_curated_character_spelling_suppresses_generated_conflict(self):
pairs = parseVocabWithCategories(
"# Game Characters\nユウ (Yuu)\n\n# Speakers\nユウ (Yu)\n"
)
matched = buildMatchedVocabText(pairs, "ユウイベント5")
self.assertIn("ユウ (Yuu)", matched)
self.assertNotIn("ユウ (Yu)", matched)
if __name__ == "__main__":
unittest.main()