"""Tests for iteration-1 quality fixes: output sanitation, kougo labeling, Hebrew-aware glossary matching.""" from __future__ import annotations from src.context.glossary import GlossaryManager, _normalize_hebrew from src.models.glossary import EnhancedGlossaryTerm from src.models.verse import Verse, VerseReference from src.pipeline.prompt import PromptBuilder from src.pipeline.translator import _sanitize_translation_output class TestSanitizeTranslationOutput: def test_empty(self) -> None: assert _sanitize_translation_output("") == "" assert _sanitize_translation_output(" \n ") == "" def test_plain_japanese_passthrough(self) -> None: text = "初めに言葉があった。言葉は神と共にあった。" assert _sanitize_translation_output(text) == text def test_self_correction_keeps_last_japanese_block(self) -> None: raw = ( "この命に命があった。\n\n" "Wait—let me provide the correct translation.\n\n" "この方の内に命があった。そしてこの命は人々の光であった。" ) assert ( _sanitize_translation_output(raw) == "この方の内に命があった。そしてこの命は人々の光であった。" ) def test_strips_code_fences(self) -> None: raw = "```\n光は闇の中で輝いている。\n```" assert _sanitize_translation_output(raw) == "光は闇の中で輝いている。" def test_strips_translation_label(self) -> None: assert _sanitize_translation_output("翻訳: 神は言われた。") == "神は言われた。" def test_all_latin_returns_original(self) -> None: raw = "I cannot translate this verse." assert _sanitize_translation_output(raw) == raw class TestKougoReference: def _verse(self, **kwargs) -> Verse: return Verse( reference=VerseReference(book="john", chapter=1, verse=1), **kwargs ) def test_prompt_labels_kougo_correctly(self) -> None: builder = PromptBuilder(GlossaryManager()) verse = self._verse(japanese_kougo="初めに言があった。") section = builder._format_reference_translations(verse) assert "口語訳 (1955): 初めに言があった。" in section assert "大正改訳" not in section def test_best_text_prefers_kougo_over_taisho(self) -> None: builder = PromptBuilder(GlossaryManager()) verse = self._verse(japanese_kougo="口語", japanese_taisho="大正") assert builder._get_best_text(verse) == "口語" class TestHebrewGlossaryMatching: def test_normalize_strips_points_and_separators(self) -> None: assert _normalize_hebrew("בְּ/רֵאשִׁ֖ית") == "בראשית" def test_pointed_text_matches_unpointed_term(self) -> None: gm = GlossaryManager( [ EnhancedGlossaryTerm( original="אלהים", transliteration="elohim", japanese="神", english="God", ) ] ) terms = gm.find_terms_in_text("בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת") assert [t.original for t in terms] == ["אלהים"] def test_capitalized_greek_inflection_matches(self) -> None: gm = GlossaryManager( [ EnhancedGlossaryTerm( original="μακάριος", transliteration="makarios", japanese="幸い", english="blessed", inflected_forms=["μακάριοι"], ) ] ) terms = gm.find_terms_in_text("Μακάριοι οἱ πτωχοὶ") assert [t.original for t in terms] == ["μακάριος"] class TestEvaluatorIteration2: def test_prompt_includes_hebrew_when_no_greek(self) -> None: from src.evaluation.evaluator import build_evaluation_prompt prompt = build_evaluation_prompt( translation="初めに神は天と地を創造された。", greek_text=None, kjv_text="In the beginning God created...", reference_japanese=None, hebrew_text="בראשית ברא אלהים", ) assert "ORIGINAL TEXT (Hebrew):" in prompt assert "בראשית" in prompt def test_prompt_includes_surrounding_context(self) -> None: from src.evaluation.evaluator import build_evaluation_prompt prompt = build_evaluation_prompt( translation="テスト", greek_text="λόγος", kjv_text=None, reference_japanese=None, surrounding_context="[before 1] 初めに言があった。", ) assert "SURROUNDING PASSAGE" in prompt assert "初めに言があった" in prompt def test_glossary_guidance_lists_entries(self) -> None: from src.evaluation.evaluator import build_evaluation_prompt prompt = build_evaluation_prompt( translation="テスト", greek_text="βασιλεία", kjv_text=None, reference_japanese=None, glossary_terms=["βασιλεία → 御国 (in Matthew = 天の国)"], ) assert "GLOSSARY GUIDANCE" in prompt assert "- βασιλεία → 御国 (in Matthew = 天の国)" in prompt def test_build_verse_prompt_formats_terms_with_notes(self, tmp_path) -> None: from src.evaluation.evaluator import TranslationEvaluator from src.models.glossary import EnhancedGlossaryTerm from src.models.verse import TranslationContext, Verse, VerseReference ref = VerseReference(book="matthew", chapter=5, verse=3) target = Verse(reference=ref, greek_text="Μακάριοι", english_kjv="Blessed") ctx = TranslationContext( target_verse=target, preceding_verses=[ Verse( reference=VerseReference(book="matthew", chapter=5, verse=2), japanese_kougo="そして口を開いて教えられた。", ) ], following_verses=[], glossary_terms=[ EnhancedGlossaryTerm( original="μακάριος", transliteration="makarios", japanese="幸い", english="blessed", notes="Beatitude formula", ) ], ) evaluator = TranslationEvaluator(data_dir=tmp_path) prompt = evaluator.build_verse_prompt("幸いである", ctx) assert "μακάριος → 幸い (Beatitude formula)" in prompt assert "そして口を開いて教えられた。" in prompt assert "[before 2 —" in prompt def test_surrounding_context_none_when_no_neighbors(self) -> None: from src.evaluation.evaluator import TranslationEvaluator from src.models.verse import TranslationContext, Verse, VerseReference ref = VerseReference(book="john", chapter=1, verse=1) ctx = TranslationContext( target_verse=Verse(reference=ref, greek_text="λόγος"), preceding_verses=[], following_verses=[], glossary_terms=[], ) evaluator = TranslationEvaluator(data_dir="data") assert evaluator._format_surrounding_context(ctx) is None class TestSeamContinuity: def test_chunk_prompt_includes_preceding_translations(self) -> None: from src.context.glossary import GlossaryManager from src.models.pericope import ChunkTranslationContext, Pericope from src.models.verse import Verse, VerseReference from src.pipeline.prompt import PromptBuilder pericope = Pericope( id="john-1-19-test", title="Testimony", start_chapter=1, start_verse=19, end_chapter=1, end_verse=20, verses=["1:19", "1:20"], ) ctx = ChunkTranslationContext( pericope=pericope, book="john", verses=[ Verse( reference=VerseReference(book="john", chapter=1, verse=19), greek_text="Καὶ αὕτη ἐστὶν ἡ μαρτυρία", ) ], preceding_verses=[], following_verses=[], glossary_terms=[], ) builder = PromptBuilder(GlossaryManager()) prompt = builder.build_chunk_prompt( ctx, preceding_translations=[ ( VerseReference(book="john", chapter=1, verse=18), "これまで神を見た者は一人もいない。", ) ], ) assert "PRECEDING TEXT AS ALREADY TRANSLATED" in prompt assert "[1:18] これまで神を見た者は一人もいない。" in prompt def test_chunk_prompt_no_seam_section_without_translations(self) -> None: from src.context.glossary import GlossaryManager from src.models.pericope import ChunkTranslationContext, Pericope from src.models.verse import Verse, VerseReference from src.pipeline.prompt import PromptBuilder pericope = Pericope( id="p1", title=None, start_chapter=1, start_verse=1, end_chapter=1, end_verse=1, verses=["1:1"], ) ctx = ChunkTranslationContext( pericope=pericope, book="john", verses=[ Verse( reference=VerseReference(book="john", chapter=1, verse=1), greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος", ) ], preceding_verses=[], following_verses=[], glossary_terms=[], ) builder = PromptBuilder(GlossaryManager()) prompt = builder.build_chunk_prompt(ctx) assert "PRECEDING TEXT AS ALREADY TRANSLATED" not in prompt class TestWebNativeStyle: def test_web_native_readability_prompt(self) -> None: from src.models.style import ( READABILITY_PROMPTS, ReadabilityLevel, TranslationStyle, build_system_prompt, ) assert ReadabilityLevel("web_native") is ReadabilityLevel.WEB_NATIVE block = READABILITY_PROMPTS[ReadabilityLevel.WEB_NATIVE] assert "online" in block assert "NEVER trade meaning for smoothness" in block style = TranslationStyle(readability_level=ReadabilityLevel.WEB_NATIVE) prompt = build_system_prompt(style) assert "Web-native" in prompt def test_polite_formality_prompt(self) -> None: from src.models.style import ( FORMALITY_PROMPTS, FormalityLevel, TranslationStyle, build_system_prompt, ) assert FormalityLevel("polite") is FormalityLevel.POLITE style = TranslationStyle(formality_level=FormalityLevel.POLITE) prompt = build_system_prompt(style) assert "です/ます" in prompt def test_style_roundtrip_with_new_values(self) -> None: from src.models.style import FormalityLevel, ReadabilityLevel, TranslationStyle style = TranslationStyle( readability_level=ReadabilityLevel.WEB_NATIVE, formality_level=FormalityLevel.POLITE, ) again = TranslationStyle.from_dict(style.to_dict()) assert again.readability_level is ReadabilityLevel.WEB_NATIVE assert again.formality_level is FormalityLevel.POLITE def test_evaluator_criteria_web_native_audience(self) -> None: from src.evaluation.evaluator import _build_style_criteria text = _build_style_criteria( {"readability_level": "web_native", "formality_level": "polite"} ) assert "20s" in text assert "です/ます" in text class TestMorphologyExposure: def test_decode_greek_participle(self) -> None: from src.pipeline.morphology import decode_greek note = decode_greek("δικαιούμενοι", "δικαιόω", "V-", "-PPPNPM-") assert note == ( "δικαιούμενοι (lemma δικαιόω): present passive participle" " nominative plural" ) def test_decode_greek_non_verb_returns_none(self) -> None: from src.pipeline.morphology import decode_greek assert decode_greek("χάριτι", "χάρις", "N-", "----DSF-") is None def test_decode_hebrew_wayyiqtol_and_suffix(self) -> None: from src.pipeline.morphology import decode_hebrew assert decode_hebrew("וַיֹּאמֶר", "HVqw3ms") == ( "וַיֹּאמֶר: qal wayyiqtol (narrative past) 3ms" ) assert "pronominal suffix 1cs" in decode_hebrew("רֹעִי", "HVqrmsc/Sp1cs") def test_selection_prioritizes_marked_forms_and_caps(self) -> None: from src.pipeline.morphology import select_morphology_notes words = [ {"word": f"w{i}", "lemma": "λύω", "pos": "V-", "parsing": "3AAI-S--"} for i in range(8) ] + [{"word": "part", "lemma": "λύω", "pos": "V-", "parsing": "-PPPNPM-"}] notes = select_morphology_notes(words, "greek", limit=3) assert len(notes) == 3 assert notes[0].startswith("part") # participle outranks plain aorists def test_prompt_flag_disables_morphology(self, monkeypatch) -> None: import src.pipeline.prompt as prompt_mod from src.models.verse import Verse, VerseReference verse = Verse( reference=VerseReference(book="romans", chapter=3, verse=24), greek_text="δικαιούμενοι", greek_words=[ {"word": "δικαιούμενοι", "lemma": "δικαιόω", "pos": "V-", "parsing": "-PPPNPM-"} ], ) monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", True) assert prompt_mod._morphology_notes_for(verse) monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", False) assert prompt_mod._morphology_notes_for(verse) == [] def test_decode_greek_finite_verb_and_short_parsing(self) -> None: from src.pipeline.morphology import decode_greek note = decode_greek("ἦν", "εἰμί", "V-", "3IAI-S--") assert note == "ἦν (lemma εἰμί): imperfect active indicative 3sg" assert decode_greek("x", "y", "V-", "abc") is None assert decode_greek("x", "y", "V-", "--------") is None def test_decode_hebrew_edge_cases(self) -> None: from src.pipeline.morphology import decode_hebrew assert decode_hebrew("שָׁלוֹם", "HNcmsa") is None # noun assert decode_hebrew("x", "HV") is None # too short assert decode_hebrew("x", "HVxz") is None # unknown stem+conj # imperative prioritized from src.pipeline.morphology import _hebrew_priority assert _hebrew_priority("HVqv2ms") == 0 assert _hebrew_priority("HVqp3ms") == 1 assert _hebrew_priority("HNcmsa") == 1 def test_select_notes_empty_and_hebrew(self) -> None: from src.pipeline.morphology import select_morphology_notes assert select_morphology_notes(None, "greek") == [] assert select_morphology_notes([], "hebrew") == [] notes = select_morphology_notes( [ {"word": "וַיֹּאמֶר", "morph": "HVqw3ms"}, {"word": "אֱלֹהִים", "morph": "HNcmpa"}, ], "hebrew", ) assert notes == ["וַיֹּאמֶר: qal wayyiqtol (narrative past) 3ms"] def test_chunk_prompt_includes_morphology(self, monkeypatch) -> None: import src.pipeline.prompt as prompt_mod monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", True) from src.context.glossary import GlossaryManager from src.models.pericope import ChunkTranslationContext, Pericope from src.models.verse import Verse, VerseReference from src.pipeline.prompt import PromptBuilder pericope = Pericope( id="p-m", title=None, start_chapter=1, start_verse=1, end_chapter=1, end_verse=1, verses=["1:1"], ) ctx = ChunkTranslationContext( pericope=pericope, book="genesis", verses=[ Verse( reference=VerseReference(book="genesis", chapter=1, verse=1), hebrew_text="בָּרָא", hebrew_words=[{"word": "בָּרָא", "morph": "HVqp3ms"}], ) ], preceding_verses=[], following_verses=[], glossary_terms=[], ) prompt = PromptBuilder(GlossaryManager()).build_chunk_prompt(ctx) assert "MORPHOLOGY" in prompt assert "[1:1] בָּרָא: qal perfect (qatal) 3ms" in prompt