Pre-publication review draft
日常訳NICHIJOYAKU日本語 ↗

OPEN SOURCE

software/tests/test_pipeline.py

Read on this site and download the files you need.

Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.

← Back to the open-source guide

software/tests/test_pipeline.py562 linesDownload this file ↓

This is the same source included in the download. Each line number is a link you can save or share.

File verification detailsSHA-256 f25d80097288efcb1aa56024c44defb09bd9b25fad2fc24f87a897bed10e88cf
1"""Tests for translation pipeline."""23import json4import pytest5from pathlib import Path6from unittest.mock import MagicMock, patch78from src.context.builder import ContextBuilder9from src.context.glossary import GlossaryManager10from src.data.loader import DataLoader11from src.data.storage import Storage12from src.exceptions import ClaudeAPIError13from src.models.verse import (14    BookMetadata,15    ChapterSummary,16    GlossaryTerm,17    TranslationContext,18    Verse,19    VerseReference,20)21from src.pipeline.prompt import SYSTEM_PROMPT, PromptBuilder22from src.pipeline.translator import Translator232425@pytest.fixture26def glossary_terms() -> list[GlossaryTerm]:27    """Create test glossary terms."""28    return [29        GlossaryTerm(30            original="λόγος",31            transliteration="logos",32            japanese="言葉",33            english="word",34            notes="The Word",35        ),36    ]373839@pytest.fixture40def sample_verse() -> Verse:41    """Create a sample verse."""42    return Verse(43        reference=VerseReference(book="john", chapter=1, verse=1),44        greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",45        greek_translit="En archē ēn ho logos",46        japanese_meiji="太初に言あり",47        japanese_taisho="元始に言あり",48        english_kjv="In the beginning was the Word",49        english_asv="In the beginning was the Word",50    )515253@pytest.fixture54def sample_context(sample_verse: Verse, glossary_terms: list[GlossaryTerm]) -> TranslationContext:55    """Create a sample translation context."""56    return TranslationContext(57        target_verse=sample_verse,58        preceding_verses=[],59        following_verses=[60            Verse(61                reference=VerseReference(book="john", chapter=1, verse=2),62                english_kjv="The same was in the beginning with God",63            )64        ],65        chapter_summary=ChapterSummary(66            book="john",67            chapter=1,68            summary="The prologue",69            key_events=[],70            key_characters=[],71            key_themes=[],72        ),73        book_metadata=BookMetadata(74            book="john",75            name_japanese="ヨハネによる福音書",76            name_english="Gospel of John",77            testament="NT",78            author="John",79            themes=["love"],80            description="The fourth Gospel",81            chapter_count=21,82            verse_count=879,83        ),84        glossary_terms=glossary_terms,85    )868788@pytest.fixture89def temp_data_dir(tmp_path: Path) -> Path:90    """Create temporary data directory."""91    sources_dir = tmp_path / "sources"92    (sources_dir / "greek").mkdir(parents=True)93    (sources_dir / "english" / "kjv").mkdir(parents=True)94    (sources_dir / "japanese" / "meiji").mkdir(parents=True)95    (sources_dir / "japanese" / "taisho").mkdir(parents=True)96    (sources_dir / "english" / "asv").mkdir(parents=True)97    (tmp_path / "metadata").mkdir(parents=True)98    (tmp_path / "output").mkdir(parents=True)99    (tmp_path / "progress").mkdir(parents=True)100101    # Add test data102    greek_data = {103        "1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},104        "1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},105    }106    with (sources_dir / "greek" / "john.json").open("w") as f:107        json.dump(greek_data, f)108109    kjv_data = {"1:1": "In the beginning", "1:2": "The same was"}110    with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:111        json.dump(kjv_data, f)112113    return tmp_path114115116class TestPromptBuilder:117    """Tests for PromptBuilder."""118119    def test_get_system_prompt(self, glossary_terms: list[GlossaryTerm]) -> None:120        """Test getting system prompt."""121        glossary = GlossaryManager(glossary_terms)122        builder = PromptBuilder(glossary)123        prompt = builder.get_system_prompt()124        assert "expert Bible translator" in prompt125        assert "新共同訳" in prompt126127    def test_build_prompt(128        self, sample_context: TranslationContext, glossary_terms: list[GlossaryTerm]129    ) -> None:130        """Test building a translation prompt."""131        glossary = GlossaryManager(glossary_terms)132        builder = PromptBuilder(glossary)133        prompt = builder.build_prompt(sample_context)134135        # Check all sections are present136        assert "SOURCE TEXT:" in prompt137        assert "REFERENCE TRANSLATIONS:" in prompt138        assert "CONTEXT:" in prompt139        assert "GLOSSARY" in prompt140        assert "TRANSLATE:" in prompt141142    def test_build_prompt_with_greek(143        self, sample_context: TranslationContext, glossary_terms: list[GlossaryTerm]144    ) -> None:145        """Test prompt includes Greek text."""146        glossary = GlossaryManager(glossary_terms)147        builder = PromptBuilder(glossary)148        prompt = builder.build_prompt(sample_context)149150        assert "Greek:" in prompt151        assert "Ἐν ἀρχῇ" in prompt152        assert "Romanization:" in prompt153154    def test_build_prompt_with_references(155        self, sample_context: TranslationContext, glossary_terms: list[GlossaryTerm]156    ) -> None:157        """Test prompt includes reference translations."""158        glossary = GlossaryManager(glossary_terms)159        builder = PromptBuilder(glossary)160        prompt = builder.build_prompt(sample_context)161162        assert "明治元訳" in prompt163        assert "KJV:" in prompt164165    def test_build_prompt_with_book_metadata(166        self, sample_context: TranslationContext, glossary_terms: list[GlossaryTerm]167    ) -> None:168        """Test prompt includes book metadata."""169        glossary = GlossaryManager(glossary_terms)170        builder = PromptBuilder(glossary)171        prompt = builder.build_prompt(sample_context)172173        assert "Gospel of John" in prompt174        assert "ヨハネによる福音書" in prompt175176    def test_build_prompt_with_following_verses(177        self, sample_context: TranslationContext, glossary_terms: list[GlossaryTerm]178    ) -> None:179        """Test prompt includes following verses."""180        glossary = GlossaryManager(glossary_terms)181        builder = PromptBuilder(glossary)182        prompt = builder.build_prompt(sample_context)183184        assert "Following verses:" in prompt185186    def test_build_prompt_hebrew_verse(187        self, glossary_terms: list[GlossaryTerm]188    ) -> None:189        """Test prompt with Hebrew text."""190        verse = Verse(191            reference=VerseReference(book="genesis", chapter=1, verse=1),192            hebrew_text="בְּרֵאשִׁית",193            hebrew_translit="bereshit",194            english_kjv="In the beginning",195        )196        context = TranslationContext(target_verse=verse)197198        glossary = GlossaryManager(glossary_terms)199        builder = PromptBuilder(glossary)200        prompt = builder.build_prompt(context)201202        assert "Hebrew:" in prompt203        assert "בְּרֵאשִׁית" in prompt204205    def test_build_prompt_no_metadata(206        self, sample_verse: Verse, glossary_terms: list[GlossaryTerm]207    ) -> None:208        """Test prompt without metadata."""209        context = TranslationContext(210            target_verse=sample_verse,211            book_metadata=None,212            chapter_summary=None,213        )214215        glossary = GlossaryManager(glossary_terms)216        builder = PromptBuilder(glossary)217        prompt = builder.build_prompt(context)218219        # Should still work without metadata220        assert "TRANSLATE:" in prompt221222    def test_get_best_text_prefers_english(223        self, glossary_terms: list[GlossaryTerm]224    ) -> None:225        """Test _get_best_text prefers English."""226        verse = Verse(227            reference=VerseReference(book="john", chapter=1, verse=1),228            english_kjv="KJV text",229            japanese_meiji="Meiji text",230            greek_text="Greek text",231        )232233        glossary = GlossaryManager(glossary_terms)234        builder = PromptBuilder(glossary)235        text = builder._get_best_text(verse)236237        assert text == "KJV text"238239    def test_get_best_text_fallback_order(240        self, glossary_terms: list[GlossaryTerm]241    ) -> None:242        """Test _get_best_text fallback order."""243        glossary = GlossaryManager(glossary_terms)244        builder = PromptBuilder(glossary)245246        # Test ASV fallback247        verse = Verse(248            reference=VerseReference(book="john", chapter=1, verse=1),249            english_asv="ASV text",250        )251        assert builder._get_best_text(verse) == "ASV text"252253        # Test Japanese fallback254        verse = Verse(255            reference=VerseReference(book="john", chapter=1, verse=1),256            japanese_taisho="Taisho text",257        )258        assert builder._get_best_text(verse) == "Taisho text"259260        # Test Meiji fallback261        verse = Verse(262            reference=VerseReference(book="john", chapter=1, verse=1),263            japanese_meiji="Meiji text",264        )265        assert builder._get_best_text(verse) == "Meiji text"266267        # Test Greek fallback268        verse = Verse(269            reference=VerseReference(book="john", chapter=1, verse=1),270            greek_text="Greek text",271        )272        assert builder._get_best_text(verse) == "Greek text"273274        # Test Hebrew fallback275        verse = Verse(276            reference=VerseReference(book="john", chapter=1, verse=1),277            hebrew_text="Hebrew text",278        )279        assert builder._get_best_text(verse) == "Hebrew text"280281        # Test empty fallback282        verse = Verse(283            reference=VerseReference(book="john", chapter=1, verse=1),284        )285        assert builder._get_best_text(verse) == "[text not available]"286287288class TestTranslator:289    """Tests for Translator."""290291    def test_init(self, temp_data_dir: Path) -> None:292        """Test initializing translator."""293        loader = DataLoader(temp_data_dir)294        storage = Storage(temp_data_dir)295        glossary = GlossaryManager()296        context_builder = ContextBuilder(loader, glossary)297        prompt_builder = PromptBuilder(glossary)298299        translator = Translator(context_builder, prompt_builder, storage)300        from src.pipeline.translator import DEFAULT_MODEL301302        assert translator.model == DEFAULT_MODEL303304    def test_translate_verse_skip_existing(self, temp_data_dir: Path) -> None:305        """Test translate_verse skips existing translations."""306        loader = DataLoader(temp_data_dir)307        storage = Storage(temp_data_dir)308        glossary = GlossaryManager()309        context_builder = ContextBuilder(loader, glossary)310        prompt_builder = PromptBuilder(glossary)311312        translator = Translator(context_builder, prompt_builder, storage)313314        # Save an existing translation315        from src.models.verse import TranslationOutput316317        ref = VerseReference(book="john", chapter=1, verse=1)318        existing = TranslationOutput(319            reference=ref,320            text="Existing translation",321            model="test",322        )323        storage.save_translation(existing)324325        # Should return existing without calling CLI326        result = translator.translate_verse(ref, skip_existing=True)327        assert result.text == "Existing translation"328329    @patch("src.pipeline.translator.subprocess.run")330    def test_translate_verse_calls_claude(331        self, mock_run: MagicMock, temp_data_dir: Path332    ) -> None:333        """Test translate_verse calls Claude CLI for new verses."""334        loader = DataLoader(temp_data_dir)335        storage = Storage(temp_data_dir)336        glossary = GlossaryManager()337        context_builder = ContextBuilder(loader, glossary)338        prompt_builder = PromptBuilder(glossary)339340        translator = Translator(context_builder, prompt_builder, storage)341342        # Mock subprocess.run343        mock_run.return_value = MagicMock(344            returncode=0, stdout="初めに言葉があった。", stderr=""345        )346347        ref = VerseReference(book="john", chapter=1, verse=1)348        result = translator.translate_verse(ref, skip_existing=False)349350        assert result.text == "初めに言葉があった。"351        mock_run.assert_called_once()352353    @patch("src.pipeline.translator.subprocess.run")354    def test_call_claude_raises_on_error(355        self, mock_run: MagicMock, temp_data_dir: Path356    ) -> None:357        """Test _call_claude raises ClaudeAPIError on CLI failure."""358        loader = DataLoader(temp_data_dir)359        storage = Storage(temp_data_dir)360        glossary = GlossaryManager()361        context_builder = ContextBuilder(loader, glossary)362        prompt_builder = PromptBuilder(glossary)363364        translator = Translator(context_builder, prompt_builder, storage)365366        # Mock subprocess.run to return error367        mock_run.return_value = MagicMock(368            returncode=1, stdout="", stderr="API error"369        )370371        with pytest.raises(ClaudeAPIError):372            translator._call_claude("system", "user")373374    def test_get_sources_used(self, temp_data_dir: Path) -> None:375        """Test _get_sources_used returns correct sources."""376        loader = DataLoader(temp_data_dir)377        storage = Storage(temp_data_dir)378        glossary = GlossaryManager()379        context_builder = ContextBuilder(loader, glossary)380        prompt_builder = PromptBuilder(glossary)381382        translator = Translator(context_builder, prompt_builder, storage)383384        verse = Verse(385            reference=VerseReference(book="john", chapter=1, verse=1),386            greek_text="Greek",387            english_kjv="KJV",388            japanese_meiji="Meiji",389        )390        context = TranslationContext(target_verse=verse)391392        sources = translator._get_sources_used(context)393        assert "greek" in sources394        assert "kjv" in sources395        assert "meiji" in sources396397    def test_get_sources_used_hebrew(self, temp_data_dir: Path) -> None:398        """Test _get_sources_used includes Hebrew."""399        loader = DataLoader(temp_data_dir)400        storage = Storage(temp_data_dir)401        glossary = GlossaryManager()402        context_builder = ContextBuilder(loader, glossary)403        prompt_builder = PromptBuilder(glossary)404405        translator = Translator(context_builder, prompt_builder, storage)406407        verse = Verse(408            reference=VerseReference(book="genesis", chapter=1, verse=1),409            hebrew_text="Hebrew",410            japanese_taisho="Taisho",411            english_asv="ASV",412        )413        context = TranslationContext(target_verse=verse)414415        sources = translator._get_sources_used(context)416        assert "hebrew" in sources417        assert "taisho" in sources418        assert "asv" in sources419420    @patch("src.pipeline.translator.subprocess.run")421    def test_translate_verse_parses_furigana(422        self, mock_run: MagicMock, temp_data_dir: Path423    ) -> None:424        """Test translate_verse parses furigana from Claude response."""425        loader = DataLoader(temp_data_dir)426        storage = Storage(temp_data_dir)427        glossary = GlossaryManager()428        context_builder = ContextBuilder(loader, glossary)429        prompt_builder = PromptBuilder(glossary)430431        translator = Translator(context_builder, prompt_builder, storage)432433        # Mock subprocess.run returning text with furigana notation434        mock_run.return_value = MagicMock(435            returncode=0, stdout="神[かみ]は愛[あい]です。", stderr=""436        )437438        ref = VerseReference(book="john", chapter=1, verse=1)439        result = translator.translate_verse(ref, skip_existing=False)440441        # Check plain text has furigana stripped442        assert result.text == "神は愛です。"443444        # Check original text with furigana is preserved445        assert result.text_with_furigana == "神[かみ]は愛[あい]です。"446447        # Check annotations are correctly extracted448        assert len(result.furigana_annotations) == 2449        assert result.furigana_annotations[0]["base_text"] == "神"450        assert result.furigana_annotations[0]["reading"] == "かみ"451        assert result.furigana_annotations[1]["base_text"] == "愛"452        assert result.furigana_annotations[1]["reading"] == "あい"453454    @patch("src.pipeline.translator.subprocess.run")455    def test_translate_verse_no_furigana(456        self, mock_run: MagicMock, temp_data_dir: Path457    ) -> None:458        """Test translate_verse handles text without furigana."""459        loader = DataLoader(temp_data_dir)460        storage = Storage(temp_data_dir)461        glossary = GlossaryManager()462        context_builder = ContextBuilder(loader, glossary)463        prompt_builder = PromptBuilder(glossary)464465        translator = Translator(context_builder, prompt_builder, storage)466467        # Mock subprocess.run returning text without furigana468        mock_run.return_value = MagicMock(469            returncode=0, stdout="初めに言葉があった。", stderr=""470        )471472        ref = VerseReference(book="john", chapter=1, verse=1)473        result = translator.translate_verse(ref, skip_existing=False)474475        assert result.text == "初めに言葉があった。"476        assert result.text_with_furigana == "初めに言葉があった。"477        assert result.furigana_annotations == []478479    def test_save_chunk_translations_parses_furigana(480        self, temp_data_dir: Path481    ) -> None:482        """Test _save_chunk_translations parses furigana from chunk output."""483        loader = DataLoader(temp_data_dir)484        storage = Storage(temp_data_dir)485        glossary = GlossaryManager()486        context_builder = ContextBuilder(loader, glossary)487        prompt_builder = PromptBuilder(glossary)488489        translator = Translator(context_builder, prompt_builder, storage)490491        # Call _save_chunk_translations with furigana text492        translations = {493            "1:1": "神[かみ]は愛[あい]です。",494            "1:2": "言葉[ことば]は神[かみ]と共[とも]にあった。",495        }496        translator._save_chunk_translations("john", translations)497498        # Load and verify499        loaded = storage.load_translation(500            VerseReference(book="john", chapter=1, verse=1)501        )502        assert loaded is not None503        assert loaded.text == "神は愛です。"504        assert loaded.text_with_furigana == "神[かみ]は愛[あい]です。"505        assert len(loaded.furigana_annotations) == 2506507        loaded2 = storage.load_translation(508            VerseReference(book="john", chapter=1, verse=2)509        )510        assert loaded2 is not None511        assert loaded2.text == "言葉は神と共にあった。"512        assert len(loaded2.furigana_annotations) == 3513514515class TestTranslatorProtoArchive:516    """Tests for translator proto archive functionality."""517518    def test_enable_proto_archive_default_false(self, temp_data_dir: Path) -> None:519        """Test enable_proto_archive is False by default."""520        loader = DataLoader(temp_data_dir)521        storage = Storage(temp_data_dir)522        glossary = GlossaryManager()523        context_builder = ContextBuilder(loader, glossary)524        prompt_builder = PromptBuilder(glossary)525526        translator = Translator(context_builder, prompt_builder, storage)527        assert translator.enable_proto_archive is False528        assert storage.enable_archive is False529530    def test_enable_proto_archive_init(self, temp_data_dir: Path) -> None:531        """Test enable_proto_archive can be set in init."""532        loader = DataLoader(temp_data_dir)533        storage = Storage(temp_data_dir)534        glossary = GlossaryManager()535        context_builder = ContextBuilder(loader, glossary)536        prompt_builder = PromptBuilder(glossary)537538        translator = Translator(539            context_builder, prompt_builder, storage, enable_proto_archive=True540        )541        assert translator.enable_proto_archive is True542        assert storage.enable_archive is True543544    def test_enable_proto_archive_property_setter(self, temp_data_dir: Path) -> None:545        """Test enable_proto_archive property setter."""546        loader = DataLoader(temp_data_dir)547        storage = Storage(temp_data_dir)548        glossary = GlossaryManager()549        context_builder = ContextBuilder(loader, glossary)550        prompt_builder = PromptBuilder(glossary)551552        translator = Translator(context_builder, prompt_builder, storage)553        assert translator.enable_proto_archive is False554555        translator.enable_proto_archive = True556        assert translator.enable_proto_archive is True557        assert storage.enable_archive is True558559        translator.enable_proto_archive = False560        assert translator.enable_proto_archive is False561        assert storage.enable_archive is False562