"""Tests for enhanced glossary models and migration utilities.""" import json import pytest from pathlib import Path from src.context.glossary import GlossaryManager from src.data.migrations import ( infer_domain_from_notes, load_glossary_any_version, migrate_glossary_file, migrate_glossary_v1_to_v2, ) from src.models.glossary import ( EnhancedGlossaryTerm, GlossaryTerm, TheologicalDomain, ) class TestTheologicalDomain: """Tests for TheologicalDomain enum.""" def test_all_domains_exist(self) -> None: """Test all expected domains are defined.""" expected = [ "christology", "soteriology", "eschatology", "pneumatology", "ecclesiology", "hamartiology", "theology_proper", "anthropology", "general", ] domain_values = [d.value for d in TheologicalDomain] for exp in expected: assert exp in domain_values def test_domain_string_value(self) -> None: """Test domain values are strings.""" assert TheologicalDomain.CHRISTOLOGY.value == "christology" assert TheologicalDomain.GENERAL.value == "general" class TestEnhancedGlossaryTerm: """Tests for EnhancedGlossaryTerm model.""" def test_create_basic_term(self) -> None: """Test creating term with only required fields.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", ) assert term.original == "λόγος" assert term.domain == TheologicalDomain.GENERAL assert term.semantic_range == [] assert term.frequency == 0 def test_create_enhanced_term(self) -> None: """Test creating term with all enhanced fields.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", notes="The divine Word", definition="The Greek concept of reason and word", semantic_range=["word", "reason", "message"], domain=TheologicalDomain.CHRISTOLOGY, usage_examples=["John 1:1", "John 1:14"], frequency=330, related_terms=["θεός", "σάρξ"], avoid_translations=["ことば"], ) assert term.definition is not None assert len(term.semantic_range) == 3 assert term.domain == TheologicalDomain.CHRISTOLOGY assert term.frequency == 330 assert len(term.related_terms) == 2 assert len(term.avoid_translations) == 1 def test_has_enhanced_metadata_false(self) -> None: """Test has_enhanced_metadata returns False for basic term.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", ) assert term.has_enhanced_metadata() is False def test_has_enhanced_metadata_true_with_definition(self) -> None: """Test has_enhanced_metadata returns True with definition.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", definition="Test definition", ) assert term.has_enhanced_metadata() is True def test_has_enhanced_metadata_true_with_domain(self) -> None: """Test has_enhanced_metadata returns True with non-general domain.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", domain=TheologicalDomain.CHRISTOLOGY, ) assert term.has_enhanced_metadata() is True def test_has_enhanced_metadata_true_with_semantic_range(self) -> None: """Test has_enhanced_metadata returns True with semantic range.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", semantic_range=["word", "reason"], ) assert term.has_enhanced_metadata() is True def test_has_enhanced_metadata_true_with_frequency(self) -> None: """Test has_enhanced_metadata returns True with frequency > 0.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", frequency=10, ) assert term.has_enhanced_metadata() is True def test_has_enhanced_metadata_true_with_avoid(self) -> None: """Test has_enhanced_metadata returns True with avoid_translations.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", avoid_translations=["ことば"], ) assert term.has_enhanced_metadata() is True def test_backward_compatibility_alias(self) -> None: """Test GlossaryTerm is an alias for EnhancedGlossaryTerm.""" assert GlossaryTerm is EnhancedGlossaryTerm def test_frequency_non_negative(self) -> None: """Test frequency must be non-negative.""" with pytest.raises(ValueError): EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", frequency=-1, ) class TestGlossaryManagerDomain: """Tests for GlossaryManager domain functionality.""" @pytest.fixture def enhanced_terms(self) -> list[EnhancedGlossaryTerm]: """Create test terms with domains.""" return [ EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", domain=TheologicalDomain.CHRISTOLOGY, related_terms=["θεός"], ), EnhancedGlossaryTerm( original="θεός", transliteration="theos", japanese="神", english="God", domain=TheologicalDomain.THEOLOGY_PROPER, related_terms=["λόγος", "πνεῦμα"], ), EnhancedGlossaryTerm( original="πνεῦμα", transliteration="pneuma", japanese="霊", english="spirit", domain=TheologicalDomain.PNEUMATOLOGY, ), EnhancedGlossaryTerm( original="ἁμαρτία", transliteration="hamartia", japanese="罪", english="sin", domain=TheologicalDomain.HAMARTIOLOGY, ), ] def test_get_by_domain( self, enhanced_terms: list[EnhancedGlossaryTerm] ) -> None: """Test getting terms by domain.""" manager = GlossaryManager(enhanced_terms) christology_terms = manager.get_by_domain(TheologicalDomain.CHRISTOLOGY) assert len(christology_terms) == 1 assert christology_terms[0].original == "λόγος" def test_get_by_domain_empty( self, enhanced_terms: list[EnhancedGlossaryTerm] ) -> None: """Test getting terms from empty domain.""" manager = GlossaryManager(enhanced_terms) eschatology_terms = manager.get_by_domain(TheologicalDomain.ESCHATOLOGY) assert len(eschatology_terms) == 0 def test_get_by_domain_returns_copy( self, enhanced_terms: list[EnhancedGlossaryTerm] ) -> None: """Test get_by_domain returns a copy.""" manager = GlossaryManager(enhanced_terms) terms = manager.get_by_domain(TheologicalDomain.CHRISTOLOGY) terms.clear() # Original should still have the term assert len(manager.get_by_domain(TheologicalDomain.CHRISTOLOGY)) == 1 def test_get_related_terms( self, enhanced_terms: list[EnhancedGlossaryTerm] ) -> None: """Test getting related terms.""" manager = GlossaryManager(enhanced_terms) theos_term = manager.get_by_original("θεός") assert theos_term is not None related = manager.get_related_terms(theos_term) assert len(related) == 2 originals = [t.original for t in related] assert "λόγος" in originals assert "πνεῦμα" in originals def test_get_related_terms_not_found( self, enhanced_terms: list[EnhancedGlossaryTerm] ) -> None: """Test related terms with missing references.""" term = EnhancedGlossaryTerm( original="test", transliteration="test", japanese="テスト", english="test", related_terms=["nonexistent"], ) manager = GlossaryManager([term]) related = manager.get_related_terms(term) assert len(related) == 0 def test_domain_index_updated_on_add(self) -> None: """Test domain index is updated when adding terms.""" manager = GlossaryManager() term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", domain=TheologicalDomain.CHRISTOLOGY, ) manager.add_term(term) assert len(manager.get_by_domain(TheologicalDomain.CHRISTOLOGY)) == 1 def test_get_domain_summary( self, enhanced_terms: list[EnhancedGlossaryTerm] ) -> None: """Test getting domain summary.""" manager = GlossaryManager(enhanced_terms) summary = manager.get_domain_summary() assert summary["christology"] == 1 assert summary["theology_proper"] == 1 assert summary["pneumatology"] == 1 assert summary["hamartiology"] == 1 assert summary["general"] == 0 class TestGlossaryManagerEnhancedFormatting: """Tests for enhanced prompt formatting.""" def test_format_with_semantic_range(self) -> None: """Test formatting includes semantic range.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", semantic_range=["word", "reason", "message"], ) manager = GlossaryManager([term]) formatted = manager.format_for_prompt([term]) assert "Semantic range: word, reason, message" in formatted def test_format_with_avoid_translations(self) -> None: """Test formatting includes avoid translations.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", avoid_translations=["ことば", "話"], ) manager = GlossaryManager([term]) formatted = manager.format_for_prompt([term]) assert "Avoid: ことば, 話" in formatted def test_format_with_definition(self) -> None: """Test formatting includes definition.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", definition="The Greek concept of word and reason", ) manager = GlossaryManager([term]) formatted = manager.format_for_prompt([term]) assert "Definition: The Greek concept" in formatted def test_format_without_enhanced(self) -> None: """Test formatting without enhanced metadata.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", semantic_range=["word", "reason"], ) manager = GlossaryManager([term]) formatted = manager.format_for_prompt([term], include_enhanced=False) assert "Semantic range" not in formatted def test_format_core_always_included(self) -> None: """Test core fields always included.""" term = EnhancedGlossaryTerm( original="λόγος", transliteration="logos", japanese="言葉", english="word", notes="The Word", ) manager = GlossaryManager([term]) formatted = manager.format_for_prompt([term]) assert "λόγος" in formatted assert "logos" in formatted assert "言葉" in formatted assert "[The Word]" in formatted class TestInferDomainFromNotes: """Tests for domain inference from notes.""" def test_infer_christology_from_son(self) -> None: """Test inferring Christology from 'Son'.""" domain = infer_domain_from_notes("Son of God", "Son") assert domain == TheologicalDomain.CHRISTOLOGY def test_infer_christology_from_word(self) -> None: """Test inferring Christology from 'Word'.""" domain = infer_domain_from_notes("The divine Word", "Word") assert domain == TheologicalDomain.CHRISTOLOGY def test_infer_pneumatology_from_spirit(self) -> None: """Test inferring Pneumatology from 'Spirit'.""" domain = infer_domain_from_notes("Holy Spirit", "Spirit") assert domain == TheologicalDomain.PNEUMATOLOGY def test_infer_soteriology_from_grace(self) -> None: """Test inferring Soteriology from 'grace'.""" domain = infer_domain_from_notes("Divine grace", "grace") assert domain == TheologicalDomain.SOTERIOLOGY def test_infer_hamartiology_from_sin(self) -> None: """Test inferring Hamartiology from 'sin'.""" domain = infer_domain_from_notes("Missing the mark", "sin") assert domain == TheologicalDomain.HAMARTIOLOGY def test_infer_theology_proper_from_god(self) -> None: """Test inferring Theology Proper from 'God'.""" domain = infer_domain_from_notes("Standard translation for God", "God") assert domain == TheologicalDomain.THEOLOGY_PROPER def test_infer_general_default(self) -> None: """Test default to GENERAL when no keywords match.""" domain = infer_domain_from_notes("Some random note", "something") assert domain == TheologicalDomain.GENERAL def test_infer_with_none_notes(self) -> None: """Test inference with None notes.""" domain = infer_domain_from_notes(None, "general") assert domain == TheologicalDomain.GENERAL class TestMigrateGlossaryV1ToV2: """Tests for glossary migration.""" def test_migrate_basic(self) -> None: """Test basic v1 to v2 migration.""" v1_data = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", "notes": "The divine Word", } ] } v2_data = migrate_glossary_v1_to_v2(v1_data) assert v2_data["version"] == "2.0" assert len(v2_data["terms"]) == 1 term = v2_data["terms"][0] assert term["domain"] == "christology" # Inferred from "Word" assert term["semantic_range"] == [] assert term["avoid_translations"] == [] def test_migrate_preserves_core_fields(self) -> None: """Test migration preserves all core fields.""" v1_data = { "terms": [ { "original": "θεός", "transliteration": "theos", "japanese": "神", "english": "God", "notes": "Standard translation", } ] } v2_data = migrate_glossary_v1_to_v2(v1_data) term = v2_data["terms"][0] assert term["original"] == "θεός" assert term["transliteration"] == "theos" assert term["japanese"] == "神" assert term["english"] == "God" assert term["notes"] == "Standard translation" def test_migrate_multiple_terms(self) -> None: """Test migrating multiple terms.""" v1_data = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", }, { "original": "θεός", "transliteration": "theos", "japanese": "神", "english": "God", }, ] } v2_data = migrate_glossary_v1_to_v2(v1_data) assert len(v2_data["terms"]) == 2 def test_migrate_empty_terms(self) -> None: """Test migrating empty terms list.""" v1_data = {"terms": []} v2_data = migrate_glossary_v1_to_v2(v1_data) assert v2_data["version"] == "2.0" assert v2_data["terms"] == [] class TestLoadGlossaryAnyVersion: """Tests for loading glossary from any version.""" def test_load_v1_format(self, tmp_path: Path) -> None: """Test loading v1 format file.""" v1_data = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", "notes": "The divine Word", } ] } path = tmp_path / "glossary.json" with path.open("w", encoding="utf-8") as f: json.dump(v1_data, f) terms = load_glossary_any_version(path) assert len(terms) == 1 assert isinstance(terms[0], EnhancedGlossaryTerm) assert terms[0].domain == TheologicalDomain.CHRISTOLOGY def test_load_v2_format(self, tmp_path: Path) -> None: """Test loading v2 format file.""" v2_data = { "version": "2.0", "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", "domain": "christology", "semantic_range": ["word", "reason"], } ], } path = tmp_path / "glossary.json" with path.open("w", encoding="utf-8") as f: json.dump(v2_data, f) terms = load_glossary_any_version(path) assert len(terms) == 1 assert terms[0].semantic_range == ["word", "reason"] def test_load_nonexistent_file(self, tmp_path: Path) -> None: """Test loading nonexistent file returns empty list.""" path = tmp_path / "nonexistent.json" terms = load_glossary_any_version(path) assert terms == [] class TestMigrateGlossaryFile: """Tests for file-based migration.""" def test_migrate_file_creates_backup(self, tmp_path: Path) -> None: """Test migration creates backup file.""" v1_data = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", } ] } path = tmp_path / "glossary.json" with path.open("w", encoding="utf-8") as f: json.dump(v1_data, f) migrate_glossary_file(path) backup_path = path.with_suffix(".json.v1.bak") assert backup_path.exists() def test_migrate_file_updates_content(self, tmp_path: Path) -> None: """Test migration updates file content.""" v1_data = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", } ] } path = tmp_path / "glossary.json" with path.open("w", encoding="utf-8") as f: json.dump(v1_data, f) migrate_glossary_file(path) with path.open("r", encoding="utf-8") as f: migrated = json.load(f) assert migrated["version"] == "2.0" def test_migrate_file_already_v2(self, tmp_path: Path) -> None: """Test migration of already v2 file returns terms.""" v2_data = { "version": "2.0", "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", "domain": "christology", } ], } path = tmp_path / "glossary.json" with path.open("w", encoding="utf-8") as f: json.dump(v2_data, f) terms = migrate_glossary_file(path) assert len(terms) == 1 # No backup should be created assert not path.with_suffix(".json.v1.bak").exists() def test_migrate_file_to_different_output(self, tmp_path: Path) -> None: """Test migration to different output file.""" v1_data = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", } ] } input_path = tmp_path / "v1_glossary.json" output_path = tmp_path / "v2_glossary.json" with input_path.open("w", encoding="utf-8") as f: json.dump(v1_data, f) migrate_glossary_file(input_path, output_path) assert output_path.exists() with output_path.open("r", encoding="utf-8") as f: migrated = json.load(f) assert migrated["version"] == "2.0" def test_migrate_file_no_backup(self, tmp_path: Path) -> None: """Test migration without backup.""" v1_data = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "Word", } ] } path = tmp_path / "glossary.json" with path.open("w", encoding="utf-8") as f: json.dump(v1_data, f) migrate_glossary_file(path, backup=False) backup_path = path.with_suffix(".json.v1.bak") assert not backup_path.exists()