"""Tests to fill coverage gaps in various modules.""" import json import pytest from pathlib import Path from tempfile import TemporaryDirectory from src.context.cross_references import ( CrossReference, CrossReferenceManager, ReferenceType, ) from src.context.glossary import GlossaryManager from src.context.pericope_generator import PericopeGenerator from src.models.glossary import EnhancedGlossaryTerm, TheologicalDomain from src.utils.retry import RetryConfig, RetryState from src.utils.validation import ( normalize_book_name, parse_reference_string, validate_chapter, ) from src.exceptions import InvalidReferenceError, RateLimitError class TestGlossaryMorphologicalMatching: """Tests for morphological glossary matching.""" def test_get_by_lemma(self) -> None: """Test getting terms by lemma.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", lemma="λογος", ) ] manager = GlossaryManager(terms) # Get by lemma found = manager.get_by_lemma("λογος") assert found is not None assert found.original == "λόγος" def test_get_by_lemma_not_found(self) -> None: """Test getting by lemma returns None for unknown lemma.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", ) ] manager = GlossaryManager(terms) found = manager.get_by_lemma("unknown") assert found is None def test_get_by_strongs(self) -> None: """Test getting terms by Strong's number.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", strongs_number="G3056", ) ] manager = GlossaryManager(terms) found = manager.get_by_strongs("G3056") assert found is not None assert found.original == "λόγος" def test_get_by_strongs_case_insensitive(self) -> None: """Test Strong's lookup is case insensitive.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", strongs_number="G3056", ) ] manager = GlossaryManager(terms) # Lowercase should work too found = manager.get_by_strongs("g3056") assert found is not None assert found.original == "λόγος" def test_get_by_strongs_not_found(self) -> None: """Test Strong's lookup returns None for unknown number.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", ) ] manager = GlossaryManager(terms) found = manager.get_by_strongs("G9999") assert found is None def test_find_terms_with_inflected_forms(self) -> None: """Test finding terms via inflected forms.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", inflected_forms=["λόγον", "λόγου", "λόγῳ"], ) ] manager = GlossaryManager(terms) # Text containing inflected form text = "καὶ ὁ λόγον ἦν" found = manager.find_terms_in_text(text) assert len(found) == 1 assert found[0].original == "λόγος" def test_get_by_domain(self) -> None: """Test getting terms by theological domain.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", domain=TheologicalDomain.CHRISTOLOGY, ), EnhancedGlossaryTerm( original="σωτηρία", transliteration="soteria", japanese="救い", english="salvation", domain=TheologicalDomain.SOTERIOLOGY, ), ] manager = GlossaryManager(terms) christology = manager.get_by_domain(TheologicalDomain.CHRISTOLOGY) assert len(christology) == 1 assert christology[0].original == "λόγος" soteriology = manager.get_by_domain(TheologicalDomain.SOTERIOLOGY) assert len(soteriology) == 1 assert soteriology[0].original == "σωτηρία" def test_get_by_any_form(self) -> None: """Test getting terms by any matchable form.""" terms = [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", lemma="λογος", inflected_forms=["λόγον"], ) ] manager = GlossaryManager(terms) # Should find by original found = manager.get_by_any_form("λόγος") assert found is not None # Should find by lemma found = manager.get_by_any_form("λογος") assert found is not None # Should find by inflected form found = manager.get_by_any_form("λόγον") assert found is not None class TestCrossReferenceEdgeCases: """Tests for cross-reference edge cases.""" def test_find_references_to_verse_exact_match(self) -> None: """Test finding references to exact verse (not range).""" manager = CrossReferenceManager() ref = CrossReference( source_book="john", source_chapter=1, source_verse=1, target_book="genesis", target_chapter=1, target_verse_start=1, ) manager.add_reference(ref) # Should find reference TO verse 1 found = manager.find_references_to_verse("genesis", 1, 1) assert len(found) == 1 def test_get_consistency_notes_no_hint(self) -> None: """Test consistency notes when no translation hint.""" manager = CrossReferenceManager() ref = CrossReference( source_book="john", source_chapter=1, source_verse=23, target_book="isaiah", target_chapter=40, target_verse_start=3, reference_type=ReferenceType.QUOTATION, # No translation_hint ) manager.add_reference(ref) notes = manager.get_translation_consistency_notes("john", 1, 23) # Should still have note about quotation assert any("quotes" in n.lower() for n in notes) def test_format_for_prompt_with_max(self) -> None: """Test prompt formatting respects max_references.""" manager = CrossReferenceManager() for i in range(10): manager.add_reference( CrossReference( source_book="john", source_chapter=1, source_verse=1, target_book="genesis", target_chapter=1, target_verse_start=i + 1, ) ) refs = manager.find_references_for_verse("john", 1, 1) formatted = manager.format_for_prompt(refs, max_references=3) # Should only include 3 assert formatted.count("genesis") == 3 class TestPericopeGeneratorEdgeCases: """Tests for pericope generator edge cases.""" def test_generate_pericopes_no_markers(self) -> None: """Test generating pericopes when no paragraph markers exist.""" with TemporaryDirectory() as tmpdir: data_dir = Path(tmpdir) greek_dir = data_dir / "sources" / "greek" greek_dir.mkdir(parents=True) # Greek data without paragraph markers greek_data = { "1:1": {"text": "Ἐν ἀρχῇ ἦν ὁ λόγος"}, "1:2": {"text": "οὗτος ἦν ἐν ἀρχῇ"}, "1:3": {"text": "πάντα δι' αὐτοῦ ἐγένετο"}, } with (greek_dir / "test.json").open("w") as f: json.dump(greek_data, f) generator = PericopeGenerator(data_dir) index = generator.generate_pericopes("test", min_size=1) # Should create one pericope covering all verses assert len(index.pericopes) >= 1 def test_ref_sort_key_complex(self) -> None: """Test reference sort key with various formats.""" with TemporaryDirectory() as tmpdir: data_dir = Path(tmpdir) (data_dir / "sources" / "greek").mkdir(parents=True) generator = PericopeGenerator(data_dir) # Standard refs assert generator._ref_sort_key("10:25") == (10, 25) assert generator._ref_sort_key("1:1") < generator._ref_sort_key("1:2") assert generator._ref_sort_key("1:10") < generator._ref_sort_key("2:1") def test_split_large_pericope_exact_fit(self) -> None: """Test splitting when verses exactly fit max_size.""" with TemporaryDirectory() as tmpdir: data_dir = Path(tmpdir) (data_dir / "sources" / "greek").mkdir(parents=True) generator = PericopeGenerator(data_dir) # 10 verses with max_size=10 should not split verses = [f"1:{i}" for i in range(1, 11)] result = generator._split_large_pericope(verses, max_size=10) assert len(result) == 1 assert result[0] == verses class TestRetryEdgeCases: """Tests for retry utility edge cases.""" def test_retry_state_with_rate_limit_retry_after(self) -> None: """Test retry state uses retry_after from rate limit error.""" config = RetryConfig( base_delay=10.0, jitter=False, retryable_exceptions=(RateLimitError,), ) state = RetryState(config) error = RateLimitError("Rate limit", retry_after=0.001) state.should_retry(error) # Wait should use retry_after, not base_delay import time start = time.time() state.wait() elapsed = time.time() - start # Should be very fast (using retry_after=0.001) assert elapsed < 1.0 def test_retry_config_jitter_bounds(self) -> None: """Test jitter stays within bounds.""" config = RetryConfig(base_delay=1.0, jitter=True) for _ in range(50): delay = config.get_delay(0) # Jitter should be +/- 25% assert 0.75 <= delay <= 1.25 class TestValidationEdgeCases: """Tests for validation edge cases.""" def test_parse_reference_numbered_book_variants(self) -> None: """Test parsing various numbered book formats.""" # Space between number and name book, chapter, verse = parse_reference_string("1 John 1:1") assert book == "1john" assert chapter == 1 assert verse == 1 # No space book, chapter, verse = parse_reference_string("1John 1:1") assert book == "1john" def test_normalize_book_with_spaces_and_hyphens(self) -> None: """Test normalizing book names with various separators.""" assert normalize_book_name("song of solomon") == "songofsolomon" assert normalize_book_name("song-of-solomon") == "songofsolomon" def test_validate_chapter_known_books(self) -> None: """Test chapter validation for known books.""" # John has 21 chapters validate_chapter("john", 21) # Should pass with pytest.raises(InvalidReferenceError): validate_chapter("john", 22) # Should fail # Psalms has 150 chapters validate_chapter("psalms", 150) # Should pass class TestEnhancedGlossaryTerm: """Tests for enhanced glossary term model.""" def test_get_all_matchable_forms_with_lemma(self) -> None: """Test getting all matchable forms.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", lemma="λογος", inflected_forms=["λόγον", "λόγου"], ) forms = term.get_all_matchable_forms() assert "λόγος" in forms # Lemma should be included if different from original assert "λογος" in forms assert "λόγον" in forms assert "λόγου" in forms def test_get_all_matchable_forms_minimal(self) -> None: """Test matchable forms with minimal data.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", ) forms = term.get_all_matchable_forms() assert "λόγος" in forms # Transliteration is NOT included in get_all_matchable_forms # Only original, lemma, and inflected forms assert len(forms) == 1 def test_get_all_matchable_forms_same_lemma(self) -> None: """Test that lemma is not duplicated if same as original.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", lemma="λόγος", # Same as original ) forms = term.get_all_matchable_forms() # Should not duplicate the original assert len(forms) == 1 assert forms[0] == "λόγος" class TestCrossReferenceManagerSaveLoad: """Tests for cross-reference save/load functionality.""" def test_save_and_load_with_all_fields(self) -> None: """Test saving and loading cross-references with all fields.""" with TemporaryDirectory() as tmpdir: data_dir = Path(tmpdir) metadata_dir = data_dir / "metadata" metadata_dir.mkdir(parents=True) manager = CrossReferenceManager() manager._data_dir = data_dir # Add reference with all optional fields manager.add_reference( CrossReference( source_book="john", source_chapter=1, source_verse=1, target_book="genesis", target_chapter=1, target_verse_start=1, target_verse_end=3, reference_type=ReferenceType.ALLUSION, note="Echoes Genesis", translation_hint="Use similar phrasing", ) ) manager.save() # Load into new manager manager2 = CrossReferenceManager(data_dir) assert len(manager2) == 1 found = manager2.find_references_for_verse("john", 1, 1) assert len(found) == 1 assert found[0].note == "Echoes Genesis" assert found[0].translation_hint == "Use similar phrasing"