"""Extended tests for translation pipeline to improve coverage.""" import json import pytest from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch from src.context.builder import ContextBuilder from src.context.glossary import GlossaryManager from src.data.loader import DataLoader from src.data.storage import Storage from src.exceptions import ClaudeAPIError from src.models.verse import ( BookMetadata, ChapterSummary, GlossaryTerm, TranslationContext, Verse, VerseReference, ) from src.pipeline.prompt import PromptBuilder from src.pipeline.translator import Translator @pytest.fixture def temp_data_dir_extended(tmp_path: Path) -> Path: """Create temp data directory with more test data.""" sources_dir = tmp_path / "sources" (sources_dir / "greek").mkdir(parents=True) (sources_dir / "english" / "kjv").mkdir(parents=True) (sources_dir / "english" / "asv").mkdir(parents=True) (sources_dir / "japanese" / "meiji").mkdir(parents=True) (sources_dir / "japanese" / "taisho").mkdir(parents=True) (tmp_path / "metadata" / "john").mkdir(parents=True) (tmp_path / "output").mkdir(parents=True) (tmp_path / "progress").mkdir(parents=True) # Greek data with multiple verses greek_data = { "1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"}, "1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"}, "1:3": {"text": "πάντα δι' αὐτοῦ", "translit": "panta di autou"}, } 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", "1:3": "All things were made", } with (sources_dir / "english" / "kjv" / "john.json").open("w") as f: json.dump(kjv_data, f) # Chapter summary chapter_summaries = { "1": { "summary": "The prologue", "key_events": [], "key_characters": [], "key_themes": [], } } with (tmp_path / "metadata" / "john" / "chapter_summaries.json").open("w") as f: json.dump(chapter_summaries, f) # Book metadata book_metadata = { "book": "john", "name_japanese": "ヨハネによる福音書", "name_english": "Gospel of John", "testament": "NT", "author": "John", "themes": ["love"], "description": "The fourth Gospel", "chapter_count": 21, "verse_count": 879, } with (tmp_path / "metadata" / "john" / "book_context.json").open("w") as f: json.dump(book_metadata, f) return tmp_path class TestPromptBuilderExtended: """Extended tests for PromptBuilder.""" def test_build_prompt_with_preceding_verses(self) -> None: """Test prompt includes preceding verses.""" glossary = GlossaryManager() builder = PromptBuilder(glossary) target_verse = Verse( reference=VerseReference(book="john", chapter=1, verse=3), greek_text="πάντα δι' αὐτοῦ", english_kjv="All things were made by him", ) preceding = [ Verse( reference=VerseReference(book="john", chapter=1, verse=1), english_kjv="In the beginning was the Word", ), Verse( reference=VerseReference(book="john", chapter=1, verse=2), english_kjv="The same was in the beginning with God", ), ] context = TranslationContext( target_verse=target_verse, preceding_verses=preceding, ) prompt = builder.build_prompt(context) assert "Preceding verses:" in prompt assert "In the beginning was the Word" in prompt def test_build_prompt_with_chapter_summary(self) -> None: """Test prompt includes chapter summary.""" glossary = GlossaryManager() builder = PromptBuilder(glossary) target_verse = Verse( reference=VerseReference(book="john", chapter=1, verse=1), greek_text="Ἐν ἀρχῇ", english_kjv="In the beginning", ) chapter_summary = ChapterSummary( book="john", chapter=1, summary="This is the prologue to John's Gospel.", key_events=[], key_characters=[], key_themes=[], ) context = TranslationContext( target_verse=target_verse, chapter_summary=chapter_summary, ) prompt = builder.build_prompt(context) assert "Chapter Summary:" in prompt assert "This is the prologue" in prompt def test_build_prompt_with_themes(self) -> None: """Test prompt includes book themes.""" glossary = GlossaryManager() builder = PromptBuilder(glossary) target_verse = Verse( reference=VerseReference(book="john", chapter=1, verse=1), greek_text="Ἐν ἀρχῇ", english_kjv="In the beginning", ) book_metadata = BookMetadata( book="john", name_japanese="ヨハネによる福音書", name_english="Gospel of John", testament="NT", author="John", themes=["love", "light", "life"], description="The fourth Gospel", chapter_count=21, verse_count=879, ) context = TranslationContext( target_verse=target_verse, book_metadata=book_metadata, ) prompt = builder.build_prompt(context) assert "Themes:" in prompt assert "love, light, life" in prompt class TestTranslatorExtended: """Extended tests for Translator.""" @patch("src.pipeline.translator.subprocess.run") def test_translate_chapter( self, mock_run: MagicMock, temp_data_dir_extended: Path ) -> None: """Test translating a full chapter.""" loader = DataLoader(temp_data_dir_extended) storage = Storage(temp_data_dir_extended) glossary = GlossaryManager() context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator( context_builder, prompt_builder, storage, rate_limit_delay=0 ) # Mock subprocess.run mock_run.return_value = MagicMock( returncode=0, stdout="翻訳テスト。", stderr="", ) # Track progress progress_log = [] def progress_callback(current: int, total: int, ref: VerseReference) -> None: progress_log.append((current, total, str(ref))) outputs = translator.translate_chapter( "john", 1, skip_existing=False, progress_callback=progress_callback ) assert len(outputs) == 3 # 3 verses in test data assert len(progress_log) == 3 @patch("src.pipeline.translator.asyncio.create_subprocess_exec") @pytest.mark.asyncio async def test_translate_verse_async( self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path ) -> None: """Test async translation of a verse.""" loader = DataLoader(temp_data_dir_extended) storage = Storage(temp_data_dir_extended) glossary = GlossaryManager() context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Mock async subprocess mock_process = AsyncMock() mock_process.returncode = 0 mock_process.communicate.return_value = ("非同期翻訳テスト。".encode(), b"") mock_subprocess.return_value = mock_process ref = VerseReference(book="john", chapter=1, verse=1) output = await translator.translate_verse_async(ref, skip_existing=False) assert output.text == "非同期翻訳テスト。" @pytest.mark.asyncio async def test_translate_verse_async_skip_existing( self, temp_data_dir_extended: Path ) -> None: """Test async translation skips existing.""" from src.models.verse import TranslationOutput loader = DataLoader(temp_data_dir_extended) storage = Storage(temp_data_dir_extended) glossary = GlossaryManager() context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Save existing ref = VerseReference(book="john", chapter=1, verse=1) existing = TranslationOutput( reference=ref, text="既存の翻訳。", model="test" ) storage.save_translation(existing) output = await translator.translate_verse_async(ref, skip_existing=True) assert output.text == "既存の翻訳。" @patch("src.pipeline.translator.asyncio.create_subprocess_exec") @pytest.mark.asyncio async def test_translate_chapter_async( self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path ) -> None: """Test async translation of a chapter.""" loader = DataLoader(temp_data_dir_extended) storage = Storage(temp_data_dir_extended) glossary = GlossaryManager() context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator( context_builder, prompt_builder, storage, rate_limit_delay=0 ) # Mock async subprocess mock_process = AsyncMock() mock_process.returncode = 0 mock_process.communicate.return_value = ("非同期チャプター翻訳。".encode(), b"") mock_subprocess.return_value = mock_process outputs = await translator.translate_chapter_async( "john", 1, skip_existing=False, max_concurrent=2 ) assert len(outputs) == 3 @patch("src.pipeline.translator.asyncio.create_subprocess_exec") @pytest.mark.asyncio async def test_call_claude_async_raises_on_error( self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path ) -> None: """Test _call_claude_async raises ClaudeAPIError on CLI failure.""" loader = DataLoader(temp_data_dir_extended) storage = Storage(temp_data_dir_extended) glossary = GlossaryManager() context_builder = ContextBuilder(loader, glossary) prompt_builder = PromptBuilder(glossary) translator = Translator(context_builder, prompt_builder, storage) # Mock async subprocess with error mock_process = AsyncMock() mock_process.returncode = 1 mock_process.communicate.return_value = (b"", b"Error: CLI failed") mock_subprocess.return_value = mock_process with pytest.raises(ClaudeAPIError, match="Claude CLI failed"): await translator._call_claude_async("system", "user") class TestContextBuilderExtended: """Extended tests for ContextBuilder.""" def test_find_terms_in_greek_translit_duplicate(self, tmp_path: Path) -> None: """Test that duplicate terms are not added from transliteration.""" sources_dir = tmp_path / "sources" (sources_dir / "greek").mkdir(parents=True) (sources_dir / "hebrew").mkdir(parents=True) (sources_dir / "japanese" / "meiji").mkdir(parents=True) (sources_dir / "japanese" / "taisho").mkdir(parents=True) (sources_dir / "english" / "kjv").mkdir(parents=True) (sources_dir / "english" / "asv").mkdir(parents=True) (tmp_path / "metadata").mkdir(parents=True) # Create Greek data with both original and transliteration containing the term greek_data = { "1:1": { "text": "λόγος was here", # Contains original "translit": "logos was here", # Contains transliteration }, } with (sources_dir / "greek" / "john.json").open("w") as f: json.dump(greek_data, f) loader = DataLoader(tmp_path) # Create glossary where term can be found in both original and translit terms = [ GlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", ) ] glossary = GlossaryManager(terms) builder = ContextBuilder(loader, glossary) ref = VerseReference(book="john", chapter=1, verse=1) context = builder.build_context(ref) # Should only have one term, not duplicates assert len(context.glossary_terms) == 1 assert context.glossary_terms[0].original == "λόγος" def test_find_terms_only_in_transliteration(self, tmp_path: Path) -> None: """Test finding terms only in transliteration (not in original).""" sources_dir = tmp_path / "sources" (sources_dir / "greek").mkdir(parents=True) (sources_dir / "hebrew").mkdir(parents=True) (sources_dir / "japanese" / "meiji").mkdir(parents=True) (sources_dir / "japanese" / "taisho").mkdir(parents=True) (sources_dir / "english" / "kjv").mkdir(parents=True) (sources_dir / "english" / "asv").mkdir(parents=True) (tmp_path / "metadata").mkdir(parents=True) # Create Greek data with only transliteration containing the term greek_data = { "1:1": { "text": "Some other Greek text", # Does NOT contain original "translit": "Some text with logos", # Contains transliteration }, } with (sources_dir / "greek" / "john.json").open("w") as f: json.dump(greek_data, f) loader = DataLoader(tmp_path) # Glossary term found only via transliteration terms = [ GlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", ) ] glossary = GlossaryManager(terms) builder = ContextBuilder(loader, glossary) ref = VerseReference(book="john", chapter=1, verse=1) context = builder.build_context(ref) # Term should be found via transliteration assert len(context.glossary_terms) == 1 assert context.glossary_terms[0].original == "λόγος" def test_find_terms_in_hebrew(self, tmp_path: Path) -> None: """Test finding terms in Hebrew text.""" # Create test data with Hebrew sources_dir = tmp_path / "sources" (sources_dir / "greek").mkdir(parents=True) (sources_dir / "japanese" / "meiji").mkdir(parents=True) (sources_dir / "japanese" / "taisho").mkdir(parents=True) (sources_dir / "english" / "kjv").mkdir(parents=True) (sources_dir / "english" / "asv").mkdir(parents=True) (tmp_path / "metadata").mkdir(parents=True) # Create Hebrew source with proper structure (sources_dir / "hebrew").mkdir(parents=True) hebrew_data = { "1:1": { "text": "בְּרֵאשִׁית בָּרָא אֱלֹהִים", "translit": "bereshit bara elohim", }, } with (sources_dir / "hebrew" / "genesis.json").open("w") as f: json.dump(hebrew_data, f) kjv_data = {"1:1": "In the beginning God created"} with (sources_dir / "english" / "kjv" / "genesis.json").open("w") as f: json.dump(kjv_data, f) loader = DataLoader(tmp_path) # Verify Hebrew is loaded verse_ref = VerseReference(book="genesis", chapter=1, verse=1) verse = loader.load_verse(verse_ref) assert verse.hebrew_text == "בְּרֵאשִׁית בָּרָא אֱלֹהִים" assert verse.hebrew_translit == "bereshit bara elohim" # Create glossary with Hebrew term terms = [ GlossaryTerm( original="אֱלֹהִים", transliteration="elohim", japanese="神", english="God", ) ] glossary = GlossaryManager(terms) builder = ContextBuilder(loader, glossary) ref = VerseReference(book="genesis", chapter=1, verse=1) context = builder.build_context(ref) # Should find the Hebrew term assert len(context.glossary_terms) > 0 assert context.glossary_terms[0].original == "אֱלֹהִים"