OPEN SOURCE
software/tests/test_final_coverage.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"""Final coverage tests to reach 98% coverage."""23import asyncio4import json5import pytest6from pathlib import Path7from unittest.mock import AsyncMock, MagicMock, patch89from src.context.builder import ContextBuilder10from src.context.glossary import GlossaryManager11from src.data.loader import DataLoader12from src.data.storage import Storage13from src.exceptions import (14ChunkParsingError,15ClaudeAPIError,16QualityThresholdError,17RateLimitError,18)19from src.models.evaluation import VerseEvaluation, EvaluationScore20from src.models.glossary import EnhancedGlossaryTerm21from src.models.verse import VerseReference22from src.pipeline.prompt import PromptBuilder23from src.pipeline.translator import Translator24from src.utils.retry import RetryConfig, RetryState, retry_with_backoff, async_retry_with_backoff25from src.utils.validation import parse_reference_string, is_valid_book26from src.exceptions import InvalidReferenceError272829@pytest.fixture30def temp_data_dir(tmp_path: Path) -> Path:31"""Create temporary data directory."""32sources_dir = tmp_path / "sources"33(sources_dir / "greek").mkdir(parents=True)34(sources_dir / "english" / "kjv").mkdir(parents=True)35(tmp_path / "metadata").mkdir(parents=True)36(tmp_path / "output").mkdir(parents=True)3738greek_data = {39"1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},40"1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},41}42with (sources_dir / "greek" / "john.json").open("w") as f:43json.dump(greek_data, f)4445kjv_data = {"1:1": "In the beginning", "1:2": "The same was"}46with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:47json.dump(kjv_data, f)4849return tmp_path505152class TestRetryDecoratorEdgeCases:53"""Tests for retry decorator edge cases."""5455def test_retry_decorator_default_config(self) -> None:56"""Test retry decorator with default config."""57call_count = 05859@retry_with_backoff()60def successful_func():61nonlocal call_count62call_count += 163return "success"6465result = successful_func()66assert result == "success"67assert call_count == 16869def test_retry_decorator_with_on_retry(self) -> None:70"""Test retry decorator calls on_retry callback."""71retry_calls = []72config = RetryConfig(73max_attempts=3,74base_delay=0.001,75jitter=False,76retryable_exceptions=(RateLimitError,),77)7879def on_retry(exc, attempt):80retry_calls.append((str(exc), attempt))8182@retry_with_backoff(config, on_retry=on_retry)83def failing_then_success():84if len(retry_calls) < 1:85raise RateLimitError("Rate limit")86return "success"8788result = failing_then_success()89assert result == "success"90assert len(retry_calls) == 19192@pytest.mark.asyncio93async def test_async_retry_decorator_default_config(self) -> None:94"""Test async retry decorator with default config."""95call_count = 09697@async_retry_with_backoff()98async def successful_func():99nonlocal call_count100call_count += 1101return "success"102103result = await successful_func()104assert result == "success"105assert call_count == 1106107@pytest.mark.asyncio108async def test_async_retry_with_callback(self) -> None:109"""Test async retry decorator calls on_retry callback."""110retry_calls = []111config = RetryConfig(112max_attempts=3,113base_delay=0.001,114jitter=False,115retryable_exceptions=(RateLimitError,),116)117118def on_retry(exc, attempt):119retry_calls.append((str(exc), attempt))120121@async_retry_with_backoff(config, on_retry=on_retry)122async def failing_then_success():123if len(retry_calls) < 1:124raise RateLimitError("Rate limit")125return "success"126127result = await failing_then_success()128assert result == "success"129assert len(retry_calls) == 1130131132class TestRetryStateEdgeCases:133"""Tests for RetryState edge cases."""134135@pytest.mark.asyncio136async def test_async_wait_with_rate_limit_retry_after(self) -> None:137"""Test async_wait uses retry_after from rate limit error."""138config = RetryConfig(139base_delay=10.0,140jitter=False,141retryable_exceptions=(RateLimitError,),142)143state = RetryState(config)144145error = RateLimitError("Rate limit", retry_after=0.001)146state.should_retry(error)147148import time149start = time.time()150await state.async_wait()151elapsed = time.time() - start152153assert elapsed < 1.0154155156class TestValidationEdgeCases:157"""Tests for validation edge cases."""158159def test_parse_reference_invalid_format_no_colon(self) -> None:160"""Test parsing reference without colon separator."""161with pytest.raises(InvalidReferenceError, match="Expected format"):162parse_reference_string("john 1")163164def test_parse_reference_invalid_chapter(self) -> None:165"""Test parsing reference with non-numeric chapter."""166with pytest.raises(InvalidReferenceError):167parse_reference_string("john abc:1")168169def test_is_valid_book_empty(self) -> None:170"""Test is_valid_book with empty string."""171assert is_valid_book("") is False172173174class TestGlossaryAddTermEdgeCases:175"""Tests for GlossaryManager add_term edge cases."""176177def test_add_term_with_all_morphological_fields(self) -> None:178"""Test adding term with all morphological fields."""179manager = GlossaryManager()180term = EnhancedGlossaryTerm(181original="λόγος",182transliteration="logos",183japanese="言葉",184english="word",185lemma="λογος",186strongs_number="G3056",187inflected_forms=["λόγον", "λόγου"],188)189manager.add_term(term)190191# Should be findable by all indexes192assert manager.get_by_original("λόγος") is not None193assert manager.get_by_lemma("λογος") is not None194assert manager.get_by_strongs("G3056") is not None195assert manager.get_by_any_form("λόγον") is not None196197def test_add_term_rebuilds_match_pattern(self) -> None:198"""Test that adding term rebuilds the match pattern."""199manager = GlossaryManager()200term = EnhancedGlossaryTerm(201original="λόγος",202transliteration="logos",203japanese="言葉",204english="word",205)206manager.add_term(term)207208# Pattern should now include the term209found = manager.find_terms_in_text("καὶ ὁ λόγος ἦν")210assert len(found) == 1211212213class TestTranslatorQualityGateEdgeCases:214"""Tests for translator quality gate edge cases."""215216@patch("src.pipeline.translator.subprocess.run")217def test_translate_with_evaluator(218self, mock_run: MagicMock, temp_data_dir: Path219) -> None:220"""Test translation with auto-evaluation."""221loader = DataLoader(temp_data_dir)222storage = Storage(temp_data_dir)223glossary = GlossaryManager()224context_builder = ContextBuilder(loader, glossary)225prompt_builder = PromptBuilder(glossary)226227translator = Translator(228context_builder,229prompt_builder,230storage,231quality_threshold=70.0,232enforce_quality_gate=False,233)234235mock_run.return_value = MagicMock(236returncode=0, stdout="翻訳テスト。", stderr=""237)238239ref = VerseReference(book="john", chapter=1, verse=1)240result = translator.translate_verse(ref, skip_existing=False)241242assert result is not None243assert result.text == "翻訳テスト。"244245246class TestTranslatorJSONExtractionEdgeCases:247"""Tests for JSON extraction edge cases."""248249def test_extract_json_from_position_invalid(self, temp_data_dir: Path) -> None:250"""Test extracting JSON from invalid position."""251loader = DataLoader(temp_data_dir)252glossary = GlossaryManager()253storage = Storage(temp_data_dir)254context_builder = ContextBuilder(loader, glossary)255prompt_builder = PromptBuilder(glossary)256translator = Translator(context_builder, prompt_builder, storage)257258# Text without valid JSON object259text = "This is just text with no JSON"260result = translator._extract_json_from_position(text, 0)261assert result is None262263def test_extract_json_from_position_nested(self, temp_data_dir: Path) -> None:264"""Test extracting nested JSON object."""265loader = DataLoader(temp_data_dir)266glossary = GlossaryManager()267storage = Storage(temp_data_dir)268context_builder = ContextBuilder(loader, glossary)269prompt_builder = PromptBuilder(glossary)270translator = Translator(context_builder, prompt_builder, storage)271272# Nested JSON273text = '{"outer": {"inner": "value"}}'274result = translator._extract_json_from_position(text, 0)275assert result is not None276assert "outer" in result277278def test_try_parse_json_not_dict(self, temp_data_dir: Path) -> None:279"""Test _try_parse_json returns None for non-dict JSON."""280loader = DataLoader(temp_data_dir)281glossary = GlossaryManager()282storage = Storage(temp_data_dir)283context_builder = ContextBuilder(loader, glossary)284prompt_builder = PromptBuilder(glossary)285translator = Translator(context_builder, prompt_builder, storage)286287# Valid JSON but not a dict288result = translator._try_parse_json('["list", "items"]')289assert result is None290291# Valid JSON string292result = translator._try_parse_json('"just a string"')293assert result is None294295296class TestTranslatorAsyncEdgeCases:297"""Tests for translator async edge cases."""298299@patch("src.pipeline.translator.asyncio.create_subprocess_exec")300@pytest.mark.asyncio301async def test_call_claude_async_success(302self, mock_subprocess: AsyncMock, temp_data_dir: Path303) -> None:304"""Test successful async Claude call."""305loader = DataLoader(temp_data_dir)306storage = Storage(temp_data_dir)307glossary = GlossaryManager()308context_builder = ContextBuilder(loader, glossary)309prompt_builder = PromptBuilder(glossary)310311translator = Translator(312context_builder,313prompt_builder,314storage,315)316317mock_process = AsyncMock()318mock_process.returncode = 0319mock_process.communicate.return_value = ("非同期翻訳。".encode(), b"")320mock_subprocess.return_value = mock_process321322result = await translator._call_claude_async("system", "user")323assert result == "非同期翻訳。"324325326class TestTranslatorCallClaudeEdgeCases:327"""Tests for _call_claude edge cases."""328329@patch("src.pipeline.translator.subprocess.run")330def test_call_claude_with_timeout(331self, mock_run: MagicMock, temp_data_dir: Path332) -> None:333"""Test _call_claude passes timeout correctly."""334loader = DataLoader(temp_data_dir)335storage = Storage(temp_data_dir)336glossary = GlossaryManager()337context_builder = ContextBuilder(loader, glossary)338prompt_builder = PromptBuilder(glossary)339340translator = Translator(341context_builder,342prompt_builder,343storage,344subprocess_timeout=45,345)346347mock_run.return_value = MagicMock(348returncode=0, stdout="翻訳結果", stderr=""349)350351translator._call_claude("system", "user")352353# Verify timeout was passed354call_kwargs = mock_run.call_args[1]355assert call_kwargs["timeout"] == 45356357@patch("src.pipeline.translator.subprocess.run")358def test_call_claude_network_error(359self, mock_run: MagicMock, temp_data_dir: Path360) -> None:361"""Test _call_claude handles network errors."""362loader = DataLoader(temp_data_dir)363storage = Storage(temp_data_dir)364glossary = GlossaryManager()365context_builder = ContextBuilder(loader, glossary)366prompt_builder = PromptBuilder(glossary)367368translator = Translator(context_builder, prompt_builder, storage)369370mock_run.return_value = MagicMock(371returncode=1, stdout="", stderr="Network connection failed"372)373374with pytest.raises(ClaudeAPIError, match="Network error"):375translator._call_claude("system", "user")376377378class TestGlossaryFindTermsWithPositions:379"""Tests for find_terms_in_text_with_positions."""380381def test_find_terms_with_positions(self) -> None:382"""Test finding terms with their positions."""383terms = [384EnhancedGlossaryTerm(385original="λόγος",386transliteration="logos",387japanese="言葉",388english="word",389)390]391manager = GlossaryManager(terms)392393text = "καὶ ὁ λόγος ἦν"394found = manager.find_terms_in_text_with_positions(text)395396assert len(found) == 1397term, start, end = found[0]398assert term.original == "λόγος"399assert start == 6 # Position of λόγος in text400assert end == 11401402def test_find_terms_with_positions_empty(self) -> None:403"""Test finding terms with positions when none found."""404terms = [405EnhancedGlossaryTerm(406original="λόγος",407transliteration="logos",408japanese="言葉",409english="word",410)411]412manager = GlossaryManager(terms)413414text = "no terms here"415found = manager.find_terms_in_text_with_positions(text)416417assert len(found) == 0418419420class TestValidationSanitizePathEdgeCases:421"""Tests for sanitize_book_name_for_path edge cases."""422423def test_sanitize_path_invalid_characters(self) -> None:424"""Test sanitize path rejects invalid path characters."""425from src.utils.validation import sanitize_book_name_for_path426427# Valid book names should pass428result = sanitize_book_name_for_path("john")429assert result == "john"430431432class TestTranslatorTimeoutEdgeCases:433"""Tests for translator timeout handling."""434435@patch("src.pipeline.translator.subprocess.run")436def test_call_claude_timeout_expired(437self, mock_run: MagicMock, temp_data_dir: Path438) -> None:439"""Test _call_claude handles TimeoutExpired."""440import subprocess441442loader = DataLoader(temp_data_dir)443storage = Storage(temp_data_dir)444glossary = GlossaryManager()445context_builder = ContextBuilder(loader, glossary)446prompt_builder = PromptBuilder(glossary)447448# Disable retries to test exception directly449translator = Translator(450context_builder,451prompt_builder,452storage,453subprocess_timeout=1,454retry_config=RetryConfig(max_attempts=1, retryable_exceptions=()),455)456457# Mock timeout458mock_run.side_effect = subprocess.TimeoutExpired("claude", 1)459460with pytest.raises(ClaudeAPIError, match="timed out"):461translator._call_claude("system", "user")462463@patch("src.pipeline.translator.subprocess.run")464def test_call_claude_file_not_found(465self, mock_run: MagicMock, temp_data_dir: Path466) -> None:467"""Test _call_claude handles FileNotFoundError (CLI not installed)."""468loader = DataLoader(temp_data_dir)469storage = Storage(temp_data_dir)470glossary = GlossaryManager()471context_builder = ContextBuilder(loader, glossary)472prompt_builder = PromptBuilder(glossary)473474translator = Translator(context_builder, prompt_builder, storage)475476# Mock file not found477mock_run.side_effect = FileNotFoundError("claude not found")478479from src.exceptions import CLINotFoundError480with pytest.raises(CLINotFoundError):481translator._call_claude("system", "user")482483484class TestValidateReferenceExists:485"""Tests for validate_reference_exists function."""486487def test_validate_reference_exists_not_found(self, temp_data_dir: Path) -> None:488"""Test validate_reference_exists raises when verse not found."""489from src.utils.validation import validate_reference_exists490from src.exceptions import VerseNotFoundError491492loader = DataLoader(temp_data_dir)493494# Verse 99:99 doesn't exist in our test data495with pytest.raises(VerseNotFoundError):496validate_reference_exists(loader, "john", 99, 99)497498def test_validate_reference_exists_success(self, temp_data_dir: Path) -> None:499"""Test validate_reference_exists passes for existing verse."""500from src.utils.validation import validate_reference_exists501502loader = DataLoader(temp_data_dir)503504# Verse 1:1 exists in our test data505validate_reference_exists(loader, "john", 1, 1) # Should not raise506507508class TestCrossReferenceEdgeCases:509"""Additional tests for cross-reference edge cases."""510511def test_cross_reference_source_ref_property(self) -> None:512"""Test CrossReference source_ref property."""513from src.context.cross_references import CrossReference514515ref = CrossReference(516source_book="john",517source_chapter=3,518source_verse=16,519target_book="genesis",520target_chapter=1,521target_verse_start=1,522)523assert ref.source_ref == "john 3:16"524525def test_cross_reference_target_ref_with_range(self) -> None:526"""Test CrossReference target_ref with verse range."""527from src.context.cross_references import CrossReference528529ref = CrossReference(530source_book="john",531source_chapter=1,532source_verse=1,533target_book="genesis",534target_chapter=1,535target_verse_start=1,536target_verse_end=5,537)538assert ref.target_ref == "genesis 1:1-5"539540541class TestPericopeGeneratorEdgeCases:542"""Additional tests for pericope generator edge cases."""543544def test_generate_and_save(self, temp_data_dir: Path) -> None:545"""Test generate_and_save creates output file."""546from src.context.pericope_generator import PericopeGenerator547548# Add Greek data549greek_dir = temp_data_dir / "sources" / "greek"550greek_data = {551"1:1": {"text": "¶ Ἐν ἀρχῇ ἦν"},552"1:2": {"text": "οὗτος ἦν ἐν"},553"1:3": {"text": "¶ πάντα δι᾽ αὐτοῦ"},554}555with (greek_dir / "testbook.json").open("w") as f:556json.dump(greek_data, f)557558generator = PericopeGenerator(temp_data_dir)559index = generator.generate_and_save("testbook")560561assert index is not None562563# Check file was created564output_path = temp_data_dir / "metadata" / "pericopes_generated.json"565assert output_path.exists()566