"""Final push tests to reach 98% coverage.""" import json import pytest from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import MagicMock, patch from src.context.builder import ContextBuilder from src.context.cross_references import CrossReference, CrossReferenceManager, ReferenceType from src.context.glossary import GlossaryManager from src.data.loader import DataLoader from src.data.storage import Storage from src.exceptions import ChunkParsingError from src.models.pericope import Pericope from src.models.verse import VerseReference from src.pipeline.prompt import PromptBuilder from src.pipeline.translator import Translator @pytest.fixture def temp_data_dir(tmp_path: Path) -> Path: """Create temporary data directory.""" sources_dir = tmp_path / "sources" (sources_dir / "greek").mkdir(parents=True) (sources_dir / "english" / "kjv").mkdir(parents=True) (tmp_path / "metadata").mkdir(parents=True) (tmp_path / "output").mkdir(parents=True) greek_data = { "1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"}, "1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"}, } with (sources_dir / "greek" / "john.json").open("w") as f: json.dump(greek_data, f) kjv_data = {"1:1": "In the beginning", "1:2": "The same was"} with (sources_dir / "english" / "kjv" / "john.json").open("w") as f: json.dump(kjv_data, f) return tmp_path class TestParseChunkResponseStrategy4: """Tests for strategy 4 JSON extraction (iterating through { chars).""" def test_parse_chunk_response_finds_json_after_text( self, temp_data_dir: Path ) -> None: """Test finding JSON when there's text before first brace.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Response with failed first strategy but valid JSON later response = 'Some text { invalid } more text {"1:1": "翻訳"}' expected = ["1:1"] translations = translator._parse_chunk_response(response, expected) assert translations["1:1"] == "翻訳" def test_parse_chunk_response_multiple_braces( self, temp_data_dir: Path ) -> None: """Test when multiple { characters exist before valid JSON.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Multiple invalid JSON before valid one response = '{ broken { also broken {"1:1": "正解"}' expected = ["1:1"] translations = translator._parse_chunk_response(response, expected) assert translations["1:1"] == "正解" class TestExtractJsonFromPositionEdgeCases: """Tests for _extract_json_from_position edge cases.""" def test_extract_json_with_escaped_chars(self, temp_data_dir: Path) -> None: """Test extraction with escaped characters inside strings.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # JSON with escaped backslash and quote text = '{"1:1": "He said \\"Hello\\" and\\nwent"}' result = translator._extract_json_from_position(text, 0) assert result == text def test_extract_json_no_closing_brace(self, temp_data_dir: Path) -> None: """Test extraction when no closing brace is found.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Unclosed JSON text = '{"1:1": "test' result = translator._extract_json_from_position(text, 0) assert result is None def test_extract_json_invalid_start(self, temp_data_dir: Path) -> None: """Test extraction with invalid start position.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Start at non-{ character text = 'abc{"1:1": "test"}' result = translator._extract_json_from_position(text, 0) assert result is None # Start past end result = translator._extract_json_from_position(text, 100) assert result is None class TestValidateTranslationsEdgeCases: """Tests for _validate_translations edge cases.""" def test_validate_non_dict_raises(self, temp_data_dir: Path) -> None: """Test validation raises for non-dict input.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) with pytest.raises(ChunkParsingError, match="Expected dict"): translator._validate_translations( ["not", "a", "dict"], # list instead of dict ["1:1"], "original response", ) def test_validate_non_string_value_raises(self, temp_data_dir: Path) -> None: """Test validation raises for non-string translation value.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) with pytest.raises(ChunkParsingError, match="is not a string"): translator._validate_translations( {"1:1": 12345}, # number instead of string ["1:1"], "original response", ) class TestExtractFromCodeBlockEdgeCases: """Tests for _extract_from_code_block edge cases.""" def test_extract_no_code_block(self, temp_data_dir: Path) -> None: """Test extraction when no code block exists.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # No code block, should return original text = '{"1:1": "test"}' result = translator._extract_from_code_block(text) assert result == text def test_extract_unclosed_code_block(self, temp_data_dir: Path) -> None: """Test extraction with unclosed code block.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Unclosed code block - should extract everything after opening text = '```json\n{"1:1": "test"}' result = translator._extract_from_code_block(text) assert '{"1:1": "test"}' in result class TestExtractJsonByBracketsEdgeCases: """Tests for _extract_json_by_brackets edge cases.""" def test_no_opening_brace(self, temp_data_dir: Path) -> None: """Test when no opening brace exists.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) result = translator._extract_json_by_brackets("no braces here") assert result is None class TestCrossReferenceConsistencyNotes: """Tests for cross-reference consistency notes.""" def test_get_translation_consistency_notes_with_parallels(self) -> None: """Test consistency notes include parallels.""" manager = CrossReferenceManager() # Add a PARALLEL type reference manager.add_reference( CrossReference( source_book="matthew", source_chapter=3, source_verse=13, target_book="mark", target_chapter=1, target_verse_start=9, reference_type=ReferenceType.PARALLEL, ) ) notes = manager.get_translation_consistency_notes("matthew", 3, 13) assert any("parallel" in n.lower() for n in notes) def test_get_translation_consistency_notes_with_hint(self) -> None: """Test consistency notes include translation hint.""" manager = CrossReferenceManager() # Add a quotation with translation hint manager.add_reference( CrossReference( source_book="john", source_chapter=12, source_verse=40, target_book="isaiah", target_chapter=6, target_verse_start=10, reference_type=ReferenceType.QUOTATION, translation_hint="Use similar wording to Isaiah 6:10", ) ) notes = manager.get_translation_consistency_notes("john", 12, 40) assert any("hint" in n.lower() for n in notes) class TestChunkTranslationWithExisting: """Tests for chunk translation when some verses exist.""" @patch("src.pipeline.translator.subprocess.run") def test_translate_chunk_skips_all_existing( self, mock_run: MagicMock, temp_data_dir: Path ) -> None: """Test chunk translation returns early when all verses exist.""" loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Pre-save translations for verses from src.models.verse import TranslationOutput for verse_ref in ["1:1", "1:2"]: chapter, verse = map(int, verse_ref.split(":")) ref = VerseReference(book="john", chapter=chapter, verse=verse) output = TranslationOutput( reference=ref, text=f"既存の翻訳 {verse_ref}", model="test", sources_used=["test"], ) storage.save_translation(output) # Create pericope pericope = Pericope( id="test-1", name="Test Pericope", start_chapter=1, start_verse=1, end_chapter=1, end_verse=2, verses=["1:1", "1:2"], ) # Should skip without calling Claude result = translator.translate_chunk("john", pericope, skip_existing=True) # Should not have called subprocess.run for translation mock_run.assert_not_called() # Should have existing translations assert "1:1" in result.translations assert "1:2" in result.translations assert result.coherence_note == "Loaded from existing translations" class TestTMSaveAfterChapter: """Tests for TM save after chapter translation.""" @patch("src.pipeline.translator.subprocess.run") def test_tm_saved_after_chapter( self, mock_run: MagicMock, temp_data_dir: Path ) -> None: """Test that translation memory is saved after chapter translation.""" from src.context.translation_memory import TranslationMemoryManager loader = DataLoader(temp_data_dir) glossary = GlossaryManager() storage = Storage(temp_data_dir) context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) # Create TM manager. Must be a temp path: the default is the real # production store, and this test drives a translator that saves. tm = TranslationMemoryManager(temp_data_dir / "memory" / "segments.json") tm_saved = [] original_save = tm.save def track_save(*args, **kwargs): tm_saved.append(True) return original_save(*args, **kwargs) tm.save = track_save translator = Translator( context_builder, prompt_builder, storage, translation_memory=tm, ) # Mock translation response mock_run.return_value = MagicMock( returncode=0, stdout="翻訳テスト", stderr="", ) translator.translate_chapter("john", 1, skip_existing=False) # TM should have been saved assert len(tm_saved) >= 1