OPEN SOURCE
software/tests/test_pipeline_extended.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
1"""Extended tests for translation pipeline to improve coverage."""23import json4import pytest5from pathlib import Path6from unittest.mock import AsyncMock, 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 (14BookMetadata,15ChapterSummary,16GlossaryTerm,17TranslationContext,18Verse,19VerseReference,20)21from src.pipeline.prompt import PromptBuilder22from src.pipeline.translator import Translator232425@pytest.fixture26def temp_data_dir_extended(tmp_path: Path) -> Path:27"""Create temp data directory with more test data."""28sources_dir = tmp_path / "sources"29(sources_dir / "greek").mkdir(parents=True)30(sources_dir / "english" / "kjv").mkdir(parents=True)31(sources_dir / "english" / "asv").mkdir(parents=True)32(sources_dir / "japanese" / "meiji").mkdir(parents=True)33(sources_dir / "japanese" / "taisho").mkdir(parents=True)34(tmp_path / "metadata" / "john").mkdir(parents=True)35(tmp_path / "output").mkdir(parents=True)36(tmp_path / "progress").mkdir(parents=True)3738# Greek data with multiple verses39greek_data = {40"1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},41"1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},42"1:3": {"text": "πάντα δι' αὐτοῦ", "translit": "panta di autou"},43}44with (sources_dir / "greek" / "john.json").open("w") as f:45json.dump(greek_data, f)4647kjv_data = {48"1:1": "In the beginning",49"1:2": "The same was",50"1:3": "All things were made",51}52with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:53json.dump(kjv_data, f)5455# Chapter summary56chapter_summaries = {57"1": {58"summary": "The prologue",59"key_events": [],60"key_characters": [],61"key_themes": [],62}63}64with (tmp_path / "metadata" / "john" / "chapter_summaries.json").open("w") as f:65json.dump(chapter_summaries, f)6667# Book metadata68book_metadata = {69"book": "john",70"name_japanese": "ヨハネによる福音書",71"name_english": "Gospel of John",72"testament": "NT",73"author": "John",74"themes": ["love"],75"description": "The fourth Gospel",76"chapter_count": 21,77"verse_count": 879,78}79with (tmp_path / "metadata" / "john" / "book_context.json").open("w") as f:80json.dump(book_metadata, f)8182return tmp_path838485class TestPromptBuilderExtended:86"""Extended tests for PromptBuilder."""8788def test_build_prompt_with_preceding_verses(self) -> None:89"""Test prompt includes preceding verses."""90glossary = GlossaryManager()91builder = PromptBuilder(glossary)9293target_verse = Verse(94reference=VerseReference(book="john", chapter=1, verse=3),95greek_text="πάντα δι' αὐτοῦ",96english_kjv="All things were made by him",97)98preceding = [99Verse(100reference=VerseReference(book="john", chapter=1, verse=1),101english_kjv="In the beginning was the Word",102),103Verse(104reference=VerseReference(book="john", chapter=1, verse=2),105english_kjv="The same was in the beginning with God",106),107]108109context = TranslationContext(110target_verse=target_verse,111preceding_verses=preceding,112)113114prompt = builder.build_prompt(context)115116assert "Preceding verses:" in prompt117assert "In the beginning was the Word" in prompt118119def test_build_prompt_with_chapter_summary(self) -> None:120"""Test prompt includes chapter summary."""121glossary = GlossaryManager()122builder = PromptBuilder(glossary)123124target_verse = Verse(125reference=VerseReference(book="john", chapter=1, verse=1),126greek_text="Ἐν ἀρχῇ",127english_kjv="In the beginning",128)129chapter_summary = ChapterSummary(130book="john",131chapter=1,132summary="This is the prologue to John's Gospel.",133key_events=[],134key_characters=[],135key_themes=[],136)137138context = TranslationContext(139target_verse=target_verse,140chapter_summary=chapter_summary,141)142143prompt = builder.build_prompt(context)144145assert "Chapter Summary:" in prompt146assert "This is the prologue" in prompt147148def test_build_prompt_with_themes(self) -> None:149"""Test prompt includes book themes."""150glossary = GlossaryManager()151builder = PromptBuilder(glossary)152153target_verse = Verse(154reference=VerseReference(book="john", chapter=1, verse=1),155greek_text="Ἐν ἀρχῇ",156english_kjv="In the beginning",157)158book_metadata = BookMetadata(159book="john",160name_japanese="ヨハネによる福音書",161name_english="Gospel of John",162testament="NT",163author="John",164themes=["love", "light", "life"],165description="The fourth Gospel",166chapter_count=21,167verse_count=879,168)169170context = TranslationContext(171target_verse=target_verse,172book_metadata=book_metadata,173)174175prompt = builder.build_prompt(context)176177assert "Themes:" in prompt178assert "love, light, life" in prompt179180181class TestTranslatorExtended:182"""Extended tests for Translator."""183184@patch("src.pipeline.translator.subprocess.run")185def test_translate_chapter(186self, mock_run: MagicMock, temp_data_dir_extended: Path187) -> None:188"""Test translating a full chapter."""189loader = DataLoader(temp_data_dir_extended)190storage = Storage(temp_data_dir_extended)191glossary = GlossaryManager()192context_builder = ContextBuilder(loader, glossary)193prompt_builder = PromptBuilder(glossary)194195translator = Translator(196context_builder, prompt_builder, storage, rate_limit_delay=0197)198199# Mock subprocess.run200mock_run.return_value = MagicMock(201returncode=0,202stdout="翻訳テスト。",203stderr="",204)205206# Track progress207progress_log = []208209def progress_callback(current: int, total: int, ref: VerseReference) -> None:210progress_log.append((current, total, str(ref)))211212outputs = translator.translate_chapter(213"john", 1, skip_existing=False, progress_callback=progress_callback214)215216assert len(outputs) == 3 # 3 verses in test data217assert len(progress_log) == 3218219@patch("src.pipeline.translator.asyncio.create_subprocess_exec")220@pytest.mark.asyncio221async def test_translate_verse_async(222self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path223) -> None:224"""Test async translation of a verse."""225loader = DataLoader(temp_data_dir_extended)226storage = Storage(temp_data_dir_extended)227glossary = GlossaryManager()228context_builder = ContextBuilder(loader, glossary)229prompt_builder = PromptBuilder(glossary)230231translator = Translator(context_builder, prompt_builder, storage)232233# Mock async subprocess234mock_process = AsyncMock()235mock_process.returncode = 0236mock_process.communicate.return_value = ("非同期翻訳テスト。".encode(), b"")237mock_subprocess.return_value = mock_process238239ref = VerseReference(book="john", chapter=1, verse=1)240output = await translator.translate_verse_async(ref, skip_existing=False)241242assert output.text == "非同期翻訳テスト。"243244@pytest.mark.asyncio245async def test_translate_verse_async_skip_existing(246self, temp_data_dir_extended: Path247) -> None:248"""Test async translation skips existing."""249from src.models.verse import TranslationOutput250251loader = DataLoader(temp_data_dir_extended)252storage = Storage(temp_data_dir_extended)253glossary = GlossaryManager()254context_builder = ContextBuilder(loader, glossary)255prompt_builder = PromptBuilder(glossary)256257translator = Translator(context_builder, prompt_builder, storage)258259# Save existing260ref = VerseReference(book="john", chapter=1, verse=1)261existing = TranslationOutput(262reference=ref, text="既存の翻訳。", model="test"263)264storage.save_translation(existing)265266output = await translator.translate_verse_async(ref, skip_existing=True)267assert output.text == "既存の翻訳。"268269@patch("src.pipeline.translator.asyncio.create_subprocess_exec")270@pytest.mark.asyncio271async def test_translate_chapter_async(272self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path273) -> None:274"""Test async translation of a chapter."""275loader = DataLoader(temp_data_dir_extended)276storage = Storage(temp_data_dir_extended)277glossary = GlossaryManager()278context_builder = ContextBuilder(loader, glossary)279prompt_builder = PromptBuilder(glossary)280281translator = Translator(282context_builder, prompt_builder, storage, rate_limit_delay=0283)284285# Mock async subprocess286mock_process = AsyncMock()287mock_process.returncode = 0288mock_process.communicate.return_value = ("非同期チャプター翻訳。".encode(), b"")289mock_subprocess.return_value = mock_process290291outputs = await translator.translate_chapter_async(292"john", 1, skip_existing=False, max_concurrent=2293)294295assert len(outputs) == 3296297@patch("src.pipeline.translator.asyncio.create_subprocess_exec")298@pytest.mark.asyncio299async def test_call_claude_async_raises_on_error(300self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path301) -> None:302"""Test _call_claude_async raises ClaudeAPIError on CLI failure."""303loader = DataLoader(temp_data_dir_extended)304storage = Storage(temp_data_dir_extended)305glossary = GlossaryManager()306context_builder = ContextBuilder(loader, glossary)307prompt_builder = PromptBuilder(glossary)308309translator = Translator(context_builder, prompt_builder, storage)310311# Mock async subprocess with error312mock_process = AsyncMock()313mock_process.returncode = 1314mock_process.communicate.return_value = (b"", b"Error: CLI failed")315mock_subprocess.return_value = mock_process316317with pytest.raises(ClaudeAPIError, match="Claude CLI failed"):318await translator._call_claude_async("system", "user")319320321class TestContextBuilderExtended:322"""Extended tests for ContextBuilder."""323324def test_find_terms_in_greek_translit_duplicate(self, tmp_path: Path) -> None:325"""Test that duplicate terms are not added from transliteration."""326sources_dir = tmp_path / "sources"327(sources_dir / "greek").mkdir(parents=True)328(sources_dir / "hebrew").mkdir(parents=True)329(sources_dir / "japanese" / "meiji").mkdir(parents=True)330(sources_dir / "japanese" / "taisho").mkdir(parents=True)331(sources_dir / "english" / "kjv").mkdir(parents=True)332(sources_dir / "english" / "asv").mkdir(parents=True)333(tmp_path / "metadata").mkdir(parents=True)334335# Create Greek data with both original and transliteration containing the term336greek_data = {337"1:1": {338"text": "λόγος was here", # Contains original339"translit": "logos was here", # Contains transliteration340},341}342with (sources_dir / "greek" / "john.json").open("w") as f:343json.dump(greek_data, f)344345loader = DataLoader(tmp_path)346347# Create glossary where term can be found in both original and translit348terms = [349GlossaryTerm(350original="λόγος",351transliteration="logos",352japanese="言葉",353english="word",354)355]356glossary = GlossaryManager(terms)357358builder = ContextBuilder(loader, glossary)359360ref = VerseReference(book="john", chapter=1, verse=1)361context = builder.build_context(ref)362363# Should only have one term, not duplicates364assert len(context.glossary_terms) == 1365assert context.glossary_terms[0].original == "λόγος"366367def test_find_terms_only_in_transliteration(self, tmp_path: Path) -> None:368"""Test finding terms only in transliteration (not in original)."""369sources_dir = tmp_path / "sources"370(sources_dir / "greek").mkdir(parents=True)371(sources_dir / "hebrew").mkdir(parents=True)372(sources_dir / "japanese" / "meiji").mkdir(parents=True)373(sources_dir / "japanese" / "taisho").mkdir(parents=True)374(sources_dir / "english" / "kjv").mkdir(parents=True)375(sources_dir / "english" / "asv").mkdir(parents=True)376(tmp_path / "metadata").mkdir(parents=True)377378# Create Greek data with only transliteration containing the term379greek_data = {380"1:1": {381"text": "Some other Greek text", # Does NOT contain original382"translit": "Some text with logos", # Contains transliteration383},384}385with (sources_dir / "greek" / "john.json").open("w") as f:386json.dump(greek_data, f)387388loader = DataLoader(tmp_path)389390# Glossary term found only via transliteration391terms = [392GlossaryTerm(393original="λόγος",394transliteration="logos",395japanese="言葉",396english="word",397)398]399glossary = GlossaryManager(terms)400401builder = ContextBuilder(loader, glossary)402403ref = VerseReference(book="john", chapter=1, verse=1)404context = builder.build_context(ref)405406# Term should be found via transliteration407assert len(context.glossary_terms) == 1408assert context.glossary_terms[0].original == "λόγος"409410def test_find_terms_in_hebrew(self, tmp_path: Path) -> None:411"""Test finding terms in Hebrew text."""412# Create test data with Hebrew413sources_dir = tmp_path / "sources"414(sources_dir / "greek").mkdir(parents=True)415(sources_dir / "japanese" / "meiji").mkdir(parents=True)416(sources_dir / "japanese" / "taisho").mkdir(parents=True)417(sources_dir / "english" / "kjv").mkdir(parents=True)418(sources_dir / "english" / "asv").mkdir(parents=True)419(tmp_path / "metadata").mkdir(parents=True)420421# Create Hebrew source with proper structure422(sources_dir / "hebrew").mkdir(parents=True)423hebrew_data = {424"1:1": {425"text": "בְּרֵאשִׁית בָּרָא אֱלֹהִים",426"translit": "bereshit bara elohim",427},428}429with (sources_dir / "hebrew" / "genesis.json").open("w") as f:430json.dump(hebrew_data, f)431432kjv_data = {"1:1": "In the beginning God created"}433with (sources_dir / "english" / "kjv" / "genesis.json").open("w") as f:434json.dump(kjv_data, f)435436loader = DataLoader(tmp_path)437438# Verify Hebrew is loaded439verse_ref = VerseReference(book="genesis", chapter=1, verse=1)440verse = loader.load_verse(verse_ref)441assert verse.hebrew_text == "בְּרֵאשִׁית בָּרָא אֱלֹהִים"442assert verse.hebrew_translit == "bereshit bara elohim"443444# Create glossary with Hebrew term445terms = [446GlossaryTerm(447original="אֱלֹהִים",448transliteration="elohim",449japanese="神",450english="God",451)452]453glossary = GlossaryManager(terms)454455builder = ContextBuilder(loader, glossary)456457ref = VerseReference(book="genesis", chapter=1, verse=1)458context = builder.build_context(ref)459460# Should find the Hebrew term461assert len(context.glossary_terms) > 0462assert context.glossary_terms[0].original == "אֱלֹהִים"463