OPEN SOURCE
software/tests/test_final_push.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 push tests to reach 98% coverage."""23import json4import pytest5from pathlib import Path6from tempfile import TemporaryDirectory7from unittest.mock import MagicMock, patch89from src.context.builder import ContextBuilder10from src.context.cross_references import CrossReference, CrossReferenceManager, ReferenceType11from src.context.glossary import GlossaryManager12from src.data.loader import DataLoader13from src.data.storage import Storage14from src.exceptions import ChunkParsingError15from src.models.pericope import Pericope16from src.models.verse import VerseReference17from src.pipeline.prompt import PromptBuilder18from src.pipeline.translator import Translator192021@pytest.fixture22def temp_data_dir(tmp_path: Path) -> Path:23"""Create temporary data directory."""24sources_dir = tmp_path / "sources"25(sources_dir / "greek").mkdir(parents=True)26(sources_dir / "english" / "kjv").mkdir(parents=True)27(tmp_path / "metadata").mkdir(parents=True)28(tmp_path / "output").mkdir(parents=True)2930greek_data = {31"1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},32"1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},33}34with (sources_dir / "greek" / "john.json").open("w") as f:35json.dump(greek_data, f)3637kjv_data = {"1:1": "In the beginning", "1:2": "The same was"}38with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:39json.dump(kjv_data, f)4041return tmp_path424344class TestParseChunkResponseStrategy4:45"""Tests for strategy 4 JSON extraction (iterating through { chars)."""4647def test_parse_chunk_response_finds_json_after_text(48self, temp_data_dir: Path49) -> None:50"""Test finding JSON when there's text before first brace."""51loader = DataLoader(temp_data_dir)52glossary = GlossaryManager()53storage = Storage(temp_data_dir)54context_builder = ContextBuilder(loader, glossary)55prompt_builder = PromptBuilder(glossary)56translator = Translator(context_builder, prompt_builder, storage)5758# Response with failed first strategy but valid JSON later59response = 'Some text { invalid } more text {"1:1": "翻訳"}'60expected = ["1:1"]6162translations = translator._parse_chunk_response(response, expected)63assert translations["1:1"] == "翻訳"6465def test_parse_chunk_response_multiple_braces(66self, temp_data_dir: Path67) -> None:68"""Test when multiple { characters exist before valid JSON."""69loader = DataLoader(temp_data_dir)70glossary = GlossaryManager()71storage = Storage(temp_data_dir)72context_builder = ContextBuilder(loader, glossary)73prompt_builder = PromptBuilder(glossary)74translator = Translator(context_builder, prompt_builder, storage)7576# Multiple invalid JSON before valid one77response = '{ broken { also broken {"1:1": "正解"}'78expected = ["1:1"]7980translations = translator._parse_chunk_response(response, expected)81assert translations["1:1"] == "正解"828384class TestExtractJsonFromPositionEdgeCases:85"""Tests for _extract_json_from_position edge cases."""8687def test_extract_json_with_escaped_chars(self, temp_data_dir: Path) -> None:88"""Test extraction with escaped characters inside strings."""89loader = DataLoader(temp_data_dir)90glossary = GlossaryManager()91storage = Storage(temp_data_dir)92context_builder = ContextBuilder(loader, glossary)93prompt_builder = PromptBuilder(glossary)94translator = Translator(context_builder, prompt_builder, storage)9596# JSON with escaped backslash and quote97text = '{"1:1": "He said \\"Hello\\" and\\nwent"}'98result = translator._extract_json_from_position(text, 0)99assert result == text100101def test_extract_json_no_closing_brace(self, temp_data_dir: Path) -> None:102"""Test extraction when no closing brace is found."""103loader = DataLoader(temp_data_dir)104glossary = GlossaryManager()105storage = Storage(temp_data_dir)106context_builder = ContextBuilder(loader, glossary)107prompt_builder = PromptBuilder(glossary)108translator = Translator(context_builder, prompt_builder, storage)109110# Unclosed JSON111text = '{"1:1": "test'112result = translator._extract_json_from_position(text, 0)113assert result is None114115def test_extract_json_invalid_start(self, temp_data_dir: Path) -> None:116"""Test extraction with invalid start position."""117loader = DataLoader(temp_data_dir)118glossary = GlossaryManager()119storage = Storage(temp_data_dir)120context_builder = ContextBuilder(loader, glossary)121prompt_builder = PromptBuilder(glossary)122translator = Translator(context_builder, prompt_builder, storage)123124# Start at non-{ character125text = 'abc{"1:1": "test"}'126result = translator._extract_json_from_position(text, 0)127assert result is None128129# Start past end130result = translator._extract_json_from_position(text, 100)131assert result is None132133134class TestValidateTranslationsEdgeCases:135"""Tests for _validate_translations edge cases."""136137def test_validate_non_dict_raises(self, temp_data_dir: Path) -> None:138"""Test validation raises for non-dict input."""139loader = DataLoader(temp_data_dir)140glossary = GlossaryManager()141storage = Storage(temp_data_dir)142context_builder = ContextBuilder(loader, glossary)143prompt_builder = PromptBuilder(glossary)144translator = Translator(context_builder, prompt_builder, storage)145146with pytest.raises(ChunkParsingError, match="Expected dict"):147translator._validate_translations(148["not", "a", "dict"], # list instead of dict149["1:1"],150"original response",151)152153def test_validate_non_string_value_raises(self, temp_data_dir: Path) -> None:154"""Test validation raises for non-string translation value."""155loader = DataLoader(temp_data_dir)156glossary = GlossaryManager()157storage = Storage(temp_data_dir)158context_builder = ContextBuilder(loader, glossary)159prompt_builder = PromptBuilder(glossary)160translator = Translator(context_builder, prompt_builder, storage)161162with pytest.raises(ChunkParsingError, match="is not a string"):163translator._validate_translations(164{"1:1": 12345}, # number instead of string165["1:1"],166"original response",167)168169170class TestExtractFromCodeBlockEdgeCases:171"""Tests for _extract_from_code_block edge cases."""172173def test_extract_no_code_block(self, temp_data_dir: Path) -> None:174"""Test extraction when no code block exists."""175loader = DataLoader(temp_data_dir)176glossary = GlossaryManager()177storage = Storage(temp_data_dir)178context_builder = ContextBuilder(loader, glossary)179prompt_builder = PromptBuilder(glossary)180translator = Translator(context_builder, prompt_builder, storage)181182# No code block, should return original183text = '{"1:1": "test"}'184result = translator._extract_from_code_block(text)185assert result == text186187def test_extract_unclosed_code_block(self, temp_data_dir: Path) -> None:188"""Test extraction with unclosed code block."""189loader = DataLoader(temp_data_dir)190glossary = GlossaryManager()191storage = Storage(temp_data_dir)192context_builder = ContextBuilder(loader, glossary)193prompt_builder = PromptBuilder(glossary)194translator = Translator(context_builder, prompt_builder, storage)195196# Unclosed code block - should extract everything after opening197text = '```json\n{"1:1": "test"}'198result = translator._extract_from_code_block(text)199assert '{"1:1": "test"}' in result200201202class TestExtractJsonByBracketsEdgeCases:203"""Tests for _extract_json_by_brackets edge cases."""204205def test_no_opening_brace(self, temp_data_dir: Path) -> None:206"""Test when no opening brace exists."""207loader = DataLoader(temp_data_dir)208glossary = GlossaryManager()209storage = Storage(temp_data_dir)210context_builder = ContextBuilder(loader, glossary)211prompt_builder = PromptBuilder(glossary)212translator = Translator(context_builder, prompt_builder, storage)213214result = translator._extract_json_by_brackets("no braces here")215assert result is None216217218class TestCrossReferenceConsistencyNotes:219"""Tests for cross-reference consistency notes."""220221def test_get_translation_consistency_notes_with_parallels(self) -> None:222"""Test consistency notes include parallels."""223manager = CrossReferenceManager()224225# Add a PARALLEL type reference226manager.add_reference(227CrossReference(228source_book="matthew",229source_chapter=3,230source_verse=13,231target_book="mark",232target_chapter=1,233target_verse_start=9,234reference_type=ReferenceType.PARALLEL,235)236)237238notes = manager.get_translation_consistency_notes("matthew", 3, 13)239assert any("parallel" in n.lower() for n in notes)240241def test_get_translation_consistency_notes_with_hint(self) -> None:242"""Test consistency notes include translation hint."""243manager = CrossReferenceManager()244245# Add a quotation with translation hint246manager.add_reference(247CrossReference(248source_book="john",249source_chapter=12,250source_verse=40,251target_book="isaiah",252target_chapter=6,253target_verse_start=10,254reference_type=ReferenceType.QUOTATION,255translation_hint="Use similar wording to Isaiah 6:10",256)257)258259notes = manager.get_translation_consistency_notes("john", 12, 40)260assert any("hint" in n.lower() for n in notes)261262263class TestChunkTranslationWithExisting:264"""Tests for chunk translation when some verses exist."""265266@patch("src.pipeline.translator.subprocess.run")267def test_translate_chunk_skips_all_existing(268self, mock_run: MagicMock, temp_data_dir: Path269) -> None:270"""Test chunk translation returns early when all verses exist."""271loader = DataLoader(temp_data_dir)272glossary = GlossaryManager()273storage = Storage(temp_data_dir)274context_builder = ContextBuilder(loader, glossary)275prompt_builder = PromptBuilder(glossary)276translator = Translator(context_builder, prompt_builder, storage)277278# Pre-save translations for verses279from src.models.verse import TranslationOutput280281for verse_ref in ["1:1", "1:2"]:282chapter, verse = map(int, verse_ref.split(":"))283ref = VerseReference(book="john", chapter=chapter, verse=verse)284output = TranslationOutput(285reference=ref,286text=f"既存の翻訳 {verse_ref}",287model="test",288sources_used=["test"],289)290storage.save_translation(output)291292# Create pericope293pericope = Pericope(294id="test-1",295name="Test Pericope",296start_chapter=1,297start_verse=1,298end_chapter=1,299end_verse=2,300verses=["1:1", "1:2"],301)302303# Should skip without calling Claude304result = translator.translate_chunk("john", pericope, skip_existing=True)305306# Should not have called subprocess.run for translation307mock_run.assert_not_called()308309# Should have existing translations310assert "1:1" in result.translations311assert "1:2" in result.translations312assert result.coherence_note == "Loaded from existing translations"313314315class TestTMSaveAfterChapter:316"""Tests for TM save after chapter translation."""317318@patch("src.pipeline.translator.subprocess.run")319def test_tm_saved_after_chapter(320self, mock_run: MagicMock, temp_data_dir: Path321) -> None:322"""Test that translation memory is saved after chapter translation."""323from src.context.translation_memory import TranslationMemoryManager324325loader = DataLoader(temp_data_dir)326glossary = GlossaryManager()327storage = Storage(temp_data_dir)328context_builder = ContextBuilder(loader, glossary)329prompt_builder = PromptBuilder(glossary)330331# Create TM manager. Must be a temp path: the default is the real332# production store, and this test drives a translator that saves.333tm = TranslationMemoryManager(temp_data_dir / "memory" / "segments.json")334tm_saved = []335original_save = tm.save336337def track_save(*args, **kwargs):338tm_saved.append(True)339return original_save(*args, **kwargs)340341tm.save = track_save342343translator = Translator(344context_builder,345prompt_builder,346storage,347translation_memory=tm,348)349350# Mock translation response351mock_run.return_value = MagicMock(352returncode=0,353stdout="翻訳テスト",354stderr="",355)356357translator.translate_chapter("john", 1, skip_existing=False)358359# TM should have been saved360assert len(tm_saved) >= 1361