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

OPEN SOURCE

software/tests/test_coverage_audit_periphery.py

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

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

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

software/tests/test_coverage_audit_periphery.py298 行このファイルを保存 ↓

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

ファイルの確認情報SHA-256 5af0cd07d5b2f5c7e66ab33afdd4e3217a4be7c895c263bfeffadf778e196b7e
1"""Branches the 2026-09 coverage audit found untested outside the core pipeline.23Evaluation storage/prompting, models, the originals exporter's segment4grouping, proto reconstruction and the TUI. No model is called; every file5lives under tmp_path.6"""78from __future__ import annotations910import dataclasses11import json12import typing13from io import StringIO14from pathlib import Path15from unittest.mock import MagicMock1617import pytest18from rich.console import Console1920from src.evaluation.evaluator import TranslationEvaluator, _build_style_criteria21from src.evaluation.storage import EvaluationStorage22from src.models.evaluation import ChapterEvaluation, EvaluationScore, VerseEvaluation23from src.models.pericope import ChunkTranslationContext, Pericope24from src.models.style import TranslationStyle, build_system_prompt25from src.models.verse import TranslationContext, Verse, VerseReference26from src.output.originals import group_segments27from src.protos.serialization import _reconstruct_dataclass28from src.tui.data_provider import TUIDataProvider, VerseData29from src.tui.widgets import (30    QUALITY_CONCERN,31    ChapterQualitySummary,32    DetailPanel,33    VerseTree,34)353637def _render(renderable) -> str:38    # Wide enough that 80 full-width characters (160 columns) never wrap.39    console = Console(file=StringIO(), force_terminal=False, width=400)40    console.print(renderable)41    return console.file.getvalue()424344def _score(dim, score=90.0, confidence=90.0, reasoning="ok"):45    return EvaluationScore(dimension=dim, score=score, confidence=confidence, reasoning=reasoning)464748def _verse_eval(number, scores=None, flags=None, notes=None):49    return VerseEvaluation(50        verse_number=number,51        evaluator_model="m",52        scores=scores if scores is not None else {"naturalness": _score("naturalness")},53        flags=flags or [],54        notes=notes,55    )565758# --- evaluation prompt ------------------------------------------------------------596061class TestEvaluatorPrompt:62    def test_non_web_native_readability_has_no_audience_line(self):63        text = _build_style_criteria({"readability_level": "scholarly"})64        assert "20s" not in text65        assert "web_native" not in text66        assert "STYLE ADHERENCE" in text6768    def test_style_config_is_loaded_once_and_cached(self, tmp_path):69        (tmp_path / "config").mkdir()70        style = tmp_path / "config" / "style.json"71        style.write_text(json.dumps({"formality_level": "polite"}), encoding="utf-8")72        evaluator = TranslationEvaluator(data_dir=tmp_path)73        first = evaluator.style_config74        style.write_text(json.dumps({"formality_level": "formal"}), encoding="utf-8")75        assert evaluator.style_config is first76        assert first == {"formality_level": "polite"}7778    def test_neighbour_without_any_text_is_left_out_of_context(self, tmp_path):79        def verse(n, **texts):80            return Verse(reference=VerseReference(book="titus", chapter=1, verse=n), **texts)8182        context = TranslationContext(83            target_verse=verse(2, english_kjv="target"),84            preceding_verses=[verse(1)],  # nothing saved, no reference text85            following_verses=[verse(3, japanese_kougo="口語訳の三節")],86        )87        text = TranslationEvaluator(data_dir=tmp_path)._format_surrounding_context(context)88        assert text == "[after 3 — reference version] 口語訳の三節"899091# --- evaluation storage -------------------------------------------------------------929394class TestEvaluationStorage:95    def test_second_verse_is_merged_into_the_existing_chapter(self, tmp_path):96        storage = EvaluationStorage(tmp_path)97        storage.save_verse_evaluation("titus", 1, _verse_eval(1))98        storage.save_verse_evaluation("titus", 1, _verse_eval(2, flags=["low"]))99        chapter = storage.load_chapter_evaluation("titus", 1)100        assert sorted(chapter.verses) == [1, 2]101        assert chapter.flagged_verses == [2]102103    def test_unpadded_stray_eval_file_is_skipped_not_misread(self, tmp_path):104        # The glob matches chapter_1_eval.json but the loader looks for the105        # zero-padded chapter_01_eval.json, so the stray file yields nothing.106        storage = EvaluationStorage(tmp_path)107        storage.save_verse_evaluation("titus", 2, _verse_eval(5, flags=["x"]))108        (tmp_path / "output" / "titus" / "chapter_1_eval.json").write_text("{}", encoding="utf-8")109        flagged = storage.get_flagged_verses("titus")110        assert [(r.chapter, r.verse) for r in flagged] == [(2, 5)]111        summary = storage.get_book_summary("titus")112        assert summary.total_verses == 1113114    def test_book_summary_is_none_when_no_chapter_has_scores(self, tmp_path):115        storage = EvaluationStorage(tmp_path)116        storage.save_chapter_evaluation(117            ChapterEvaluation(book="titus", chapter=1, evaluator_model="m", verses={})118        )119        assert storage.get_book_summary("titus") is None120121122# --- models ---------------------------------------------------------------------------123124125class TestModels:126    def test_verse_evaluation_without_scores_scores_zero(self):127        assert _verse_eval(1, scores={}).overall_score == 0.0128129    def test_source_texts_omits_verses_with_no_original(self):130        pericope = Pericope(131            id="p1", start_chapter=1, start_verse=1, end_chapter=1, end_verse=3,132            verses=["1:1", "1:2", "1:3"],133        )134        verses = [135            Verse(reference=VerseReference(book="ruth", chapter=1, verse=1), hebrew_text="וַיְהִי"),136            Verse(reference=VerseReference(book="ruth", chapter=1, verse=2)),137            Verse(reference=VerseReference(book="ruth", chapter=1, verse=3), greek_text="καί"),138        ]139        context = ChunkTranslationContext(pericope=pericope, book="ruth", verses=verses)140        assert context.source_texts == {"1:1": "וַיְהִי", "1:3": "καί"}141142    def test_system_prompt_without_archaic_list_skips_that_line(self):143        prompt = build_system_prompt(TranslationStyle(avoid_archaic=[]))144        assert "Avoid archaic expressions" not in prompt145        with_list = build_system_prompt(TranslationStyle(avoid_archaic=["候"]))146        assert "Avoid archaic expressions (候)" in with_list147148149# --- originals exporter ---------------------------------------------------------------150151152class TestGroupSegments:153    def test_strongs_are_kept_per_word_across_a_joined_run(self):154        words = [155            {"language": "heb", "source": "oshb", "text": "בְּרֵאשִׁית", "strong": "H7225"},156            {"language": "heb", "source": "oshb", "text": "בָּרָא"},157            {"language": "grc", "source": "lxx", "text": "ἐν", "strong": "G1722"},158        ]159        segments = group_segments(words)160        assert segments[0]["text"] == "בְּרֵאשִׁית בָּרָא"161        # An unkeyed word keeps its slot, so split(" ") still lines up.162        assert segments[0]["strongs"] == "H7225 "163        assert segments[0]["strongs"].split(" ") == ["H7225", ""]164        assert segments[1] == {"language": "grc", "source": "lxx", "text": "ἐν", "strongs": "G1722"}165166    def test_a_word_containing_a_space_is_rejected(self):167        with pytest.raises(ValueError, match="contains a space"):168            group_segments([{"language": "heb", "source": "oshb", "text": "two words"}])169170171# --- proto reconstruction ---------------------------------------------------------------172173174@dataclasses.dataclass175class _Inner:176    name: str = ""177178179@dataclasses.dataclass180class _Bare:181    items: typing.List = dataclasses.field(default_factory=list)  # noqa: UP006 - bare List has no args182    inner: _Inner | None = None183184185class TestReconstructDataclass:186    def test_bare_list_annotation_keeps_items_as_given(self):187        obj = _reconstruct_dataclass({"items": [{"name": "a"}, 1]}, _Bare)188        assert obj.items == [{"name": "a"}, 1]189190    def test_optional_nested_dataclass_is_rebuilt(self):191        obj = _reconstruct_dataclass({"inner": {"name": "x"}, "unknown": 1}, _Bare)192        assert obj.inner == _Inner(name="x")193194195# --- TUI data provider --------------------------------------------------------------------196197198class TestTUIDataProvider:199    def test_only_book_dirs_with_chapter_files_are_listed(self, tmp_path):200        out = tmp_path / "output"201        (out / "titus").mkdir(parents=True)202        (out / "titus" / "chapter_01.json").write_text("{}", encoding="utf-8")203        (out / "ruth").mkdir()204        (out / "ruth" / "chapter_01_eval.json").write_text("{}", encoding="utf-8")  # eval only205        (out / "notes.txt").write_text("not a book", encoding="utf-8")206        assert TUIDataProvider(tmp_path).get_available_books() == ["titus"]207208    def test_verse_data_for_an_untranslated_chapter(self, tmp_path):209        provider = TUIDataProvider(tmp_path)210        provider.loader = MagicMock()211        provider.loader.load_verse.return_value = Verse(212            reference=VerseReference(book="titus", chapter=2, verse=1), english_kjv="But speak"213        )214        data = provider.get_verse_data("titus", 2, 1)215        assert data.kjv_text == "But speak"216        assert data.translation_text is None and data.glossary_terms == []217        assert data.evaluation is None218219220# --- TUI widgets --------------------------------------------------------------------------221222223def _verse_data(**over):224    base = dict(225        book="titus", chapter=1, verse=1, greek_text=None, kjv_text=None,226        japanese_reference=None, glossary_terms=[], translation_text=None,227        translation_model=None, translation_timestamp=None, evaluation=None,228    )229    base.update(over)230    return VerseData(**base)231232233class TestDetailPanel:234    def _panel(self, **over):235        panel = DetailPanel(MagicMock(), id="detail")236        panel.current_verse = _verse_data(**over)237        return _render(panel.render())238239    def test_translation_without_model_shows_no_model_line(self):240        out = self._panel(translation_text="訳文")241        assert "訳文" in out and "Model:" not in out242243    def test_model_without_timestamp_has_no_separator(self):244        out = self._panel(translation_text="訳文", translation_model="opus")245        assert "Model: opus" in out and "|" not in out.split("Model: opus")[1].split("\n")[0]246247    def test_evaluation_without_scores_has_no_details_section(self):248        out = self._panel(evaluation=_verse_eval(1, scores={}))249        assert "Details:" not in out250        assert "No flags" in out251252    def test_long_reasoning_is_truncated_and_blank_reasoning_skipped(self):253        scores = {254            "naturalness": _score("naturalness", reasoning="あ" * 90),255            "readability": _score("readability", reasoning=""),256        }257        out = self._panel(evaluation=_verse_eval(1, scores=scores))258        details = out.split("Details:")[1]259        assert "あ" * 80 + "..." in details260        assert "Readability:" not in details261262263class TestVerseTreeLabels:264    def test_unevaluated_chapter_has_no_star(self):265        provider = MagicMock()266        provider.get_available_books.return_value = ["titus"]267        provider.get_book_chapters.return_value = [1, 2]268        provider.is_chapter_evaluated.side_effect = lambda b, c: c == 1269        provider.get_chapter_verses.return_value = []270        tree = VerseTree(provider, id="tree")271        tree._populate_tree()272        (book,) = tree.root.children273        assert [str(c.label) for c in book.children] == ["Chapter 1 *", "Chapter 2"]274275276class TestChapterQualitySummaryRendering:277    def _summary(self, verses):278        provider = MagicMock()279        provider.eval_storage.load_chapter_evaluation.return_value = ChapterEvaluation(280            book="titus", chapter=1, evaluator_model="m", verses=verses281        )282        widget = ChapterQualitySummary(provider, id="q")283        widget.update_chapter("titus", 1)284        return _render(widget.render())285286    def test_clean_chapter_lists_no_flagged_verses(self):287        out = self._summary({1: _verse_eval(1), 2: _verse_eval(2)})288        assert "2 total, 2 clean" in out289        assert "flagged" not in out.lower()290291    def test_flagged_chapter_lists_them(self):292        out = self._summary({1: _verse_eval(1), 3: _verse_eval(3, flags=["x"])})293        assert ", 1 flagged" in out and "Flagged Verses: 3" in out294295    def test_score_below_concern_is_red(self):296        widget = ChapterQualitySummary(MagicMock(), id="q")297        assert widget._get_score_color(QUALITY_CONCERN - 0.1) == "red"298