OPEN SOURCE
software/tests/test_final_coverage_2.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 - part 2."""23import asyncio4import json5import pytest6from pathlib import Path7from unittest.mock import AsyncMock, MagicMock, patch89from src.context.builder import ContextBuilder10from src.context.glossary import GlossaryManager11from src.context.cross_references import CrossReferenceManager, CrossReference, ReferenceType12from src.context.pericope_generator import PericopeGenerator13from src.data.loader import DataLoader14from src.data.storage import Storage15from src.exceptions import ClaudeAPIError, RateLimitError16from src.models.verse import VerseReference17from src.pipeline.prompt import PromptBuilder18from src.pipeline.translator import Translator19from src.utils.retry import RetryConfig, RetryState202122@pytest.fixture23def temp_data_dir(tmp_path: Path) -> Path:24"""Create temporary data directory."""25sources_dir = tmp_path / "sources"26(sources_dir / "greek").mkdir(parents=True)27(sources_dir / "english" / "kjv").mkdir(parents=True)28(tmp_path / "metadata").mkdir(parents=True)29(tmp_path / "output").mkdir(parents=True)3031greek_data = {32"1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},33"1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},34}35with (sources_dir / "greek" / "john.json").open("w") as f:36json.dump(greek_data, f)3738kjv_data = {"1:1": "In the beginning", "1:2": "The same was"}39with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:40json.dump(kjv_data, f)4142return tmp_path434445class TestTranslatorAsyncRetry:46"""Tests for async retry logic in translator."""4748@patch("src.pipeline.translator.asyncio.create_subprocess_exec")49@pytest.mark.asyncio50async def test_call_claude_async_rate_limit_retry(51self, mock_subprocess: AsyncMock, temp_data_dir: Path52) -> None:53"""Test async Claude call retries on rate limit."""54loader = DataLoader(temp_data_dir)55storage = Storage(temp_data_dir)56glossary = GlossaryManager()57context_builder = ContextBuilder(loader, glossary)58prompt_builder = PromptBuilder(glossary)5960retry_config = RetryConfig(61max_attempts=3,62base_delay=0.001,63jitter=False,64retryable_exceptions=(RateLimitError,),65)66translator = Translator(67context_builder,68prompt_builder,69storage,70retry_config=retry_config,71)7273call_count = 07475async def create_process(*args, **kwargs):76nonlocal call_count77call_count += 178mock_process = AsyncMock()79if call_count < 3:80mock_process.returncode = 181mock_process.communicate.return_value = (b"", b"rate limit exceeded")82else:83mock_process.returncode = 084mock_process.communicate.return_value = ("成功。".encode(), b"")85return mock_process8687mock_subprocess.side_effect = create_process8889result = await translator._call_claude_async("system", "user")90assert result == "成功。"91assert call_count == 39293@patch("src.pipeline.translator.asyncio.create_subprocess_exec")94@pytest.mark.asyncio95async def test_call_claude_async_timeout(96self, mock_subprocess: AsyncMock, temp_data_dir: Path97) -> None:98"""Test async Claude call handles timeout."""99loader = DataLoader(temp_data_dir)100storage = Storage(temp_data_dir)101glossary = GlossaryManager()102context_builder = ContextBuilder(loader, glossary)103prompt_builder = PromptBuilder(glossary)104105translator = Translator(106context_builder,107prompt_builder,108storage,109retry_config=RetryConfig(max_attempts=1, retryable_exceptions=()),110)111112async def timeout_process(*args, **kwargs):113mock_process = AsyncMock()114mock_process.communicate.side_effect = asyncio.TimeoutError()115return mock_process116117mock_subprocess.side_effect = timeout_process118119with pytest.raises(ClaudeAPIError, match="timed out"):120await translator._call_claude_async("system", "user")121122123class TestRetryStateEdgeCases:124"""Edge case tests for RetryState."""125126def test_reset_clears_all_state(self) -> None:127"""Test reset clears attempt count and exception."""128config = RetryConfig(max_attempts=3, base_delay=0.001)129state = RetryState(config)130131# Set some state132state.attempt = 2133state.last_exception = RateLimitError("test")134135state.reset()136137assert state.attempt == 0138assert state.last_exception is None139140141class TestCrossReferenceManagerEdgeCases:142"""Additional cross-reference edge cases."""143144def test_find_references_by_type_no_match(self) -> None:145"""Test finding references by type when none match."""146manager = CrossReferenceManager()147manager.add_reference(148CrossReference(149source_book="john",150source_chapter=1,151source_verse=1,152target_book="genesis",153target_chapter=1,154target_verse_start=1,155reference_type=ReferenceType.ALLUSION,156)157)158159# Look for QUOTATION but only ALLUSION exists160found = manager.find_references_by_type("john", 1, 1, ReferenceType.QUOTATION)161assert len(found) == 0162163def test_get_parallel_passages_empty(self) -> None:164"""Test getting parallel passages when none exist."""165manager = CrossReferenceManager()166manager.add_reference(167CrossReference(168source_book="john",169source_chapter=1,170source_verse=1,171target_book="genesis",172target_chapter=1,173target_verse_start=1,174reference_type=ReferenceType.ALLUSION,175)176)177178parallels = manager.get_parallel_passages("john", 1, 1)179assert len(parallels) == 0180181def test_get_quotation_sources_empty(self) -> None:182"""Test getting quotation sources when none exist."""183manager = CrossReferenceManager()184manager.add_reference(185CrossReference(186source_book="john",187source_chapter=1,188source_verse=1,189target_book="genesis",190target_chapter=1,191target_verse_start=1,192reference_type=ReferenceType.THEMATIC,193)194)195196quotes = manager.get_quotation_sources("john", 1, 1)197assert len(quotes) == 0198199200class TestPericopeGeneratorMerge:201"""Tests for pericope merge functionality."""202203def test_merge_small_pericopes(self, tmp_path: Path) -> None:204"""Test merging small pericopes into larger ones."""205# Was a fixed /tmp/test_pericope_merge (via a no-op `with Path(...)`),206# which concurrent test runs on one machine share and race on.207greek_dir = tmp_path / "sources" / "greek"208greek_dir.mkdir(parents=True, exist_ok=True)209210# Create data with many small paragraph breaks211greek_data = {212"1:1": {"text": "¶ verse1"},213"1:2": {"text": "¶ verse2"},214"1:3": {"text": "¶ verse3"},215"1:4": {"text": "verse4"},216"1:5": {"text": "verse5"},217}218with (greek_dir / "test.json").open("w") as f:219json.dump(greek_data, f)220221generator = PericopeGenerator(tmp_path)222# With min_size=3, small pericopes should be merged223index = generator.generate_pericopes("test", min_size=3)224225# Should have fewer pericopes due to merging226assert len(index.pericopes) <= 2227228229class TestValidationPathSafety:230"""Tests for path safety validation."""231232def test_sanitize_book_already_normalized(self) -> None:233"""Test sanitizing already normalized book name."""234from src.utils.validation import sanitize_book_name_for_path235236result = sanitize_book_name_for_path("matthew")237assert result == "matthew"238239def test_sanitize_book_with_alias(self) -> None:240"""Test sanitizing book alias."""241from src.utils.validation import sanitize_book_name_for_path242243result = sanitize_book_name_for_path("jn")244assert result == "john"245