"""Branches the 2026-09 coverage audit found untested outside the core pipeline. Evaluation storage/prompting, models, the originals exporter's segment grouping, proto reconstruction and the TUI. No model is called; every file lives under tmp_path. """ from __future__ import annotations import dataclasses import json import typing from io import StringIO from pathlib import Path from unittest.mock import MagicMock import pytest from rich.console import Console from src.evaluation.evaluator import TranslationEvaluator, _build_style_criteria from src.evaluation.storage import EvaluationStorage from src.models.evaluation import ChapterEvaluation, EvaluationScore, VerseEvaluation from src.models.pericope import ChunkTranslationContext, Pericope from src.models.style import TranslationStyle, build_system_prompt from src.models.verse import TranslationContext, Verse, VerseReference from src.output.originals import group_segments from src.protos.serialization import _reconstruct_dataclass from src.tui.data_provider import TUIDataProvider, VerseData from src.tui.widgets import ( QUALITY_CONCERN, ChapterQualitySummary, DetailPanel, VerseTree, ) def _render(renderable) -> str: # Wide enough that 80 full-width characters (160 columns) never wrap. console = Console(file=StringIO(), force_terminal=False, width=400) console.print(renderable) return console.file.getvalue() def _score(dim, score=90.0, confidence=90.0, reasoning="ok"): return EvaluationScore(dimension=dim, score=score, confidence=confidence, reasoning=reasoning) def _verse_eval(number, scores=None, flags=None, notes=None): return VerseEvaluation( verse_number=number, evaluator_model="m", scores=scores if scores is not None else {"naturalness": _score("naturalness")}, flags=flags or [], notes=notes, ) # --- evaluation prompt ------------------------------------------------------------ class TestEvaluatorPrompt: def test_non_web_native_readability_has_no_audience_line(self): text = _build_style_criteria({"readability_level": "scholarly"}) assert "20s" not in text assert "web_native" not in text assert "STYLE ADHERENCE" in text def test_style_config_is_loaded_once_and_cached(self, tmp_path): (tmp_path / "config").mkdir() style = tmp_path / "config" / "style.json" style.write_text(json.dumps({"formality_level": "polite"}), encoding="utf-8") evaluator = TranslationEvaluator(data_dir=tmp_path) first = evaluator.style_config style.write_text(json.dumps({"formality_level": "formal"}), encoding="utf-8") assert evaluator.style_config is first assert first == {"formality_level": "polite"} def test_neighbour_without_any_text_is_left_out_of_context(self, tmp_path): def verse(n, **texts): return Verse(reference=VerseReference(book="titus", chapter=1, verse=n), **texts) context = TranslationContext( target_verse=verse(2, english_kjv="target"), preceding_verses=[verse(1)], # nothing saved, no reference text following_verses=[verse(3, japanese_kougo="口語訳の三節")], ) text = TranslationEvaluator(data_dir=tmp_path)._format_surrounding_context(context) assert text == "[after 3 — reference version] 口語訳の三節" # --- evaluation storage ------------------------------------------------------------- class TestEvaluationStorage: def test_second_verse_is_merged_into_the_existing_chapter(self, tmp_path): storage = EvaluationStorage(tmp_path) storage.save_verse_evaluation("titus", 1, _verse_eval(1)) storage.save_verse_evaluation("titus", 1, _verse_eval(2, flags=["low"])) chapter = storage.load_chapter_evaluation("titus", 1) assert sorted(chapter.verses) == [1, 2] assert chapter.flagged_verses == [2] def test_unpadded_stray_eval_file_is_skipped_not_misread(self, tmp_path): # The glob matches chapter_1_eval.json but the loader looks for the # zero-padded chapter_01_eval.json, so the stray file yields nothing. storage = EvaluationStorage(tmp_path) storage.save_verse_evaluation("titus", 2, _verse_eval(5, flags=["x"])) (tmp_path / "output" / "titus" / "chapter_1_eval.json").write_text("{}", encoding="utf-8") flagged = storage.get_flagged_verses("titus") assert [(r.chapter, r.verse) for r in flagged] == [(2, 5)] summary = storage.get_book_summary("titus") assert summary.total_verses == 1 def test_book_summary_is_none_when_no_chapter_has_scores(self, tmp_path): storage = EvaluationStorage(tmp_path) storage.save_chapter_evaluation( ChapterEvaluation(book="titus", chapter=1, evaluator_model="m", verses={}) ) assert storage.get_book_summary("titus") is None # --- models --------------------------------------------------------------------------- class TestModels: def test_verse_evaluation_without_scores_scores_zero(self): assert _verse_eval(1, scores={}).overall_score == 0.0 def test_source_texts_omits_verses_with_no_original(self): pericope = Pericope( id="p1", start_chapter=1, start_verse=1, end_chapter=1, end_verse=3, verses=["1:1", "1:2", "1:3"], ) verses = [ Verse(reference=VerseReference(book="ruth", chapter=1, verse=1), hebrew_text="וַיְהִי"), Verse(reference=VerseReference(book="ruth", chapter=1, verse=2)), Verse(reference=VerseReference(book="ruth", chapter=1, verse=3), greek_text="καί"), ] context = ChunkTranslationContext(pericope=pericope, book="ruth", verses=verses) assert context.source_texts == {"1:1": "וַיְהִי", "1:3": "καί"} def test_system_prompt_without_archaic_list_skips_that_line(self): prompt = build_system_prompt(TranslationStyle(avoid_archaic=[])) assert "Avoid archaic expressions" not in prompt with_list = build_system_prompt(TranslationStyle(avoid_archaic=["候"])) assert "Avoid archaic expressions (候)" in with_list # --- originals exporter --------------------------------------------------------------- class TestGroupSegments: def test_strongs_are_kept_per_word_across_a_joined_run(self): words = [ {"language": "heb", "source": "oshb", "text": "בְּרֵאשִׁית", "strong": "H7225"}, {"language": "heb", "source": "oshb", "text": "בָּרָא"}, {"language": "grc", "source": "lxx", "text": "ἐν", "strong": "G1722"}, ] segments = group_segments(words) assert segments[0]["text"] == "בְּרֵאשִׁית בָּרָא" # An unkeyed word keeps its slot, so split(" ") still lines up. assert segments[0]["strongs"] == "H7225 " assert segments[0]["strongs"].split(" ") == ["H7225", ""] assert segments[1] == {"language": "grc", "source": "lxx", "text": "ἐν", "strongs": "G1722"} def test_a_word_containing_a_space_is_rejected(self): with pytest.raises(ValueError, match="contains a space"): group_segments([{"language": "heb", "source": "oshb", "text": "two words"}]) # --- proto reconstruction --------------------------------------------------------------- @dataclasses.dataclass class _Inner: name: str = "" @dataclasses.dataclass class _Bare: items: typing.List = dataclasses.field(default_factory=list) # noqa: UP006 - bare List has no args inner: _Inner | None = None class TestReconstructDataclass: def test_bare_list_annotation_keeps_items_as_given(self): obj = _reconstruct_dataclass({"items": [{"name": "a"}, 1]}, _Bare) assert obj.items == [{"name": "a"}, 1] def test_optional_nested_dataclass_is_rebuilt(self): obj = _reconstruct_dataclass({"inner": {"name": "x"}, "unknown": 1}, _Bare) assert obj.inner == _Inner(name="x") # --- TUI data provider -------------------------------------------------------------------- class TestTUIDataProvider: def test_only_book_dirs_with_chapter_files_are_listed(self, tmp_path): out = tmp_path / "output" (out / "titus").mkdir(parents=True) (out / "titus" / "chapter_01.json").write_text("{}", encoding="utf-8") (out / "ruth").mkdir() (out / "ruth" / "chapter_01_eval.json").write_text("{}", encoding="utf-8") # eval only (out / "notes.txt").write_text("not a book", encoding="utf-8") assert TUIDataProvider(tmp_path).get_available_books() == ["titus"] def test_verse_data_for_an_untranslated_chapter(self, tmp_path): provider = TUIDataProvider(tmp_path) provider.loader = MagicMock() provider.loader.load_verse.return_value = Verse( reference=VerseReference(book="titus", chapter=2, verse=1), english_kjv="But speak" ) data = provider.get_verse_data("titus", 2, 1) assert data.kjv_text == "But speak" assert data.translation_text is None and data.glossary_terms == [] assert data.evaluation is None # --- TUI widgets -------------------------------------------------------------------------- def _verse_data(**over): base = dict( book="titus", chapter=1, verse=1, greek_text=None, kjv_text=None, japanese_reference=None, glossary_terms=[], translation_text=None, translation_model=None, translation_timestamp=None, evaluation=None, ) base.update(over) return VerseData(**base) class TestDetailPanel: def _panel(self, **over): panel = DetailPanel(MagicMock(), id="detail") panel.current_verse = _verse_data(**over) return _render(panel.render()) def test_translation_without_model_shows_no_model_line(self): out = self._panel(translation_text="訳文") assert "訳文" in out and "Model:" not in out def test_model_without_timestamp_has_no_separator(self): out = self._panel(translation_text="訳文", translation_model="opus") assert "Model: opus" in out and "|" not in out.split("Model: opus")[1].split("\n")[0] def test_evaluation_without_scores_has_no_details_section(self): out = self._panel(evaluation=_verse_eval(1, scores={})) assert "Details:" not in out assert "No flags" in out def test_long_reasoning_is_truncated_and_blank_reasoning_skipped(self): scores = { "naturalness": _score("naturalness", reasoning="あ" * 90), "readability": _score("readability", reasoning=""), } out = self._panel(evaluation=_verse_eval(1, scores=scores)) details = out.split("Details:")[1] assert "あ" * 80 + "..." in details assert "Readability:" not in details class TestVerseTreeLabels: def test_unevaluated_chapter_has_no_star(self): provider = MagicMock() provider.get_available_books.return_value = ["titus"] provider.get_book_chapters.return_value = [1, 2] provider.is_chapter_evaluated.side_effect = lambda b, c: c == 1 provider.get_chapter_verses.return_value = [] tree = VerseTree(provider, id="tree") tree._populate_tree() (book,) = tree.root.children assert [str(c.label) for c in book.children] == ["Chapter 1 *", "Chapter 2"] class TestChapterQualitySummaryRendering: def _summary(self, verses): provider = MagicMock() provider.eval_storage.load_chapter_evaluation.return_value = ChapterEvaluation( book="titus", chapter=1, evaluator_model="m", verses=verses ) widget = ChapterQualitySummary(provider, id="q") widget.update_chapter("titus", 1) return _render(widget.render()) def test_clean_chapter_lists_no_flagged_verses(self): out = self._summary({1: _verse_eval(1), 2: _verse_eval(2)}) assert "2 total, 2 clean" in out assert "flagged" not in out.lower() def test_flagged_chapter_lists_them(self): out = self._summary({1: _verse_eval(1), 3: _verse_eval(3, flags=["x"])}) assert ", 1 flagged" in out and "Flagged Verses: 3" in out def test_score_below_concern_is_red(self): widget = ChapterQualitySummary(MagicMock(), id="q") assert widget._get_score_color(QUALITY_CONCERN - 0.1) == "red"