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