公開前の確認用サイト
日常訳NICHIJOYAKUEnglish ↗

OPEN SOURCE

software/tests/test_translation_memory.py

このサイトで読み、必要なファイルを保存できます。

日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。

← オープンソースの案内へ

software/tests/test_translation_memory.py798 行このファイルを保存 ↓

ZIPに含まれるものと同じ公開ソースです。行番号から、その行へのリンクを保存できます。

ファイルの確認情報SHA-256 82d12255d4a6a2bd7fd10d8de9d682813f2bdbc4ee2f681b7262a034d609f1e2
1"""Tests for Translation Memory models and manager."""23import json4import pytest5from datetime import datetime6from pathlib import Path78from src.context.translation_memory import TranslationMemoryManager9from src.models.translation_memory import (10    MatchType,11    SegmentLevel,12    TMMatch,13    TMSegment,14    TMStatistics,15    calculate_similarity,16    normalize_text,17    _levenshtein_distance,18)19from src.models.verse import VerseReference202122class TestNormalizeText:23    """Tests for text normalization."""2425    def test_lowercase(self) -> None:26        """Test lowercasing."""27        assert normalize_text("ABC") == "abc"2829    def test_remove_punctuation(self) -> None:30        """Test punctuation removal."""31        result = normalize_text("Hello, World!")32        assert "," not in result33        assert "!" not in result3435    def test_collapse_whitespace(self) -> None:36        """Test whitespace collapsing."""37        result = normalize_text("hello   world")38        assert result == "hello world"3940    def test_unicode_normalization(self) -> None:41        """Test Unicode normalization."""42        # Greek text should be normalized43        text = "λόγος"44        result = normalize_text(text)45        assert result == "λόγος"4647    def test_preserve_greek_characters(self) -> None:48        """Test Greek characters are preserved."""49        text = "Ἐν ἀρχῇ ἦν ὁ λόγος"50        result = normalize_text(text)51        assert "λόγος" in result525354class TestCalculateSimilarity:55    """Tests for similarity calculation."""5657    def test_exact_match(self) -> None:58        """Test exact match returns 1.0."""59        assert calculate_similarity("hello", "hello") == 1.06061    def test_completely_different(self) -> None:62        """Test completely different returns low score."""63        score = calculate_similarity("abc", "xyz")64        assert score < 0.56566    def test_similar_strings(self) -> None:67        """Test similar strings return high score."""68        score = calculate_similarity("hello", "hallo")69        assert score > 0.77071    def test_empty_strings(self) -> None:72        """Test empty string handling."""73        assert calculate_similarity("", "") == 1.074        assert calculate_similarity("hello", "") == 0.075        assert calculate_similarity("", "hello") == 0.07677    def test_symmetric(self) -> None:78        """Test similarity is symmetric."""79        s1 = calculate_similarity("hello", "hallo")80        s2 = calculate_similarity("hallo", "hello")81        assert s1 == s2828384class TestLevenshteinDistance:85    """Tests for Levenshtein distance."""8687    def test_identical(self) -> None:88        """Test identical strings have distance 0."""89        assert _levenshtein_distance("hello", "hello") == 09091    def test_single_insertion(self) -> None:92        """Test single insertion."""93        assert _levenshtein_distance("hello", "hellos") == 19495    def test_single_deletion(self) -> None:96        """Test single deletion."""97        assert _levenshtein_distance("hello", "hell") == 19899    def test_single_substitution(self) -> None:100        """Test single substitution."""101        assert _levenshtein_distance("hello", "hallo") == 1102103    def test_empty_to_string(self) -> None:104        """Test empty to string."""105        assert _levenshtein_distance("", "hello") == 5106107    def test_string_to_empty(self) -> None:108        """Test string to empty."""109        assert _levenshtein_distance("hello", "") == 5110111112class TestSegmentLevel:113    """Tests for SegmentLevel enum."""114115    def test_values(self) -> None:116        """Test enum values."""117        assert SegmentLevel.VERSE.value == "verse"118        assert SegmentLevel.PHRASE.value == "phrase"119120121class TestMatchType:122    """Tests for MatchType enum."""123124    def test_values(self) -> None:125        """Test enum values."""126        assert MatchType.EXACT.value == "exact"127        assert MatchType.FUZZY.value == "fuzzy"128        assert MatchType.PHRASE.value == "phrase"129130131class TestTMSegment:132    """Tests for TMSegment model."""133134    def test_create_basic(self) -> None:135        """Test basic segment creation."""136        ref = VerseReference(book="john", chapter=1, verse=1)137        segment = TMSegment(138            source_text="Ἐν ἀρχῇ ἦν ὁ λόγος",139            source_normalized="ἐν ἀρχῇ ἦν ὁ λόγος",140            target_text="初めに言葉があった",141            reference=ref,142        )143        assert segment.source_text == "Ἐν ἀρχῇ ἦν ὁ λόγος"144        assert segment.level == SegmentLevel.VERSE145        assert segment.reuse_count == 0146147    def test_create_factory(self) -> None:148        """Test factory method with auto-normalization."""149        ref = VerseReference(book="john", chapter=1, verse=1)150        segment = TMSegment.create(151            source_text="Ἐν ἀρχῇ ἦν ὁ λόγος",152            target_text="初めに言葉があった",153            reference=ref,154        )155        # Normalized should be lowercase without extra punctuation156        assert segment.source_normalized == normalize_text("Ἐν ἀρχῇ ἦν ὁ λόγος")157158    def test_create_with_metadata(self) -> None:159        """Test creation with metadata."""160        ref = VerseReference(book="john", chapter=1, verse=1)161        segment = TMSegment.create(162            source_text="Test",163            target_text="テスト",164            reference=ref,165            metadata={"model": "claude-sonnet", "version": "1.0"},166        )167        assert segment.metadata["model"] == "claude-sonnet"168169    def test_auto_generated_id(self) -> None:170        """Test ID is auto-generated."""171        ref = VerseReference(book="john", chapter=1, verse=1)172        segment = TMSegment.create(173            source_text="Test",174            target_text="テスト",175            reference=ref,176        )177        assert segment.id is not None178        assert len(segment.id) > 0179180    def test_increment_reuse(self) -> None:181        """Test reuse counter increment."""182        ref = VerseReference(book="john", chapter=1, verse=1)183        segment = TMSegment.create(184            source_text="Test",185            target_text="テスト",186            reference=ref,187        )188        assert segment.reuse_count == 0189        segment.increment_reuse()190        assert segment.reuse_count == 1191        segment.increment_reuse()192        assert segment.reuse_count == 2193194195class TestTMMatch:196    """Tests for TMMatch model."""197198    @pytest.fixture199    def sample_segment(self) -> TMSegment:200        """Create a sample segment."""201        ref = VerseReference(book="john", chapter=1, verse=1)202        return TMSegment.create(203            source_text="Test",204            target_text="テスト",205            reference=ref,206        )207208    def test_create_match(self, sample_segment: TMSegment) -> None:209        """Test creating a match."""210        match = TMMatch(211            segment=sample_segment,212            similarity=0.95,213            match_type=MatchType.FUZZY,214        )215        assert match.similarity == 0.95216        assert match.match_type == MatchType.FUZZY217218    def test_is_exact(self, sample_segment: TMSegment) -> None:219        """Test is_exact property."""220        exact = TMMatch(221            segment=sample_segment,222            similarity=1.0,223            match_type=MatchType.EXACT,224        )225        fuzzy = TMMatch(226            segment=sample_segment,227            similarity=0.9,228            match_type=MatchType.FUZZY,229        )230        assert exact.is_exact is True231        assert fuzzy.is_exact is False232233    def test_is_usable(self, sample_segment: TMSegment) -> None:234        """Test is_usable property."""235        high = TMMatch(236            segment=sample_segment,237            similarity=0.8,238            match_type=MatchType.FUZZY,239        )240        low = TMMatch(241            segment=sample_segment,242            similarity=0.5,243            match_type=MatchType.FUZZY,244        )245        assert high.is_usable is True246        assert low.is_usable is False247248249class TestTMStatistics:250    """Tests for TMStatistics model."""251252    def test_from_empty_segments(self) -> None:253        """Test statistics from empty list."""254        stats = TMStatistics.from_segments([])255        assert stats.total_segments == 0256        assert stats.average_reuse == 0.0257258    def test_from_segments(self) -> None:259        """Test statistics calculation."""260        segments = [261            TMSegment.create(262                source_text=f"Test {i}",263                target_text=f"テスト {i}",264                reference=VerseReference(book="john", chapter=1, verse=i),265                level=SegmentLevel.VERSE if i % 2 == 0 else SegmentLevel.PHRASE,266            )267            for i in range(1, 6)268        ]269        # Set some reuse counts270        segments[0].reuse_count = 5271        segments[1].reuse_count = 3272273        stats = TMStatistics.from_segments(segments)274        assert stats.total_segments == 5275        # range(1,6) gives [1,2,3,4,5]. i%2==0 for i=2,4. So verse_segments = 2276        assert stats.verse_segments == 2277        assert stats.phrase_segments == 3278        assert stats.total_reuses == 8279        assert "john" in stats.books_covered280281282class TestTranslationMemoryManager:283    """Tests for TranslationMemoryManager."""284285    @pytest.fixture286    def manager(self, tmp_path: Path) -> TranslationMemoryManager:287        """Create a manager with temporary storage."""288        return TranslationMemoryManager(tmp_path / "segments.json")289290    @pytest.fixture291    def sample_segment(self) -> TMSegment:292        """Create a sample segment."""293        ref = VerseReference(book="john", chapter=1, verse=1)294        return TMSegment.create(295            source_text="Ἐν ἀρχῇ ἦν ὁ λόγος",296            target_text="初めに言葉があった",297            reference=ref,298        )299300    def test_init_default_path(self) -> None:301        """The default path is taken from the module constant.302303        The constant is redirected by an autouse fixture during tests, so this304        asserts the wiring rather than a literal production path — which is305        the point: no test may reach the real store.306        """307        import src.context.translation_memory as tm_module308309        manager = TranslationMemoryManager()310        assert manager.storage_path == tm_module.DEFAULT_STORAGE_PATH311        assert "data/memory/segments.json" not in str(manager.storage_path)312313    def test_init_custom_path(self, tmp_path: Path) -> None:314        """Test custom storage path."""315        path = tmp_path / "custom.json"316        manager = TranslationMemoryManager(path)317        assert manager.storage_path == path318319    def test_add_segment(320        self, manager: TranslationMemoryManager, sample_segment: TMSegment321    ) -> None:322        """Test adding a segment."""323        manager.add_segment(sample_segment)324        assert len(manager) == 1325326    def test_add_from_translation(self, manager: TranslationMemoryManager) -> None:327        """Test creating segment from translation."""328        ref = VerseReference(book="john", chapter=1, verse=1)329        segment = manager.add_from_translation(330            source_text="Test source",331            target_text="Test target",332            reference=ref,333            metadata={"model": "test"},334        )335        assert len(manager) == 1336        assert segment.metadata["model"] == "test"337338    def test_get_segment(339        self, manager: TranslationMemoryManager, sample_segment: TMSegment340    ) -> None:341        """Test getting a segment by ID."""342        manager.add_segment(sample_segment)343        retrieved = manager.get_segment(sample_segment.id)344        assert retrieved is not None345        assert retrieved.id == sample_segment.id346347    def test_get_segment_not_found(self, manager: TranslationMemoryManager) -> None:348        """Test getting nonexistent segment."""349        result = manager.get_segment("nonexistent")350        assert result is None351352    def test_get_by_reference(353        self, manager: TranslationMemoryManager, sample_segment: TMSegment354    ) -> None:355        """Test getting segments by reference."""356        manager.add_segment(sample_segment)357        results = manager.get_by_reference(sample_segment.reference)358        assert len(results) == 1359        assert results[0].id == sample_segment.id360361    def test_get_by_reference_empty(self, manager: TranslationMemoryManager) -> None:362        """Test getting segments for unknown reference."""363        ref = VerseReference(book="john", chapter=99, verse=99)364        results = manager.get_by_reference(ref)365        assert len(results) == 0366367    def test_find_matches_exact(368        self, manager: TranslationMemoryManager, sample_segment: TMSegment369    ) -> None:370        """Test finding exact match."""371        manager.add_segment(sample_segment)372        matches = manager.find_matches(sample_segment.source_text)373        assert len(matches) == 1374        assert matches[0].is_exact375376    def test_find_matches_fuzzy(self, manager: TranslationMemoryManager) -> None:377        """Test finding fuzzy matches."""378        ref = VerseReference(book="john", chapter=1, verse=1)379        segment = TMSegment.create(380            source_text="hello world",381            target_text="こんにちは世界",382            reference=ref,383        )384        manager.add_segment(segment)385386        matches = manager.find_matches("hello worlds", threshold=0.8)387        assert len(matches) == 1388        assert matches[0].match_type == MatchType.FUZZY389390    def test_find_matches_threshold(self, manager: TranslationMemoryManager) -> None:391        """Test threshold filtering."""392        ref = VerseReference(book="john", chapter=1, verse=1)393        segment = TMSegment.create(394            source_text="hello world",395            target_text="こんにちは世界",396            reference=ref,397        )398        manager.add_segment(segment)399400        # High threshold should filter out401        matches = manager.find_matches("completely different text", threshold=0.9)402        assert len(matches) == 0403404    def test_find_matches_max_results(self, manager: TranslationMemoryManager) -> None:405        """Test max results limit."""406        ref = VerseReference(book="john", chapter=1, verse=1)407        for i in range(10):408            manager.add_segment(409                TMSegment.create(410                    source_text=f"test {i}",411                    target_text=f"テスト {i}",412                    reference=ref,413                )414            )415416        matches = manager.find_matches("test 5", threshold=0.5, max_results=3)417        assert len(matches) <= 3418419    def test_find_exact_match(420        self, manager: TranslationMemoryManager, sample_segment: TMSegment421    ) -> None:422        """Test find_exact_match helper."""423        manager.add_segment(sample_segment)424425        result = manager.find_exact_match(sample_segment.source_text)426        assert result is not None427        assert result.id == sample_segment.id428429    def test_find_exact_match_not_found(430        self, manager: TranslationMemoryManager431    ) -> None:432        """Test find_exact_match when no exact match."""433        result = manager.find_exact_match("nonexistent text")434        assert result is None435436    def test_increment_reuse(437        self, manager: TranslationMemoryManager, sample_segment: TMSegment438    ) -> None:439        """Test incrementing reuse count."""440        manager.add_segment(sample_segment)441        assert sample_segment.reuse_count == 0442443        result = manager.increment_reuse(sample_segment.id)444        assert result is True445        assert manager.get_segment(sample_segment.id).reuse_count == 1446447    def test_increment_reuse_not_found(448        self, manager: TranslationMemoryManager449    ) -> None:450        """Test increment for nonexistent segment."""451        result = manager.increment_reuse("nonexistent")452        assert result is False453454    def test_get_statistics(455        self, manager: TranslationMemoryManager, sample_segment: TMSegment456    ) -> None:457        """Test getting statistics."""458        manager.add_segment(sample_segment)459        stats = manager.get_statistics()460        assert stats.total_segments == 1461        assert "john" in stats.books_covered462463    def test_get_all_segments(464        self, manager: TranslationMemoryManager, sample_segment: TMSegment465    ) -> None:466        """Test getting all segments."""467        manager.add_segment(sample_segment)468        all_segs = manager.get_all_segments()469        assert len(all_segs) == 1470471    def test_remove_segment(472        self, manager: TranslationMemoryManager, sample_segment: TMSegment473    ) -> None:474        """Test removing a segment."""475        manager.add_segment(sample_segment)476        assert len(manager) == 1477478        result = manager.remove_segment(sample_segment.id)479        assert result is True480        assert len(manager) == 0481482    def test_remove_segment_not_found(483        self, manager: TranslationMemoryManager484    ) -> None:485        """Test removing nonexistent segment."""486        result = manager.remove_segment("nonexistent")487        assert result is False488489    def test_clear(490        self, manager: TranslationMemoryManager, sample_segment: TMSegment491    ) -> None:492        """Test clearing all segments."""493        manager.add_segment(sample_segment)494        manager.clear()495        assert len(manager) == 0496497    def test_save_and_load(498        self, manager: TranslationMemoryManager, sample_segment: TMSegment499    ) -> None:500        """Test save and load roundtrip."""501        manager.add_segment(sample_segment)502        manager.save()503504        # Create new manager and load505        manager2 = TranslationMemoryManager(manager.storage_path)506        manager2.load()507508        assert len(manager2) == 1509        loaded = manager2.get_segment(sample_segment.id)510        assert loaded is not None511        assert loaded.source_text == sample_segment.source_text512        assert loaded.target_text == sample_segment.target_text513514    def test_save_creates_directory(self, tmp_path: Path) -> None:515        """Test save creates parent directories."""516        path = tmp_path / "nested" / "dir" / "segments.json"517        manager = TranslationMemoryManager(path)518        manager.add_from_translation(519            source_text="Test",520            target_text="テスト",521            reference=VerseReference(book="john", chapter=1, verse=1),522        )523        manager.save()524        assert path.exists()525526    def test_load_nonexistent(self, tmp_path: Path) -> None:527        """Test loading from nonexistent file."""528        path = tmp_path / "nonexistent.json"529        manager = TranslationMemoryManager(path)530        manager.load()531        assert len(manager) == 0532533    def test_reload(534        self, manager: TranslationMemoryManager, sample_segment: TMSegment535    ) -> None:536        """Test reload discards in-memory changes."""537        # Save initial state538        manager.add_segment(sample_segment)539        manager.save()540541        # Add more in memory542        manager.add_from_translation(543            source_text="New",544            target_text="新規",545            reference=VerseReference(book="john", chapter=1, verse=2),546        )547        assert len(manager) == 2548549        # Reload should restore to saved state550        manager.reload()551        assert len(manager) == 1552553    def test_format_matches_for_prompt(self, manager: TranslationMemoryManager) -> None:554        """Test formatting matches for prompt."""555        ref = VerseReference(book="john", chapter=1, verse=1)556        segment = TMSegment.create(557            source_text="Test",558            target_text="テスト",559            reference=ref,560        )561        match = TMMatch(segment=segment, similarity=0.9, match_type=MatchType.FUZZY)562563        formatted = manager.format_matches_for_prompt([match])564        assert "90%" in formatted565        assert "john:1:1" in formatted566        assert "テスト" in formatted567568    def test_format_matches_empty(self, manager: TranslationMemoryManager) -> None:569        """Test formatting empty matches."""570        formatted = manager.format_matches_for_prompt([])571        assert "No similar translations" in formatted572573    def test_format_matches_limit(self, manager: TranslationMemoryManager) -> None:574        """Test match display limit."""575        matches = []576        for i in range(5):577            ref = VerseReference(book="john", chapter=1, verse=i + 1)578            segment = TMSegment.create(579                source_text=f"Test {i}",580                target_text=f"テスト {i}",581                reference=ref,582            )583            matches.append(584                TMMatch(segment=segment, similarity=0.9 - i * 0.05, match_type=MatchType.FUZZY)585            )586587        formatted = manager.format_matches_for_prompt(matches)588        # Should show "and X more"589        assert "more" in formatted590591592class TestPromptBuilderWithTM:593    """Tests for PromptBuilder with Translation Memory integration."""594595    def test_build_prompt_with_tm_matches(self) -> None:596        """Test prompt includes TM matches."""597        from src.context.glossary import GlossaryManager598        from src.pipeline.prompt import PromptBuilder599        from src.models.verse import (600            TranslationContext,601            Verse,602            VerseReference,603        )604605        glossary = GlossaryManager()606        builder = PromptBuilder(glossary)607608        ref = VerseReference(book="john", chapter=1, verse=1)609        verse = Verse(610            reference=ref,611            greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",612        )613        context = TranslationContext(target_verse=verse)614615        # Create TM match616        segment = TMSegment.create(617            source_text="Ἐν ἀρχῇ",618            target_text="初めに",619            reference=VerseReference(book="john", chapter=1, verse=1),620        )621        match = TMMatch(segment=segment, similarity=0.8, match_type=MatchType.FUZZY)622623        prompt = builder.build_prompt(context, tm_matches=[match])624        assert "TRANSLATION MEMORY" in prompt625        assert "80%" in prompt626        assert "初めに" in prompt627628    def test_build_prompt_without_tm(self) -> None:629        """Test prompt without TM matches."""630        from src.context.glossary import GlossaryManager631        from src.pipeline.prompt import PromptBuilder632        from src.models.verse import (633            TranslationContext,634            Verse,635            VerseReference,636        )637638        glossary = GlossaryManager()639        builder = PromptBuilder(glossary)640641        ref = VerseReference(book="john", chapter=1, verse=1)642        verse = Verse(reference=ref, greek_text="Ἐν ἀρχῇ")643        context = TranslationContext(target_verse=verse)644645        prompt = builder.build_prompt(context)646        assert "TRANSLATION MEMORY" not in prompt647648649class TestConcurrentSavesNeverLoseWork:650    """Parallel book workers must not roll back each other's translations.651652    Each worker holds its own manager over one shared file, loaded when its653    book started and saved hours later. A plain overwrite meant the last654    writer discarded every other book's segments — days of paid translation655    gone, silently, with the run still reporting success.656    """657658    @staticmethod659    def _ref(book: str, chapter: int, verse: int) -> VerseReference:660        """Build a verse reference.661662        Args:663            book: Book name.664            chapter: Chapter number.665            verse: Verse number.666667        Returns:668            The VerseReference.669        """670        return VerseReference(book=book, chapter=chapter, verse=verse)671672    def test_a_later_save_keeps_an_earlier_workers_segments(self, tmp_path):673        path = tmp_path / "segments.json"674675        first = TranslationMemoryManager(path)676        second = TranslationMemoryManager(path)677        first.load()678        second.load()679680        first.add_from_translation("source A", "訳A", self._ref("john", 1, 1))681        second.add_from_translation("source B", "訳B", self._ref("mark", 2, 2))682683        first.save()684        second.save()685686        final = TranslationMemoryManager(path)687        final.load()688        assert sorted(s.source_text for s in final.get_all_segments()) == [689            "source A",690            "source B",691        ]692693    def test_many_concurrent_saves_lose_nothing(self, tmp_path):694        import threading695696        path = tmp_path / "segments.json"697        managers = []698        for index in range(8):699            manager = TranslationMemoryManager(path)700            manager.load()701            manager.add_from_translation(702                f"source {index}", f"訳 {index}", self._ref("john", 1, index + 1)703            )704            managers.append(manager)705706        threads = [threading.Thread(target=m.save) for m in managers]707        for thread in threads:708            thread.start()709        for thread in threads:710            thread.join()711712        final = TranslationMemoryManager(path)713        final.load()714        assert len(final) == 8715716    def test_a_retranslation_is_added_rather_than_replacing_the_earlier_one(self, tmp_path):717        # Segment ids are random UUIDs here, so re-translating the same source718        # yields a second segment rather than overwriting the first. That is719        # duplication, not loss — and loss is the property under test. Merging720        # must never be the thing that drops either rendering.721        path = tmp_path / "segments.json"722723        first = TranslationMemoryManager(path)724        first.add_from_translation("same source", "古い訳", self._ref("john", 1, 1))725        first.save()726727        second = TranslationMemoryManager(path)728        second.add_from_translation("same source", "新しい訳", self._ref("john", 1, 1))729        second.save()730731        final = TranslationMemoryManager(path)732        final.load()733        assert sorted(s.target_text for s in final.get_all_segments()) == ["古い訳", "新しい訳"]734735    def test_saving_the_same_manager_twice_does_not_duplicate(self, tmp_path):736        path = tmp_path / "segments.json"737738        manager = TranslationMemoryManager(path)739        manager.add_from_translation("source", "訳", self._ref("john", 1, 1))740        manager.save()741        manager.save()742743        final = TranslationMemoryManager(path)744        final.load()745        assert len(final) == 1746747    def test_the_reference_index_survives_a_merge(self, tmp_path):748        path = tmp_path / "segments.json"749750        first = TranslationMemoryManager(path)751        first.add_from_translation("source A", "訳A", self._ref("john", 1, 1))752        first.save()753754        second = TranslationMemoryManager(path)755        second.load()756        second.add_from_translation("source B", "訳B", self._ref("mark", 2, 2))757        second.save()758759        assert second.get_by_reference(self._ref("john", 1, 1))760        assert second.get_by_reference(self._ref("mark", 2, 2))761762763class TestCorruptStoreIsPreserved:764    def test_an_unreadable_store_is_quarantined_not_overwritten(self, tmp_path, caplog):765        # Silently starting fresh meant the next save replaced days of766        # accumulated translation with whatever the process happened to hold.767        path = tmp_path / "segments.json"768        path.write_text("{ this is not json", encoding="utf-8")769770        manager = TranslationMemoryManager(path)771        manager.load()772        manager.add_from_translation(773            "after corruption", "訳", VerseReference(book="luke", chapter=1, verse=1)774        )775        manager.save()776777        quarantined = list(tmp_path.glob("segments.corrupt.*.json"))778        assert len(quarantined) == 1779        assert quarantined[0].read_text(encoding="utf-8") == "{ this is not json"780781        final = TranslationMemoryManager(path)782        final.load()783        assert len(final) == 1784785    def test_a_missing_store_is_not_an_error(self, tmp_path):786        manager = TranslationMemoryManager(tmp_path / "absent.json")787        manager.load()788        assert len(manager) == 0789790    def test_no_stray_temp_file_survives_a_save(self, tmp_path):791        path = tmp_path / "segments.json"792        manager = TranslationMemoryManager(path)793        manager.add_from_translation(794            "source", "訳", VerseReference(book="john", chapter=1, verse=1)795        )796        manager.save()797        assert not list(tmp_path.glob("*.tmp"))798