OPEN SOURCE
software/tests/test_evaluation.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"""Tests for the evaluation module."""23from __future__ import annotations45import json6import tempfile7from datetime import datetime8from pathlib import Path910import pytest1112from src.models.evaluation import (13ANY_DIMENSION_THRESHOLD,14CONFIDENCE_THRESHOLD,15DIMENSIONS,16SOURCE_FIDELITY_THRESHOLD,17THEOLOGICAL_ACCURACY_THRESHOLD,18BookEvaluationSummary,19ChapterEvaluation,20EvaluationScore,21VerseEvaluation,22)23from src.evaluation.evaluator import (24TranslationEvaluator,25build_evaluation_prompt,26parse_evaluation_response,27)28from src.evaluation.storage import EvaluationStorage293031class TestEvaluationScore:32"""Tests for EvaluationScore model."""3334def test_create_score(self) -> None:35"""Test creating an evaluation score."""36score = EvaluationScore(37dimension="theological_accuracy",38score=95.0,39confidence=90.0,40reasoning="Theological meaning is well preserved.",41)42assert score.dimension == "theological_accuracy"43assert score.score == 95.044assert score.confidence == 90.045assert "preserved" in score.reasoning4647def test_score_bounds(self) -> None:48"""Test score validation bounds."""49# Valid scores50score = EvaluationScore(51dimension="test", score=0.0, confidence=100.0, reasoning="test"52)53assert score.score == 0.054assert score.confidence == 100.05556# Invalid scores should raise57with pytest.raises(ValueError):58EvaluationScore(59dimension="test", score=-1.0, confidence=50.0, reasoning="test"60)6162with pytest.raises(ValueError):63EvaluationScore(64dimension="test", score=101.0, confidence=50.0, reasoning="test"65)666768class TestVerseEvaluation:69"""Tests for VerseEvaluation model."""7071def test_create_verse_evaluation(self) -> None:72"""Test creating a verse evaluation."""73scores = {74"theological_accuracy": EvaluationScore(75dimension="theological_accuracy",76score=95.0,77confidence=90.0,78reasoning="Good",79),80"source_fidelity": EvaluationScore(81dimension="source_fidelity",82score=88.0,83confidence=85.0,84reasoning="Good",85),86}87evaluation = VerseEvaluation(88verse_number=1,89evaluator_model="test-model",90scores=scores,91)92assert evaluation.verse_number == 193assert evaluation.evaluator_model == "test-model"94assert len(evaluation.scores) == 29596def test_overall_score_calculation(self) -> None:97"""Test overall score is calculated correctly."""98scores = {99"dim1": EvaluationScore(100dimension="dim1", score=80.0, confidence=90.0, reasoning="test"101),102"dim2": EvaluationScore(103dimension="dim2", score=90.0, confidence=90.0, reasoning="test"104),105}106evaluation = VerseEvaluation(107verse_number=1, evaluator_model="test", scores=scores108)109assert evaluation.overall_score == 85.0110111def test_compute_flags_theological_concern(self) -> None:112"""Test flagging for theological accuracy below threshold."""113scores = {114"theological_accuracy": EvaluationScore(115dimension="theological_accuracy",116score=THEOLOGICAL_ACCURACY_THRESHOLD - 1,117confidence=90.0,118reasoning="test",119),120}121evaluation = VerseEvaluation(122verse_number=1, evaluator_model="test", scores=scores123)124flags = evaluation.compute_flags()125assert "theological_concern" in flags126127def test_compute_flags_low_confidence(self) -> None:128"""Test flagging for low confidence on theology."""129scores = {130"theological_accuracy": EvaluationScore(131dimension="theological_accuracy",132score=90.0,133confidence=CONFIDENCE_THRESHOLD - 1,134reasoning="test",135),136}137evaluation = VerseEvaluation(138verse_number=1, evaluator_model="test", scores=scores139)140flags = evaluation.compute_flags()141assert "low_confidence_theology" in flags142143def test_compute_flags_source_fidelity(self) -> None:144"""Test flagging for source fidelity below threshold."""145scores = {146"source_fidelity": EvaluationScore(147dimension="source_fidelity",148score=SOURCE_FIDELITY_THRESHOLD - 1,149confidence=90.0,150reasoning="test",151),152}153evaluation = VerseEvaluation(154verse_number=1, evaluator_model="test", scores=scores155)156flags = evaluation.compute_flags()157assert "source_fidelity_concern" in flags158159def test_compute_flags_any_low_dimension(self) -> None:160"""Test flagging for any dimension below threshold."""161scores = {162"naturalness": EvaluationScore(163dimension="naturalness",164score=ANY_DIMENSION_THRESHOLD - 1,165confidence=90.0,166reasoning="test",167),168}169evaluation = VerseEvaluation(170verse_number=1, evaluator_model="test", scores=scores171)172flags = evaluation.compute_flags()173assert "low_naturalness" in flags174175def test_no_flags_for_good_scores(self) -> None:176"""Test no flags when all scores are good."""177scores = {178dim: EvaluationScore(179dimension=dim, score=90.0, confidence=90.0, reasoning="test"180)181for dim in DIMENSIONS182}183evaluation = VerseEvaluation(184verse_number=1, evaluator_model="test", scores=scores185)186flags = evaluation.compute_flags()187assert len(flags) == 0188189190class TestChapterEvaluation:191"""Tests for ChapterEvaluation model."""192193def test_create_chapter_evaluation(self) -> None:194"""Test creating a chapter evaluation."""195evaluation = ChapterEvaluation(196book="titus",197chapter=1,198evaluator_model="test-model",199)200assert evaluation.book == "titus"201assert evaluation.chapter == 1202assert len(evaluation.verses) == 0203204def test_aggregate_scores(self) -> None:205"""Test aggregate scores calculation."""206scores1 = {207"dim1": EvaluationScore(208dimension="dim1", score=80.0, confidence=90.0, reasoning="test"209),210}211scores2 = {212"dim1": EvaluationScore(213dimension="dim1", score=90.0, confidence=90.0, reasoning="test"214),215}216verse1 = VerseEvaluation(verse_number=1, evaluator_model="test", scores=scores1)217verse2 = VerseEvaluation(verse_number=2, evaluator_model="test", scores=scores2)218219chapter = ChapterEvaluation(220book="test",221chapter=1,222evaluator_model="test",223verses={1: verse1, 2: verse2},224)225assert chapter.aggregate_scores["dim1"] == 85.0226227def test_flagged_verses(self) -> None:228"""Test flagged verses list."""229scores_bad = {230"theological_accuracy": EvaluationScore(231dimension="theological_accuracy",232score=50.0,233confidence=90.0,234reasoning="test",235),236}237scores_good = {238"theological_accuracy": EvaluationScore(239dimension="theological_accuracy",240score=95.0,241confidence=90.0,242reasoning="test",243),244}245246verse1 = VerseEvaluation(247verse_number=1, evaluator_model="test", scores=scores_bad248)249verse1.flags = verse1.compute_flags()250251verse2 = VerseEvaluation(252verse_number=2, evaluator_model="test", scores=scores_good253)254verse2.flags = verse2.compute_flags()255256chapter = ChapterEvaluation(257book="test",258chapter=1,259evaluator_model="test",260verses={1: verse1, 2: verse2},261)262assert 1 in chapter.flagged_verses263assert 2 not in chapter.flagged_verses264assert chapter.flagged_count == 1265266267class TestBuildEvaluationPrompt:268"""Tests for build_evaluation_prompt function."""269270def test_basic_prompt(self) -> None:271"""Test building a basic evaluation prompt."""272prompt = build_evaluation_prompt(273translation="初めに言葉があった。",274greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",275kjv_text="In the beginning was the Word",276reference_japanese=None,277glossary_terms=None,278)279assert "初めに言葉があった" in prompt280assert "Ἐν ἀρχῇ" in prompt281assert "In the beginning" in prompt282assert "theological_accuracy" in prompt.lower()283284def test_prompt_with_glossary(self) -> None:285"""Test prompt includes glossary terms."""286prompt = build_evaluation_prompt(287translation="test",288greek_text="test",289kjv_text=None,290reference_japanese=None,291glossary_terms=["言葉", "神"],292)293assert "言葉" in prompt294assert "神" in prompt295296def test_prompt_with_reference_japanese(self) -> None:297"""Test prompt includes reference Japanese."""298prompt = build_evaluation_prompt(299translation="test",300greek_text=None,301kjv_text=None,302reference_japanese="参考日本語訳",303glossary_terms=None,304)305assert "参考日本語訳" in prompt306307308class TestParseEvaluationResponse:309"""Tests for parse_evaluation_response function."""310311def test_parse_valid_json(self) -> None:312"""Test parsing valid JSON response."""313response = json.dumps(314{315dim: {"score": 90, "confidence": 85, "reasoning": "Good"}316for dim in DIMENSIONS317}318)319scores = parse_evaluation_response(response)320assert len(scores) == len(DIMENSIONS)321assert all(dim in scores for dim in DIMENSIONS)322assert scores["theological_accuracy"].score == 90323324def test_parse_json_in_code_block(self) -> None:325"""Test parsing JSON wrapped in code block."""326json_content = json.dumps(327{328dim: {"score": 90, "confidence": 85, "reasoning": "Good"}329for dim in DIMENSIONS330}331)332response = f"```json\n{json_content}\n```"333scores = parse_evaluation_response(response)334assert len(scores) == len(DIMENSIONS)335336def test_parse_invalid_json(self) -> None:337"""Test parsing invalid JSON raises error."""338with pytest.raises(ValueError, match="Failed to parse"):339parse_evaluation_response("not valid json")340341def test_parse_missing_dimension(self) -> None:342"""Test parsing response with missing dimension raises error."""343response = json.dumps(344{"theological_accuracy": {"score": 90, "confidence": 85, "reasoning": "Good"}}345)346with pytest.raises(ValueError, match="Missing dimension"):347parse_evaluation_response(response)348349350class TestTranslationEvaluator:351"""Tests for TranslationEvaluator class."""352353def test_create_evaluator(self) -> None:354"""Test creating an evaluator."""355evaluator = TranslationEvaluator(model_name="test-model")356assert evaluator.model_name == "test-model"357358def test_create_verse_evaluation(self) -> None:359"""Test creating a verse evaluation from scores."""360evaluator = TranslationEvaluator()361scores = {362dim: EvaluationScore(363dimension=dim, score=90.0, confidence=85.0, reasoning="Good"364)365for dim in DIMENSIONS366}367evaluation = evaluator.create_verse_evaluation(368verse_number=1, scores=scores, notes="Test note"369)370assert evaluation.verse_number == 1371assert evaluation.notes == "Test note"372assert len(evaluation.flags) == 0 # Good scores, no flags373374def test_create_chapter_evaluation(self) -> None:375"""Test creating a chapter evaluation."""376evaluator = TranslationEvaluator()377scores = {378dim: EvaluationScore(379dimension=dim, score=90.0, confidence=85.0, reasoning="Good"380)381for dim in DIMENSIONS382}383verse_eval = evaluator.create_verse_evaluation(verse_number=1, scores=scores)384385chapter_eval = evaluator.create_chapter_evaluation(386book="titus", chapter=1, verse_evaluations={1: verse_eval}387)388assert chapter_eval.book == "titus"389assert chapter_eval.chapter == 1390assert len(chapter_eval.verses) == 1391392393class TestEvaluationStorage:394"""Tests for EvaluationStorage class."""395396def test_save_and_load_chapter_evaluation(self) -> None:397"""Test saving and loading a chapter evaluation."""398with tempfile.TemporaryDirectory() as tmpdir:399storage = EvaluationStorage(Path(tmpdir))400401# Create evaluation402scores = {403dim: EvaluationScore(404dimension=dim, score=90.0, confidence=85.0, reasoning="Good"405)406for dim in DIMENSIONS407}408verse_eval = VerseEvaluation(409verse_number=1, evaluator_model="test", scores=scores410)411chapter_eval = ChapterEvaluation(412book="titus",413chapter=1,414evaluator_model="test",415verses={1: verse_eval},416)417418# Save419path = storage.save_chapter_evaluation(chapter_eval)420assert path.exists()421422# Load423loaded = storage.load_chapter_evaluation("titus", 1)424assert loaded is not None425assert loaded.book == "titus"426assert loaded.chapter == 1427assert len(loaded.verses) == 1428assert loaded.verses[1].scores["theological_accuracy"].score == 90.0429430def test_load_nonexistent(self) -> None:431"""Test loading non-existent evaluation returns None."""432with tempfile.TemporaryDirectory() as tmpdir:433storage = EvaluationStorage(Path(tmpdir))434loaded = storage.load_chapter_evaluation("nonexistent", 1)435assert loaded is None436437def test_is_chapter_evaluated(self) -> None:438"""Test checking if chapter is evaluated."""439with tempfile.TemporaryDirectory() as tmpdir:440storage = EvaluationStorage(Path(tmpdir))441442assert not storage.is_chapter_evaluated("titus", 1)443444# Save evaluation445chapter_eval = ChapterEvaluation(446book="titus", chapter=1, evaluator_model="test", verses={}447)448storage.save_chapter_evaluation(chapter_eval)449450assert storage.is_chapter_evaluated("titus", 1)451452def test_get_flagged_verses(self) -> None:453"""Test getting flagged verses for a book."""454with tempfile.TemporaryDirectory() as tmpdir:455storage = EvaluationStorage(Path(tmpdir))456457# Create evaluation with flagged verse458scores = {459"theological_accuracy": EvaluationScore(460dimension="theological_accuracy",461score=50.0,462confidence=90.0,463reasoning="Issues",464),465}466verse_eval = VerseEvaluation(467verse_number=5, evaluator_model="test", scores=scores468)469verse_eval.flags = verse_eval.compute_flags()470471chapter_eval = ChapterEvaluation(472book="titus",473chapter=1,474evaluator_model="test",475verses={5: verse_eval},476)477storage.save_chapter_evaluation(chapter_eval)478479flagged = storage.get_flagged_verses("titus")480assert len(flagged) == 1481assert flagged[0].chapter == 1482assert flagged[0].verse == 5483484def test_get_book_summary(self) -> None:485"""Test getting book evaluation summary."""486with tempfile.TemporaryDirectory() as tmpdir:487storage = EvaluationStorage(Path(tmpdir))488489# Save two chapter evaluations490for chapter in [1, 2]:491scores = {492dim: EvaluationScore(493dimension=dim,494score=85.0 + chapter,495confidence=90.0,496reasoning="Good",497)498for dim in DIMENSIONS499}500verse_eval = VerseEvaluation(501verse_number=1, evaluator_model="test", scores=scores502)503chapter_eval = ChapterEvaluation(504book="titus",505chapter=chapter,506evaluator_model="test",507verses={1: verse_eval},508)509storage.save_chapter_evaluation(chapter_eval)510511summary = storage.get_book_summary("titus")512assert summary is not None513assert summary.book == "titus"514assert summary.total_chapters == 2515assert summary.total_verses == 2516assert summary.overall_score > 0517518def test_get_book_summary_no_evaluations(self) -> None:519"""Test getting summary when no evaluations exist."""520with tempfile.TemporaryDirectory() as tmpdir:521storage = EvaluationStorage(Path(tmpdir))522summary = storage.get_book_summary("nonexistent")523assert summary is None524525def test_get_book_summary_dir_exists_no_eval_files(self) -> None:526"""Test getting summary when book dir exists but has no eval files."""527with tempfile.TemporaryDirectory() as tmpdir:528storage = EvaluationStorage(Path(tmpdir))529# Create the book directory but don't add any eval files530book_dir = Path(tmpdir) / "output" / "titus"531book_dir.mkdir(parents=True)532# Add a non-eval file533(book_dir / "chapter_01.json").write_text("{}")534535summary = storage.get_book_summary("titus")536assert summary is None537538def test_get_book_summary_with_flagged_verses(self) -> None:539"""Test getting book summary with flagged verses."""540with tempfile.TemporaryDirectory() as tmpdir:541storage = EvaluationStorage(Path(tmpdir))542543# Create evaluation with low theological accuracy (will be flagged)544scores = {545dim: EvaluationScore(546dimension=dim,547score=95.0 if dim != "theological_accuracy" else 75.0,548confidence=90.0,549reasoning="Test",550)551for dim in DIMENSIONS552}553verse_eval = VerseEvaluation(554verse_number=5,555evaluator_model="test",556scores=scores,557flags=["theological_concern"], # Explicitly set flag558)559chapter_eval = ChapterEvaluation(560book="titus",561chapter=1,562evaluator_model="test",563verses={5: verse_eval},564)565storage.save_chapter_evaluation(chapter_eval)566567summary = storage.get_book_summary("titus")568assert summary is not None569assert len(summary.flagged_verses) == 1570assert "titus:1:5" in summary.flagged_verses571572573class TestBookEvaluationSummary:574"""Tests for BookEvaluationSummary model."""575576def test_create_summary(self) -> None:577"""Test creating a book evaluation summary."""578summary = BookEvaluationSummary(579book="titus",580total_chapters=3,581total_verses=46,582aggregate_scores={583"theological_accuracy": 92.5,584"source_fidelity": 88.0,585},586overall_score=90.3,587flagged_verses=["titus:1:5", "titus:2:3"],588)589assert summary.book == "titus"590assert summary.total_chapters == 3591assert summary.total_verses == 46592assert len(summary.flagged_verses) == 2593594595class TestParseEvaluationResponseExtended:596"""Extended tests for parse_evaluation_response function."""597598def test_parse_json_in_plain_code_block(self) -> None:599"""Test parsing JSON wrapped in plain code block (no json tag)."""600json_content = json.dumps(601{602dim: {"score": 90, "confidence": 85, "reasoning": "Good"}603for dim in DIMENSIONS604}605)606response = f"```\n{json_content}\n```"607scores = parse_evaluation_response(response)608assert len(scores) == len(DIMENSIONS)609610611class TestEvaluationStorageExtended:612"""Extended tests for EvaluationStorage class."""613614def test_serialize_datetime(self) -> None:615"""Test datetime serialization helper."""616with tempfile.TemporaryDirectory() as tmpdir:617storage = EvaluationStorage(Path(tmpdir))618dt = datetime(2024, 1, 15, 10, 30, 0)619result = storage._serialize_datetime(dt)620assert result == "2024-01-15T10:30:00"621622def test_serialize_datetime_invalid_type(self) -> None:623"""Test datetime serialization with invalid type."""624with tempfile.TemporaryDirectory() as tmpdir:625storage = EvaluationStorage(Path(tmpdir))626with pytest.raises(TypeError, match="not JSON serializable"):627storage._serialize_datetime("not a datetime")628629def test_get_flagged_verses_empty_book(self) -> None:630"""Test getting flagged verses when book directory doesn't exist."""631with tempfile.TemporaryDirectory() as tmpdir:632storage = EvaluationStorage(Path(tmpdir))633flagged = storage.get_flagged_verses("nonexistent")634assert flagged == []635636def test_load_chapter_with_missing_fields(self) -> None:637"""Test loading chapter with some optional fields missing."""638with tempfile.TemporaryDirectory() as tmpdir:639storage = EvaluationStorage(Path(tmpdir))640641# Create minimal evaluation file642output_dir = Path(tmpdir) / "output" / "titus"643output_dir.mkdir(parents=True)644eval_file = output_dir / "chapter_01_eval.json"645646data = {647"book": "titus",648"chapter": 1,649"verses": {650"1": {651"scores": {652dim: {"score": 90, "confidence": 85, "reasoning": "Good"}653for dim in DIMENSIONS654},655"overall_score": 90.0,656"flags": [],657}658},659}660with eval_file.open("w") as f:661json.dump(data, f)662663# Load should work even with missing optional fields664loaded = storage.load_chapter_evaluation("titus", 1)665assert loaded is not None666assert loaded.book == "titus"667668669class TestTranslationEvaluatorExtended:670"""Extended tests for TranslationEvaluator class."""671672def test_build_verse_prompt_with_context(self) -> None:673"""Test building verse prompt with full translation context."""674from src.models.verse import (675GlossaryTerm,676TranslationContext,677Verse,678VerseReference,679)680681# Create a mock context682ref = VerseReference(book="titus", chapter=1, verse=1)683verse = Verse(684reference=ref,685greek_text="Παῦλος δοῦλος θεοῦ",686greek_translit=None,687hebrew_text=None,688hebrew_translit=None,689japanese_meiji=None,690japanese_taisho="神の僕パウロ",691english_kjv="Paul, a servant of God",692english_asv=None,693)694glossary = [695GlossaryTerm(696original="θεός",697transliteration="theos",698japanese="神",699english="God",700notes=None,701)702]703context = TranslationContext(704target_verse=verse,705preceding_verses=[],706following_verses=[],707chapter_summary=None,708book_metadata=None,709glossary_terms=glossary,710)711712evaluator = TranslationEvaluator()713prompt = evaluator.build_verse_prompt("神の僕パウロから", context)714715assert "神の僕パウロから" in prompt716assert "Παῦλος" in prompt717assert "神" in prompt # Glossary term718719def test_build_verse_prompt_empty_glossary(self) -> None:720"""Test building verse prompt with empty glossary."""721from src.models.verse import TranslationContext, Verse, VerseReference722723ref = VerseReference(book="titus", chapter=1, verse=1)724verse = Verse(725reference=ref,726greek_text="Test Greek",727greek_translit=None,728hebrew_text=None,729hebrew_translit=None,730japanese_meiji=None,731japanese_taisho=None,732english_kjv="Test English",733english_asv=None,734)735context = TranslationContext(736target_verse=verse,737preceding_verses=[],738following_verses=[],739chapter_summary=None,740book_metadata=None,741glossary_terms=[], # Empty glossary742)743744evaluator = TranslationEvaluator()745prompt = evaluator.build_verse_prompt("テスト翻訳", context)746747assert "テスト翻訳" in prompt748assert "GLOSSARY TERMS" not in prompt # Should not include glossary section749