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

OPEN SOURCE

software/tests/test_final_push.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_final_push.py361 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 b730e0debfbe6d210c0fc4a5e02c790666f8fc497ac1dff9da7c07f99cbf5aa7
1"""Final push tests to reach 98% coverage."""23import json4import pytest5from pathlib import Path6from tempfile import TemporaryDirectory7from unittest.mock import MagicMock, patch89from src.context.builder import ContextBuilder10from src.context.cross_references import CrossReference, CrossReferenceManager, ReferenceType11from src.context.glossary import GlossaryManager12from src.data.loader import DataLoader13from src.data.storage import Storage14from src.exceptions import ChunkParsingError15from src.models.pericope import Pericope16from src.models.verse import VerseReference17from src.pipeline.prompt import PromptBuilder18from src.pipeline.translator import Translator192021@pytest.fixture22def temp_data_dir(tmp_path: Path) -> Path:23    """Create temporary data directory."""24    sources_dir = tmp_path / "sources"25    (sources_dir / "greek").mkdir(parents=True)26    (sources_dir / "english" / "kjv").mkdir(parents=True)27    (tmp_path / "metadata").mkdir(parents=True)28    (tmp_path / "output").mkdir(parents=True)2930    greek_data = {31        "1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},32        "1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},33    }34    with (sources_dir / "greek" / "john.json").open("w") as f:35        json.dump(greek_data, f)3637    kjv_data = {"1:1": "In the beginning", "1:2": "The same was"}38    with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:39        json.dump(kjv_data, f)4041    return tmp_path424344class TestParseChunkResponseStrategy4:45    """Tests for strategy 4 JSON extraction (iterating through { chars)."""4647    def test_parse_chunk_response_finds_json_after_text(48        self, temp_data_dir: Path49    ) -> None:50        """Test finding JSON when there's text before first brace."""51        loader = DataLoader(temp_data_dir)52        glossary = GlossaryManager()53        storage = Storage(temp_data_dir)54        context_builder = ContextBuilder(loader, glossary)55        prompt_builder = PromptBuilder(glossary)56        translator = Translator(context_builder, prompt_builder, storage)5758        # Response with failed first strategy but valid JSON later59        response = 'Some text { invalid } more text {"1:1": "翻訳"}'60        expected = ["1:1"]6162        translations = translator._parse_chunk_response(response, expected)63        assert translations["1:1"] == "翻訳"6465    def test_parse_chunk_response_multiple_braces(66        self, temp_data_dir: Path67    ) -> None:68        """Test when multiple { characters exist before valid JSON."""69        loader = DataLoader(temp_data_dir)70        glossary = GlossaryManager()71        storage = Storage(temp_data_dir)72        context_builder = ContextBuilder(loader, glossary)73        prompt_builder = PromptBuilder(glossary)74        translator = Translator(context_builder, prompt_builder, storage)7576        # Multiple invalid JSON before valid one77        response = '{ broken { also broken {"1:1": "正解"}'78        expected = ["1:1"]7980        translations = translator._parse_chunk_response(response, expected)81        assert translations["1:1"] == "正解"828384class TestExtractJsonFromPositionEdgeCases:85    """Tests for _extract_json_from_position edge cases."""8687    def test_extract_json_with_escaped_chars(self, temp_data_dir: Path) -> None:88        """Test extraction with escaped characters inside strings."""89        loader = DataLoader(temp_data_dir)90        glossary = GlossaryManager()91        storage = Storage(temp_data_dir)92        context_builder = ContextBuilder(loader, glossary)93        prompt_builder = PromptBuilder(glossary)94        translator = Translator(context_builder, prompt_builder, storage)9596        # JSON with escaped backslash and quote97        text = '{"1:1": "He said \\"Hello\\" and\\nwent"}'98        result = translator._extract_json_from_position(text, 0)99        assert result == text100101    def test_extract_json_no_closing_brace(self, temp_data_dir: Path) -> None:102        """Test extraction when no closing brace is found."""103        loader = DataLoader(temp_data_dir)104        glossary = GlossaryManager()105        storage = Storage(temp_data_dir)106        context_builder = ContextBuilder(loader, glossary)107        prompt_builder = PromptBuilder(glossary)108        translator = Translator(context_builder, prompt_builder, storage)109110        # Unclosed JSON111        text = '{"1:1": "test'112        result = translator._extract_json_from_position(text, 0)113        assert result is None114115    def test_extract_json_invalid_start(self, temp_data_dir: Path) -> None:116        """Test extraction with invalid start position."""117        loader = DataLoader(temp_data_dir)118        glossary = GlossaryManager()119        storage = Storage(temp_data_dir)120        context_builder = ContextBuilder(loader, glossary)121        prompt_builder = PromptBuilder(glossary)122        translator = Translator(context_builder, prompt_builder, storage)123124        # Start at non-{ character125        text = 'abc{"1:1": "test"}'126        result = translator._extract_json_from_position(text, 0)127        assert result is None128129        # Start past end130        result = translator._extract_json_from_position(text, 100)131        assert result is None132133134class TestValidateTranslationsEdgeCases:135    """Tests for _validate_translations edge cases."""136137    def test_validate_non_dict_raises(self, temp_data_dir: Path) -> None:138        """Test validation raises for non-dict input."""139        loader = DataLoader(temp_data_dir)140        glossary = GlossaryManager()141        storage = Storage(temp_data_dir)142        context_builder = ContextBuilder(loader, glossary)143        prompt_builder = PromptBuilder(glossary)144        translator = Translator(context_builder, prompt_builder, storage)145146        with pytest.raises(ChunkParsingError, match="Expected dict"):147            translator._validate_translations(148                ["not", "a", "dict"],  # list instead of dict149                ["1:1"],150                "original response",151            )152153    def test_validate_non_string_value_raises(self, temp_data_dir: Path) -> None:154        """Test validation raises for non-string translation value."""155        loader = DataLoader(temp_data_dir)156        glossary = GlossaryManager()157        storage = Storage(temp_data_dir)158        context_builder = ContextBuilder(loader, glossary)159        prompt_builder = PromptBuilder(glossary)160        translator = Translator(context_builder, prompt_builder, storage)161162        with pytest.raises(ChunkParsingError, match="is not a string"):163            translator._validate_translations(164                {"1:1": 12345},  # number instead of string165                ["1:1"],166                "original response",167            )168169170class TestExtractFromCodeBlockEdgeCases:171    """Tests for _extract_from_code_block edge cases."""172173    def test_extract_no_code_block(self, temp_data_dir: Path) -> None:174        """Test extraction when no code block exists."""175        loader = DataLoader(temp_data_dir)176        glossary = GlossaryManager()177        storage = Storage(temp_data_dir)178        context_builder = ContextBuilder(loader, glossary)179        prompt_builder = PromptBuilder(glossary)180        translator = Translator(context_builder, prompt_builder, storage)181182        # No code block, should return original183        text = '{"1:1": "test"}'184        result = translator._extract_from_code_block(text)185        assert result == text186187    def test_extract_unclosed_code_block(self, temp_data_dir: Path) -> None:188        """Test extraction with unclosed code block."""189        loader = DataLoader(temp_data_dir)190        glossary = GlossaryManager()191        storage = Storage(temp_data_dir)192        context_builder = ContextBuilder(loader, glossary)193        prompt_builder = PromptBuilder(glossary)194        translator = Translator(context_builder, prompt_builder, storage)195196        # Unclosed code block - should extract everything after opening197        text = '```json\n{"1:1": "test"}'198        result = translator._extract_from_code_block(text)199        assert '{"1:1": "test"}' in result200201202class TestExtractJsonByBracketsEdgeCases:203    """Tests for _extract_json_by_brackets edge cases."""204205    def test_no_opening_brace(self, temp_data_dir: Path) -> None:206        """Test when no opening brace exists."""207        loader = DataLoader(temp_data_dir)208        glossary = GlossaryManager()209        storage = Storage(temp_data_dir)210        context_builder = ContextBuilder(loader, glossary)211        prompt_builder = PromptBuilder(glossary)212        translator = Translator(context_builder, prompt_builder, storage)213214        result = translator._extract_json_by_brackets("no braces here")215        assert result is None216217218class TestCrossReferenceConsistencyNotes:219    """Tests for cross-reference consistency notes."""220221    def test_get_translation_consistency_notes_with_parallels(self) -> None:222        """Test consistency notes include parallels."""223        manager = CrossReferenceManager()224225        # Add a PARALLEL type reference226        manager.add_reference(227            CrossReference(228                source_book="matthew",229                source_chapter=3,230                source_verse=13,231                target_book="mark",232                target_chapter=1,233                target_verse_start=9,234                reference_type=ReferenceType.PARALLEL,235            )236        )237238        notes = manager.get_translation_consistency_notes("matthew", 3, 13)239        assert any("parallel" in n.lower() for n in notes)240241    def test_get_translation_consistency_notes_with_hint(self) -> None:242        """Test consistency notes include translation hint."""243        manager = CrossReferenceManager()244245        # Add a quotation with translation hint246        manager.add_reference(247            CrossReference(248                source_book="john",249                source_chapter=12,250                source_verse=40,251                target_book="isaiah",252                target_chapter=6,253                target_verse_start=10,254                reference_type=ReferenceType.QUOTATION,255                translation_hint="Use similar wording to Isaiah 6:10",256            )257        )258259        notes = manager.get_translation_consistency_notes("john", 12, 40)260        assert any("hint" in n.lower() for n in notes)261262263class TestChunkTranslationWithExisting:264    """Tests for chunk translation when some verses exist."""265266    @patch("src.pipeline.translator.subprocess.run")267    def test_translate_chunk_skips_all_existing(268        self, mock_run: MagicMock, temp_data_dir: Path269    ) -> None:270        """Test chunk translation returns early when all verses exist."""271        loader = DataLoader(temp_data_dir)272        glossary = GlossaryManager()273        storage = Storage(temp_data_dir)274        context_builder = ContextBuilder(loader, glossary)275        prompt_builder = PromptBuilder(glossary)276        translator = Translator(context_builder, prompt_builder, storage)277278        # Pre-save translations for verses279        from src.models.verse import TranslationOutput280281        for verse_ref in ["1:1", "1:2"]:282            chapter, verse = map(int, verse_ref.split(":"))283            ref = VerseReference(book="john", chapter=chapter, verse=verse)284            output = TranslationOutput(285                reference=ref,286                text=f"既存の翻訳 {verse_ref}",287                model="test",288                sources_used=["test"],289            )290            storage.save_translation(output)291292        # Create pericope293        pericope = Pericope(294            id="test-1",295            name="Test Pericope",296            start_chapter=1,297            start_verse=1,298            end_chapter=1,299            end_verse=2,300            verses=["1:1", "1:2"],301        )302303        # Should skip without calling Claude304        result = translator.translate_chunk("john", pericope, skip_existing=True)305306        # Should not have called subprocess.run for translation307        mock_run.assert_not_called()308309        # Should have existing translations310        assert "1:1" in result.translations311        assert "1:2" in result.translations312        assert result.coherence_note == "Loaded from existing translations"313314315class TestTMSaveAfterChapter:316    """Tests for TM save after chapter translation."""317318    @patch("src.pipeline.translator.subprocess.run")319    def test_tm_saved_after_chapter(320        self, mock_run: MagicMock, temp_data_dir: Path321    ) -> None:322        """Test that translation memory is saved after chapter translation."""323        from src.context.translation_memory import TranslationMemoryManager324325        loader = DataLoader(temp_data_dir)326        glossary = GlossaryManager()327        storage = Storage(temp_data_dir)328        context_builder = ContextBuilder(loader, glossary)329        prompt_builder = PromptBuilder(glossary)330331        # Create TM manager. Must be a temp path: the default is the real332        # production store, and this test drives a translator that saves.333        tm = TranslationMemoryManager(temp_data_dir / "memory" / "segments.json")334        tm_saved = []335        original_save = tm.save336337        def track_save(*args, **kwargs):338            tm_saved.append(True)339            return original_save(*args, **kwargs)340341        tm.save = track_save342343        translator = Translator(344            context_builder,345            prompt_builder,346            storage,347            translation_memory=tm,348        )349350        # Mock translation response351        mock_run.return_value = MagicMock(352            returncode=0,353            stdout="翻訳テスト",354            stderr="",355        )356357        translator.translate_chapter("john", 1, skip_existing=False)358359        # TM should have been saved360        assert len(tm_saved) >= 1361