"""Tests for data loading and storage.""" import json import pytest from datetime import datetime from pathlib import Path from src.data.loader import DataLoader from src.data.storage import Storage from src.models.verse import ( TranslationOutput, VerseReference, ) @pytest.fixture def temp_data_dir(tmp_path: Path) -> Path: """Create a temporary data directory with test data.""" # Create directory structure sources_dir = tmp_path / "sources" (sources_dir / "greek").mkdir(parents=True) (sources_dir / "japanese" / "meiji").mkdir(parents=True) (sources_dir / "japanese" / "taisho").mkdir(parents=True) (sources_dir / "english" / "kjv").mkdir(parents=True) (sources_dir / "english" / "asv").mkdir(parents=True) (tmp_path / "metadata" / "john").mkdir(parents=True) (tmp_path / "output").mkdir(parents=True) (tmp_path / "progress").mkdir(parents=True) # Create test Greek data greek_data = { "1:1": {"text": "Ἐν ἀρχῇ ἦν ὁ λόγος", "translit": "En archē ēn ho logos"}, "1:2": {"text": "οὗτος ἦν ἐν ἀρχῇ", "translit": "houtos ēn en archē"}, "1:3": "Verse 3 text only", } with (sources_dir / "greek" / "john.json").open("w") as f: json.dump(greek_data, f) # Create test KJV data kjv_data = { "1:1": "In the beginning was the Word", "1:2": "The same was in the beginning with God", } with (sources_dir / "english" / "kjv" / "john.json").open("w") as f: json.dump(kjv_data, f) # Create test Meiji data meiji_data = { "1:1": "太初に言あり", } with (sources_dir / "japanese" / "meiji" / "john.json").open("w") as f: json.dump(meiji_data, f) # Create test metadata book_metadata = { "book": "john", "name_japanese": "ヨハネによる福音書", "name_english": "Gospel of John", "testament": "NT", "author": "John the Apostle", "themes": ["love", "light"], "description": "The fourth Gospel", "chapter_count": 21, "verse_count": 879, } with (tmp_path / "metadata" / "john" / "book_context.json").open("w") as f: json.dump(book_metadata, f) # Create chapter summaries chapter_summaries = { "1": { "summary": "The prologue", "key_events": ["Word becomes flesh"], "key_characters": ["Jesus"], "key_themes": ["Light"], } } with (tmp_path / "metadata" / "john" / "chapter_summaries.json").open("w") as f: json.dump(chapter_summaries, f) # Create glossary glossary = { "terms": [ { "original": "λόγος", "transliteration": "logos", "japanese": "言葉", "english": "word", "notes": "The Word", } ] } with (tmp_path / "metadata" / "glossary.json").open("w") as f: json.dump(glossary, f) return tmp_path class TestDataLoader: """Tests for DataLoader.""" def test_init(self, temp_data_dir: Path) -> None: """Test initializing data loader.""" loader = DataLoader(temp_data_dir) assert loader.data_dir == temp_data_dir def test_load_greek_book(self, temp_data_dir: Path) -> None: """Test loading Greek text.""" loader = DataLoader(temp_data_dir) greek = loader.load_greek_book("john") assert "1:1" in greek def test_load_greek_book_not_found(self, temp_data_dir: Path) -> None: """Test loading non-existent Greek text.""" loader = DataLoader(temp_data_dir) greek = loader.load_greek_book("nonexistent") assert greek == {} def test_load_japanese_meiji(self, temp_data_dir: Path) -> None: """Test loading Meiji translation.""" loader = DataLoader(temp_data_dir) meiji = loader.load_japanese_meiji("john") assert "1:1" in meiji def test_load_japanese_taisho(self, temp_data_dir: Path) -> None: """Test loading Taisho translation.""" loader = DataLoader(temp_data_dir) taisho = loader.load_japanese_taisho("john") assert taisho == {} # No test data for taisho def test_load_english_kjv(self, temp_data_dir: Path) -> None: """Test loading KJV translation.""" loader = DataLoader(temp_data_dir) kjv = loader.load_english_kjv("john") assert "1:1" in kjv def test_load_english_asv(self, temp_data_dir: Path) -> None: """Test loading ASV translation.""" loader = DataLoader(temp_data_dir) asv = loader.load_english_asv("john") assert asv == {} # No test data for ASV def test_load_verse(self, temp_data_dir: Path) -> None: """Test loading a verse with all sources.""" loader = DataLoader(temp_data_dir) ref = VerseReference(book="john", chapter=1, verse=1) verse = loader.load_verse(ref) assert verse.greek_text == "Ἐν ἀρχῇ ἦν ὁ λόγος" assert verse.greek_translit == "En archē ēn ho logos" assert verse.english_kjv == "In the beginning was the Word" assert verse.japanese_meiji == "太初に言あり" def test_load_verse_with_string_greek(self, temp_data_dir: Path) -> None: """Test loading verse with string-only Greek data.""" loader = DataLoader(temp_data_dir) ref = VerseReference(book="john", chapter=1, verse=3) verse = loader.load_verse(ref) assert verse.greek_text == "Verse 3 text only" assert verse.greek_translit is None def test_load_chapter_verses(self, temp_data_dir: Path) -> None: """Test loading all verses in a chapter.""" loader = DataLoader(temp_data_dir) verses = loader.load_chapter_verses("john", 1) assert len(verses) >= 2 # At least 2 verses in test data def test_load_book_metadata(self, temp_data_dir: Path) -> None: """Test loading book metadata.""" loader = DataLoader(temp_data_dir) meta = loader.load_book_metadata("john") assert meta is not None assert meta.name_japanese == "ヨハネによる福音書" def test_load_book_metadata_not_found(self, temp_data_dir: Path) -> None: """Test loading non-existent metadata.""" loader = DataLoader(temp_data_dir) meta = loader.load_book_metadata("nonexistent") assert meta is None def test_load_chapter_summary(self, temp_data_dir: Path) -> None: """Test loading chapter summary.""" loader = DataLoader(temp_data_dir) summary = loader.load_chapter_summary("john", 1) assert summary is not None assert summary.summary == "The prologue" def test_load_chapter_summary_not_found(self, temp_data_dir: Path) -> None: """Test loading non-existent chapter summary.""" loader = DataLoader(temp_data_dir) summary = loader.load_chapter_summary("john", 99) assert summary is None def test_load_glossary(self, temp_data_dir: Path) -> None: """Test loading glossary.""" loader = DataLoader(temp_data_dir) glossary = loader.load_glossary() assert len(glossary) == 1 assert glossary[0].original == "λόγος" def test_load_glossary_empty(self, tmp_path: Path) -> None: """Test loading empty glossary.""" (tmp_path / "sources").mkdir() (tmp_path / "metadata").mkdir() loader = DataLoader(tmp_path) glossary = loader.load_glossary() assert glossary == [] def test_get_book_chapters(self, temp_data_dir: Path) -> None: """Test getting chapter list.""" loader = DataLoader(temp_data_dir) chapters = loader.get_book_chapters("john") assert 1 in chapters def test_get_book_chapters_handles_invalid_keys(self, temp_data_dir: Path) -> None: """Test get_book_chapters handles non-numeric keys.""" # Add invalid key to Greek data greek_file = temp_data_dir / "sources" / "greek" / "john.json" with greek_file.open("r") as f: data = json.load(f) data["invalid"] = "test" with greek_file.open("w") as f: json.dump(data, f) loader = DataLoader(temp_data_dir) chapters = loader.get_book_chapters("john") assert 1 in chapters def test_load_hebrew_book(self, temp_data_dir: Path) -> None: """Test loading Hebrew text.""" # Create Hebrew source directory and file hebrew_dir = temp_data_dir / "sources" / "hebrew" hebrew_dir.mkdir(parents=True, exist_ok=True) hebrew_data = { "1:1": {"text": "בְּרֵאשִׁית", "translit": "bereshit"}, } with (hebrew_dir / "genesis.json").open("w") as f: json.dump(hebrew_data, f) loader = DataLoader(temp_data_dir) hebrew = loader.load_hebrew_book("genesis") assert "1:1" in hebrew def test_load_verse_with_hebrew(self, temp_data_dir: Path) -> None: """Test loading a verse with Hebrew text.""" # Create Hebrew source hebrew_dir = temp_data_dir / "sources" / "hebrew" hebrew_dir.mkdir(parents=True, exist_ok=True) hebrew_data = { "1:1": {"text": "בְּרֵאשִׁית", "translit": "bereshit"}, } with (hebrew_dir / "genesis.json").open("w") as f: json.dump(hebrew_data, f) loader = DataLoader(temp_data_dir) ref = VerseReference(book="genesis", chapter=1, verse=1) verse = loader.load_verse(ref) assert verse.hebrew_text == "בְּרֵאשִׁית" assert verse.hebrew_translit == "bereshit" def test_load_verse_with_hebrew_string_only(self, temp_data_dir: Path) -> None: """Test loading verse with string-only Hebrew data.""" hebrew_dir = temp_data_dir / "sources" / "hebrew" hebrew_dir.mkdir(parents=True, exist_ok=True) hebrew_data = { "1:1": "בְּרֵאשִׁית", # String instead of dict } with (hebrew_dir / "genesis.json").open("w") as f: json.dump(hebrew_data, f) loader = DataLoader(temp_data_dir) ref = VerseReference(book="genesis", chapter=1, verse=1) verse = loader.load_verse(ref) assert verse.hebrew_text == "בְּרֵאשִׁית" assert verse.hebrew_translit is None class TestStorage: """Tests for Storage.""" def test_init(self, temp_data_dir: Path) -> None: """Test initializing storage.""" storage = Storage(temp_data_dir) assert storage.data_dir == temp_data_dir def test_save_and_load_translation(self, temp_data_dir: Path) -> None: """Test saving and loading a translation.""" storage = Storage(temp_data_dir) ref = VerseReference(book="john", chapter=1, verse=1) output = TranslationOutput( reference=ref, text="初めに言葉があった。", model="gpt-4o", sources_used=["greek", "kjv"], glossary_terms_applied=["λόγος"], ) storage.save_translation(output) loaded = storage.load_translation(ref) assert loaded is not None assert loaded.text == "初めに言葉があった。" assert loaded.model == "gpt-4o" def test_load_translation_not_found(self, temp_data_dir: Path) -> None: """Test loading non-existent translation.""" storage = Storage(temp_data_dir) ref = VerseReference(book="john", chapter=99, verse=99) loaded = storage.load_translation(ref) assert loaded is None def test_load_chapter_translations(self, temp_data_dir: Path) -> None: """Test loading all translations for a chapter.""" storage = Storage(temp_data_dir) # Save a few translations for verse_num in [1, 2]: ref = VerseReference(book="john", chapter=1, verse=verse_num) output = TranslationOutput( reference=ref, text=f"Translation {verse_num}", model="test", ) storage.save_translation(output) translations = storage.load_chapter_translations("john", 1) assert len(translations) == 2 assert 1 in translations assert 2 in translations def test_is_verse_translated(self, temp_data_dir: Path) -> None: """Test checking if verse is translated.""" storage = Storage(temp_data_dir) ref = VerseReference(book="john", chapter=1, verse=1) assert storage.is_verse_translated(ref) is False output = TranslationOutput(reference=ref, text="Test", model="test") storage.save_translation(output) assert storage.is_verse_translated(ref) is True def test_get_progress(self, temp_data_dir: Path) -> None: """Test getting progress.""" storage = Storage(temp_data_dir) ref = VerseReference(book="john", chapter=1, verse=1) output = TranslationOutput(reference=ref, text="Test", model="test") storage.save_translation(output) progress = storage.get_progress("john") assert "completed" in progress assert "1" in progress["completed"] def test_get_completion_stats(self, temp_data_dir: Path) -> None: """Test getting completion stats.""" storage = Storage(temp_data_dir) # Save some translations for verse in [1, 2, 3]: ref = VerseReference(book="john", chapter=1, verse=verse) output = TranslationOutput(reference=ref, text="Test", model="test") storage.save_translation(output) stats = storage.get_completion_stats("john", 100) assert stats["completed"] == 3 assert stats["total"] == 100 assert stats["percentage"] == 3.0 def test_get_completion_stats_zero_total(self, temp_data_dir: Path) -> None: """Test completion stats with zero total.""" storage = Storage(temp_data_dir) stats = storage.get_completion_stats("john", 0) assert stats["percentage"] == 0 def test_save_and_load_translation_with_furigana( self, temp_data_dir: Path ) -> None: """Test saving and loading a translation with furigana.""" storage = Storage(temp_data_dir) ref = VerseReference(book="john", chapter=1, verse=1) annotations = [ { "start_offset": 0, "end_offset": 1, "base_text": "神", "reading": "かみ", }, { "start_offset": 2, "end_offset": 3, "base_text": "愛", "reading": "あい", }, ] output = TranslationOutput( reference=ref, text="神は愛です", text_with_furigana="神[かみ]は愛[あい]です", furigana_annotations=annotations, model="gpt-4o", ) storage.save_translation(output) loaded = storage.load_translation(ref) assert loaded is not None assert loaded.text == "神は愛です" assert loaded.text_with_furigana == "神[かみ]は愛[あい]です" assert len(loaded.furigana_annotations) == 2 assert loaded.furigana_annotations[0]["base_text"] == "神" assert loaded.furigana_annotations[1]["reading"] == "あい" def test_load_translation_backward_compatibility( self, temp_data_dir: Path ) -> None: """Test loading translation without furigana fields (backward compatibility).""" storage = Storage(temp_data_dir) ref = VerseReference(book="john", chapter=1, verse=1) # Manually create a chapter file without furigana fields chapter_file = storage.output_dir / "john" / "chapter_01.json" chapter_file.parent.mkdir(parents=True, exist_ok=True) chapter_data = { "book": "john", "chapter": 1, "verses": { "1": { "text": "初めに言葉があった。", "model": "test-model", "timestamp": "2024-01-01T00:00:00", "prompt_version": "v1.0", "sources_used": [], "glossary_terms_applied": [], } }, } with chapter_file.open("w", encoding="utf-8") as f: json.dump(chapter_data, f) loaded = storage.load_translation(ref) assert loaded is not None assert loaded.text == "初めに言葉があった。" assert loaded.text_with_furigana == "" assert loaded.furigana_annotations == [] def test_load_chapter_translations_with_furigana( self, temp_data_dir: Path ) -> None: """Test loading chapter translations includes furigana fields.""" storage = Storage(temp_data_dir) # Save translations with furigana for verse_num in [1, 2]: ref = VerseReference(book="john", chapter=1, verse=verse_num) output = TranslationOutput( reference=ref, text=f"翻訳{verse_num}", text_with_furigana=f"翻訳[ほんやく]{verse_num}", furigana_annotations=[ { "start_offset": 0, "end_offset": 2, "base_text": "翻訳", "reading": "ほんやく", } ], model="test", ) storage.save_translation(output) translations = storage.load_chapter_translations("john", 1) assert len(translations) == 2 assert translations[1].text_with_furigana == "翻訳[ほんやく]1" assert len(translations[1].furigana_annotations) == 1 class TestStorageArchive: """Tests for archive storage functionality.""" def test_init_with_archive_disabled(self, temp_data_dir: Path) -> None: """Test storage init with archive disabled by default.""" storage = Storage(temp_data_dir) assert storage.enable_archive is False assert not storage.archive_dir.exists() def test_init_with_archive_enabled(self, temp_data_dir: Path) -> None: """Test storage init with archive enabled.""" storage = Storage(temp_data_dir, enable_archive=True) assert storage.enable_archive is True assert storage.archive_dir.exists() def test_enable_archive_property(self, temp_data_dir: Path) -> None: """Test enable_archive property setter.""" storage = Storage(temp_data_dir) assert storage.enable_archive is False storage.enable_archive = True assert storage.enable_archive is True assert storage.archive_dir.exists() def test_save_translation_archive(self, temp_data_dir: Path) -> None: """Test saving translation archive.""" from src.protos.converters import ( TranslationPromptInputProto, TranslationResultProto, VerseProto, VerseReferenceProto, ) storage = Storage(temp_data_dir, enable_archive=True) ref = VerseReference(book="john", chapter=1, verse=1) input_proto = TranslationPromptInputProto( target_verse=VerseProto( reference=VerseReferenceProto(book="john", chapter=1, verse=1), greek_text="Ἐν ἀρχῇ", ), system_prompt="System", user_prompt="User", model="test-model", ) output_proto = TranslationResultProto( reference=VerseReferenceProto(book="john", chapter=1, verse=1), text="初めに", model="test-model", ) archive_path = storage.save_translation_archive(ref, input_proto, output_proto) assert archive_path.exists() assert (archive_path / "verse_01_input.msgpack").exists() assert (archive_path / "verse_01_output.msgpack").exists() def test_load_translation_archive(self, temp_data_dir: Path) -> None: """Test loading translation archive.""" from src.protos.converters import ( TranslationPromptInputProto, TranslationResultProto, VerseProto, VerseReferenceProto, ) storage = Storage(temp_data_dir, enable_archive=True) ref = VerseReference(book="john", chapter=1, verse=1) input_proto = TranslationPromptInputProto( target_verse=VerseProto( reference=VerseReferenceProto(book="john", chapter=1, verse=1), greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος", ), system_prompt="You are a translator", user_prompt="Translate this", model="claude-3-opus", ) output_proto = TranslationResultProto( reference=VerseReferenceProto(book="john", chapter=1, verse=1), text="初めに言葉があった", model="claude-3-opus", input_hash="abc123", ) storage.save_translation_archive(ref, input_proto, output_proto) result = storage.load_translation_archive(ref) assert result is not None loaded_input, loaded_output = result assert loaded_input.target_verse.greek_text == "Ἐν ἀρχῇ ἦν ὁ λόγος" assert loaded_input.system_prompt == "You are a translator" assert loaded_output.text == "初めに言葉があった" assert loaded_output.input_hash == "abc123" def test_load_translation_archive_not_found(self, temp_data_dir: Path) -> None: """Test loading non-existent archive returns None.""" storage = Storage(temp_data_dir, enable_archive=True) ref = VerseReference(book="john", chapter=99, verse=99) result = storage.load_translation_archive(ref) assert result is None def test_archive_directory_structure(self, temp_data_dir: Path) -> None: """Test archive creates proper directory structure.""" from src.protos.converters import ( TranslationPromptInputProto, TranslationResultProto, VerseReferenceProto, ) storage = Storage(temp_data_dir, enable_archive=True) # Save multiple verses for chapter in [1, 2]: for verse in [1, 2, 3]: ref = VerseReference(book="john", chapter=chapter, verse=verse) input_proto = TranslationPromptInputProto(model="test") output_proto = TranslationResultProto( reference=VerseReferenceProto( book="john", chapter=chapter, verse=verse ), text=f"Translation {chapter}:{verse}", model="test", ) storage.save_translation_archive(ref, input_proto, output_proto) # Check directory structure assert (storage.archive_dir / "john").exists() assert (storage.archive_dir / "john" / "chapter_01").exists() assert (storage.archive_dir / "john" / "chapter_02").exists() # Check files ch1_path = storage.archive_dir / "john" / "chapter_01" assert (ch1_path / "verse_01_input.msgpack").exists() assert (ch1_path / "verse_02_input.msgpack").exists() assert (ch1_path / "verse_03_input.msgpack").exists() def test_archive_roundtrip_with_provenance(self, temp_data_dir: Path) -> None: """Test archive roundtrip includes provenance data.""" from datetime import datetime, timezone from src.protos.converters import ( TranslationPromptInputProto, TranslationProvenanceProto, TranslationResultProto, VerseReferenceProto, ) storage = Storage(temp_data_dir, enable_archive=True) ref = VerseReference(book="matthew", chapter=5, verse=3) provenance = TranslationProvenanceProto( source_texts={"greek": "Μακάριοι"}, model="claude-opus", prompt_version="v2.0", started_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc), completed_at=datetime(2024, 1, 1, 12, 0, 30, tzinfo=timezone.utc), ) input_proto = TranslationPromptInputProto( system_prompt="Translate", user_prompt="Verse", model="claude-opus", ) output_proto = TranslationResultProto( reference=VerseReferenceProto(book="matthew", chapter=5, verse=3), text="心の貧しい人々は幸いです", model="claude-opus", provenance=provenance, ) storage.save_translation_archive(ref, input_proto, output_proto) result = storage.load_translation_archive(ref) assert result is not None _, loaded_output = result assert loaded_output.provenance is not None assert loaded_output.provenance.model == "claude-opus" assert loaded_output.provenance.source_texts == {"greek": "Μακάριοι"}