OPEN SOURCE
software/tests/test_auto_evaluation.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Tests for auto-evaluation and TM integration in translator."""23import json4import pytest5from pathlib import Path6from unittest.mock import MagicMock, patch78from src.context.builder import ContextBuilder9from src.context.glossary import GlossaryManager10from src.context.translation_memory import TranslationMemoryManager11from src.data.loader import DataLoader12from src.data.storage import Storage13from src.evaluation.evaluator import TranslationEvaluator14from src.evaluation.storage import EvaluationStorage15from src.models.evaluation import EvaluationScore, VerseEvaluation, DIMENSIONS16from src.models.verse import (17GlossaryTerm,18TranslationContext,19TranslationOutput,20Verse,21VerseReference,22)23from src.pipeline.prompt import PromptBuilder24from src.pipeline.translator import Translator252627@pytest.fixture28def temp_data_dir(tmp_path: Path) -> Path:29"""Create temporary data directory with test data."""30sources_dir = tmp_path / "sources"31(sources_dir / "greek").mkdir(parents=True)32(sources_dir / "japanese" / "meiji").mkdir(parents=True)33(sources_dir / "japanese" / "taisho").mkdir(parents=True)34(sources_dir / "english" / "kjv").mkdir(parents=True)35(sources_dir / "english" / "asv").mkdir(parents=True)36(tmp_path / "metadata" / "john").mkdir(parents=True)37(tmp_path / "output" / "john").mkdir(parents=True)38(tmp_path / "memory").mkdir(parents=True)39(tmp_path / "config").mkdir(parents=True)4041# Greek data42greek_data = {43"1:1": {"text": "Ἐν ἀρχῇ ἦν ὁ λόγος", "translit": "En archē ēn ho logos"},44"1:2": {"text": "οὗτος ἦν ἐν ἀρχῇ", "translit": "houtos ēn en archē"},45}46with (sources_dir / "greek" / "john.json").open("w") as f:47json.dump(greek_data, f)4849# KJV data50kjv_data = {51"1:1": "In the beginning was the Word",52"1:2": "The same was in the beginning with God",53}54with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:55json.dump(kjv_data, f)5657# Glossary58glossary = {"terms": []}59with (tmp_path / "metadata" / "glossary.json").open("w") as f:60json.dump(glossary, f)6162# Style config63style_config = {64"equivalence_approach": "dynamic",65"gender_language": "moderate_inclusive",66"readability_level": "general_public",67"theological_tradition": "evangelical",68"textual_basis": "MorphGNT/SBLGNT",69"term_consistency": "balanced",70"formality_level": "standard",71}72with (tmp_path / "config" / "style.json").open("w") as f:73json.dump(style_config, f)7475# Empty TM76tm_data = {"version": "1.0", "updated_at": None, "segments": []}77with (tmp_path / "memory" / "segments.json").open("w") as f:78json.dump(tm_data, f)7980return tmp_path818283@pytest.fixture84def translator_with_tm(temp_data_dir: Path) -> Translator:85"""Create translator with TM enabled."""86loader = DataLoader(temp_data_dir)87storage = Storage(temp_data_dir)88glossary = GlossaryManager(loader.load_glossary())89tm = TranslationMemoryManager(temp_data_dir / "memory" / "segments.json")9091context_builder = ContextBuilder(loader, glossary)92prompt_builder = PromptBuilder(glossary, translation_memory=tm)9394return Translator(95context_builder, prompt_builder, storage, translation_memory=tm96)979899@pytest.fixture100def translator_with_evaluation(temp_data_dir: Path) -> Translator:101"""Create translator with auto-evaluation enabled."""102loader = DataLoader(temp_data_dir)103storage = Storage(temp_data_dir)104glossary = GlossaryManager(loader.load_glossary())105106context_builder = ContextBuilder(loader, glossary)107prompt_builder = PromptBuilder(glossary)108evaluator = TranslationEvaluator()109eval_storage = EvaluationStorage(temp_data_dir)110111return Translator(112context_builder,113prompt_builder,114storage,115auto_evaluate=True,116evaluator=evaluator,117evaluation_storage=eval_storage,118)119120121class TestTranslatorTMIntegration:122"""Tests for TM integration in Translator."""123124def test_translation_memory_property(self, translator_with_tm: Translator) -> None:125"""Test translation_memory property."""126assert translator_with_tm.translation_memory is not None127assert isinstance(128translator_with_tm.translation_memory, TranslationMemoryManager129)130131def test_translation_memory_setter(self, temp_data_dir: Path) -> None:132"""Test setting translation_memory."""133loader = DataLoader(temp_data_dir)134storage = Storage(temp_data_dir)135glossary = GlossaryManager()136context_builder = ContextBuilder(loader, glossary)137prompt_builder = PromptBuilder(glossary)138139translator = Translator(context_builder, prompt_builder, storage)140assert translator.translation_memory is None141142tm = TranslationMemoryManager(temp_data_dir / "memory" / "segments.json")143translator.translation_memory = tm144assert translator.translation_memory is tm145146def test_get_tm_matches_no_tm(self, temp_data_dir: Path) -> None:147"""Test _get_tm_matches with no TM configured."""148loader = DataLoader(temp_data_dir)149storage = Storage(temp_data_dir)150glossary = GlossaryManager()151context_builder = ContextBuilder(loader, glossary)152prompt_builder = PromptBuilder(glossary)153154translator = Translator(context_builder, prompt_builder, storage)155156ref = VerseReference(book="john", chapter=1, verse=1)157context = context_builder.build_context(ref)158matches = translator._get_tm_matches(context)159assert matches is None160161def test_get_tm_matches_no_source_text(162self, translator_with_tm: Translator163) -> None:164"""Test _get_tm_matches with no source text."""165verse = Verse(reference=VerseReference(book="test", chapter=1, verse=1))166context = TranslationContext(target_verse=verse)167matches = translator_with_tm._get_tm_matches(context)168assert matches is None169170def test_get_tm_matches_with_source(171self, translator_with_tm: Translator172) -> None:173"""Test _get_tm_matches finds matches."""174# Add a segment to TM175ref = VerseReference(book="john", chapter=1, verse=1)176translator_with_tm.translation_memory.add_from_translation(177source_text="Ἐν ἀρχῇ ἦν ὁ λόγος",178target_text="初めに言葉があった",179reference=ref,180)181182# Create context with same source text183verse = Verse(184reference=ref,185greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",186)187context = TranslationContext(target_verse=verse)188matches = translator_with_tm._get_tm_matches(context)189assert matches is not None190assert len(matches) == 1191192def test_add_to_tm_no_tm(self, temp_data_dir: Path) -> None:193"""Test _add_to_tm with no TM configured."""194loader = DataLoader(temp_data_dir)195storage = Storage(temp_data_dir)196glossary = GlossaryManager()197context_builder = ContextBuilder(loader, glossary)198prompt_builder = PromptBuilder(glossary)199200translator = Translator(context_builder, prompt_builder, storage)201202ref = VerseReference(book="john", chapter=1, verse=1)203verse = Verse(reference=ref, greek_text="Test text")204context = TranslationContext(target_verse=verse)205206# Should not raise207translator._add_to_tm(context, "翻訳")208209def test_add_to_tm_no_source(self, translator_with_tm: Translator) -> None:210"""Test _add_to_tm with no source text."""211ref = VerseReference(book="john", chapter=1, verse=1)212verse = Verse(reference=ref)213context = TranslationContext(target_verse=verse)214215initial_count = len(translator_with_tm.translation_memory)216translator_with_tm._add_to_tm(context, "翻訳")217assert len(translator_with_tm.translation_memory) == initial_count218219def test_add_to_tm_with_source(self, translator_with_tm: Translator) -> None:220"""Test _add_to_tm adds segment."""221ref = VerseReference(book="john", chapter=1, verse=1)222verse = Verse(reference=ref, greek_text="Test source for TM")223context = TranslationContext(target_verse=verse)224225# Force load the TM226translator_with_tm.translation_memory.load()227initial_count = len(translator_with_tm.translation_memory)228translator_with_tm._add_to_tm(context, "翻訳")229assert len(translator_with_tm.translation_memory) == initial_count + 1230231232class TestTranslatorAutoEvaluation:233"""Tests for auto-evaluation in Translator."""234235def test_auto_evaluate_property(236self, translator_with_evaluation: Translator237) -> None:238"""Test auto_evaluate property."""239assert translator_with_evaluation.auto_evaluate is True240241def test_auto_evaluate_setter(self, temp_data_dir: Path) -> None:242"""Test setting auto_evaluate."""243loader = DataLoader(temp_data_dir)244storage = Storage(temp_data_dir)245glossary = GlossaryManager()246context_builder = ContextBuilder(loader, glossary)247prompt_builder = PromptBuilder(glossary)248249# Default is now True, explicitly test with False250translator = Translator(251context_builder, prompt_builder, storage, auto_evaluate=False252)253assert translator.auto_evaluate is False254255translator.auto_evaluate = True256assert translator.auto_evaluate is True257258def test_evaluate_translation_disabled(self, temp_data_dir: Path) -> None:259"""Test _evaluate_translation when disabled."""260loader = DataLoader(temp_data_dir)261storage = Storage(temp_data_dir)262glossary = GlossaryManager()263context_builder = ContextBuilder(loader, glossary)264prompt_builder = PromptBuilder(glossary)265266translator = Translator(267context_builder, prompt_builder, storage, auto_evaluate=False268)269270ref = VerseReference(book="john", chapter=1, verse=1)271output = TranslationOutput(272reference=ref, text="テスト", model="test-model"273)274verse = Verse(reference=ref)275context = TranslationContext(target_verse=verse)276277result = translator._evaluate_translation(output, context)278assert result is None279280def test_evaluate_translation_no_evaluator(281self, temp_data_dir: Path282) -> None:283"""Test _evaluate_translation without evaluator."""284loader = DataLoader(temp_data_dir)285storage = Storage(temp_data_dir)286glossary = GlossaryManager()287context_builder = ContextBuilder(loader, glossary)288prompt_builder = PromptBuilder(glossary)289290translator = Translator(291context_builder, prompt_builder, storage, auto_evaluate=True292)293294ref = VerseReference(book="john", chapter=1, verse=1)295output = TranslationOutput(296reference=ref, text="テスト", model="test-model"297)298verse = Verse(reference=ref)299context = TranslationContext(target_verse=verse)300301result = translator._evaluate_translation(output, context)302assert result is None303304@patch("src.pipeline.translator.Translator._call_claude")305def test_evaluate_translation_success(306self, mock_claude: MagicMock, translator_with_evaluation: Translator307) -> None:308"""Test _evaluate_translation success path."""309# Mock response310mock_response = json.dumps({311dim: {"score": 85, "confidence": 90, "reasoning": "Good"}312for dim in DIMENSIONS313})314mock_claude.return_value = mock_response315316ref = VerseReference(book="john", chapter=1, verse=1)317output = TranslationOutput(318reference=ref, text="初めに言葉があった", model="test-model"319)320verse = Verse(reference=ref, greek_text="Ἐν ἀρχῇ")321context = TranslationContext(target_verse=verse)322323result = translator_with_evaluation._evaluate_translation(output, context)324assert result is not None325assert isinstance(result, VerseEvaluation)326327@patch("src.pipeline.translator.Translator._call_claude")328def test_evaluate_translation_exception(329self, mock_claude: MagicMock, translator_with_evaluation: Translator330) -> None:331"""Test _evaluate_translation handles exceptions."""332mock_claude.side_effect = RuntimeError("Claude failed")333334ref = VerseReference(book="john", chapter=1, verse=1)335output = TranslationOutput(336reference=ref, text="テスト", model="test-model"337)338verse = Verse(reference=ref, greek_text="Test")339context = TranslationContext(target_verse=verse)340341# Should not raise, returns None342result = translator_with_evaluation._evaluate_translation(output, context)343assert result is None344345def test_save_evaluation_no_storage(self, temp_data_dir: Path) -> None:346"""Test _save_evaluation without storage."""347loader = DataLoader(temp_data_dir)348storage = Storage(temp_data_dir)349glossary = GlossaryManager()350context_builder = ContextBuilder(loader, glossary)351prompt_builder = PromptBuilder(glossary)352353translator = Translator(context_builder, prompt_builder, storage)354355ref = VerseReference(book="john", chapter=1, verse=1)356scores = {357dim: EvaluationScore(358dimension=dim, score=80, confidence=90, reasoning="Good"359)360for dim in DIMENSIONS361}362evaluation = VerseEvaluation(363verse_number=1,364evaluator_model="test",365scores=scores,366flags=[],367)368369# Should not raise370translator._save_evaluation(evaluation, ref)371372def test_save_evaluation_with_storage(373self, temp_data_dir: Path374) -> None:375"""Test _save_evaluation with storage."""376loader = DataLoader(temp_data_dir)377storage = Storage(temp_data_dir)378glossary = GlossaryManager()379context_builder = ContextBuilder(loader, glossary)380prompt_builder = PromptBuilder(glossary)381evaluator = TranslationEvaluator()382eval_storage = EvaluationStorage(temp_data_dir)383384translator = Translator(385context_builder,386prompt_builder,387storage,388auto_evaluate=True,389evaluator=evaluator,390evaluation_storage=eval_storage,391)392393ref = VerseReference(book="john", chapter=1, verse=1)394scores = {395dim: EvaluationScore(396dimension=dim, score=80, confidence=90, reasoning="Good"397)398for dim in DIMENSIONS399}400evaluation = VerseEvaluation(401verse_number=1,402evaluator_model="test",403scores=scores,404flags=[],405)406407# Should not raise408translator._save_evaluation(evaluation, ref)409410411class TestTranslatorTranslateVerse:412"""Tests for translate_verse with TM and evaluation."""413414@patch("src.pipeline.translator.Translator._call_claude")415def test_translate_verse_adds_to_tm(416self, mock_claude: MagicMock, translator_with_tm: Translator417) -> None:418"""Test translate_verse adds to TM."""419mock_claude.return_value = "初めに言葉があった"420421ref = VerseReference(book="john", chapter=1, verse=1)422initial_count = len(translator_with_tm.translation_memory)423424translator_with_tm.translate_verse(ref, skip_existing=False)425426assert len(translator_with_tm.translation_memory) == initial_count + 1427428@patch("src.pipeline.translator.Translator._call_claude")429def test_translate_verse_with_evaluate_override(430self, mock_claude: MagicMock, translator_with_evaluation: Translator431) -> None:432"""Test translate_verse with evaluate override."""433mock_claude.return_value = "初めに言葉があった"434435ref = VerseReference(book="john", chapter=1, verse=1)436437# Disable evaluation for this call438translator_with_evaluation.translate_verse(439ref, skip_existing=False, evaluate=False440)441442# Should only call Claude once for translation (not evaluation)443assert mock_claude.call_count == 1444445446@patch("src.pipeline.translator.Translator._call_claude")447def test_translate_verse_with_successful_evaluation(448self, mock_claude: MagicMock, translator_with_evaluation: Translator449) -> None:450"""Test translate_verse with successful evaluation flow."""451# First call for translation, second for evaluation452eval_response = json.dumps({453dim: {"score": 85, "confidence": 90, "reasoning": "Good"}454for dim in DIMENSIONS455})456mock_claude.side_effect = ["初めに言葉があった", eval_response]457458ref = VerseReference(book="john", chapter=1, verse=1)459460# Enable evaluation for this call461translator_with_evaluation.translate_verse(462ref, skip_existing=False, evaluate=True463)464465# Should call Claude twice (translation + evaluation)466assert mock_claude.call_count == 2467468469class TestTranslatorTranslateChapter:470"""Tests for translate_chapter with TM."""471472@patch("src.pipeline.translator.Translator._call_claude")473def test_translate_chapter_saves_tm(474self, mock_claude: MagicMock, translator_with_tm: Translator475) -> None:476"""Test translate_chapter saves TM after completion."""477mock_claude.return_value = "翻訳テキスト"478479# Monkey patch save to track calls480save_called = []481original_save = translator_with_tm.translation_memory.save482483def track_save():484save_called.append(True)485original_save()486487translator_with_tm.translation_memory.save = track_save488489translator_with_tm.translate_chapter("john", 1, skip_existing=False)490491# Save should have been called492assert len(save_called) > 0493