test: cover help_md keys, header smoke, and bilingual ES smoke

Two stale Spanish smoke assertions still expected English page titles
for PDF Extractor and Reconciler — the i18n work landed real
translations ("PDF a CSV", "Reconciliar dos archivos"), so refresh the
expected substrings and the surrounding comment.

Add new coverage for the help-popover feature:
- TestHelpPopoverKeys (test_lang_packs): every tool_id resolves a
  non-empty tools.<id>.help_md in BOTH packs; help.button_label and
  help.missing_body resolve in both.
- TestDescriptionCopy (test_tools_registry): every Tool.description
  non-empty and under 120 chars — pins the post-jargon-scrub copy
  so future drift back into multi-clause prose is loud.
- TestRenderToolHeaderSmoke: render_tool_header is callable, listed
  in components.__all__, and every i18n key it touches resolves in
  both packs. Runs without a Streamlit script context.

Suite: 2427 passed (+9 new), 91 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 18:07:02 +00:00
parent 4a8961d58a
commit 4955fb239b
3 changed files with 106 additions and 6 deletions

View File

@@ -63,12 +63,11 @@ EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
"7_Multi_File_Merger": {"en": "Combine Files", "es": "Combinar archivos"},
"8_Validator_Reporter": {"en": "Quality Check", "es": "Verificación de calidad"},
"9_Pipeline_Runner": {"en": "Automated", "es": "Flujos automatizados"},
# The PDF Extractor and Reconciler pages are English-only today
# (translations tracked as a follow-up). The smoke test value is
# still that the page *renders at all* in 'es'; the substring is
# the same English hero text under both languages.
"10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF to CSV"},
"11_Reconciler": {"en": "Reconcile", "es": "Reconcile"},
# PDF Extractor + Reconciler page titles are now translated in
# both packs (``tools.<id>.page_title``). Their hero copy diverges
# by language, so the smoke test pins the localized substring.
"10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF a CSV"},
"11_Reconciler": {"en": "Reconcile", "es": "Reconciliar"},
"99_Close": {"en": "Shutting down", "es": "Cerrando"},
}

View File

@@ -178,3 +178,32 @@ class TestKeyCoverage:
for lang in ("en", "es"):
value = t(key, lang)
assert value and value != key, f"missing {key!r} in {lang}"
class TestHelpPopoverKeys:
"""Every tool's inline Help popover (``render_tool_header``) pulls
its copy from ``tools.<id>.help_md`` and the two shared labels
``help.button_label`` / ``help.missing_body``. A missing key would
fall back to the literal lookup key and render that string in the
popover instead of helpful content."""
@pytest.mark.parametrize("lang", ["en", "es"])
def test_help_shared_keys_present(self, lang):
for key in ("help.button_label", "help.missing_body"):
value = t(key, lang)
assert value and value != key, f"missing {key!r} in {lang!r}"
@pytest.mark.parametrize("lang", ["en", "es"])
def test_every_tool_has_help_md(self, lang):
# Import lazily so this file stays importable without the GUI.
from src.gui.tools_registry import TOOLS
missing: list[str] = []
for tool in TOOLS:
key = f"tools.{tool.tool_id}.help_md"
value = t(key, lang)
if not value or value == key or not value.strip():
missing.append(tool.tool_id)
assert not missing, (
f"language {lang!r} is missing help_md for: {missing}"
)

View File

@@ -157,6 +157,78 @@ class TestLocalizedAccessors:
assert label and label != f"nav.section_{section}"
class TestDescriptionCopy:
"""The post-jargon-strip descriptions are intentionally tight one-
liners. Pin them so future drift toward bloated marketing copy
(or an accidentally-empty string) is caught by CI."""
# Roomy upper bound; the tightest description today is ~60 chars
# and the longest is just over 90. ~120 leaves headroom for minor
# copy tweaks without inviting paragraph-length card bodies.
_MAX_DESCRIPTION_CHARS = 120
def test_every_description_is_non_empty(self):
empty = [t.tool_id for t in TOOLS if not t.description.strip()]
assert not empty, f"tools with empty descriptions: {empty}"
def test_every_description_under_max_chars(self):
too_long = [
(t.tool_id, len(t.description))
for t in TOOLS
if len(t.description) > self._MAX_DESCRIPTION_CHARS
]
assert not too_long, (
f"tool descriptions exceed {self._MAX_DESCRIPTION_CHARS} chars: "
f"{too_long}"
)
class TestRenderToolHeaderSmoke:
"""``render_tool_header`` is the helper every tool page now calls in
place of ``st.title(...) + st.caption(...)``. We can't render it
without a Streamlit script context, but we CAN verify it imports
cleanly via the public ``src.gui.components`` surface and resolves
the expected i18n keys for a known tool id."""
def test_importable_from_public_components_package(self):
from src.gui.components import render_tool_header
assert callable(render_tool_header)
def test_listed_in_public_all(self):
# The public ``__all__`` is what per-tool builds key off; a
# removal here would silently break tool pages that import
# from ``src.gui.components`` directly.
from src.gui import components as components_pkg
assert "render_tool_header" in components_pkg.__all__
def test_resolves_expected_i18n_keys_for_known_tool(self):
# The helper reads four pack keys per render:
# ``tools.<id>.page_title``, ``tools.<id>.page_caption``,
# ``tools.<id>.help_md``, plus shared ``help.button_label`` /
# ``help.missing_body``. We don't invoke the helper (no script
# context) — we verify the keys it would touch resolve to
# non-empty strings in both packs.
from src.i18n import t as _t
tool_id = "02_text_cleaner"
for lang in ("en", "es"):
for suffix in ("page_title", "page_caption", "help_md"):
key = f"tools.{tool_id}.{suffix}"
value = _t(key, lang)
assert value and value != key, (
f"render_tool_header({tool_id!r}) "
f"would render the literal key {key!r} in {lang!r}"
)
for key in ("help.button_label", "help.missing_body"):
value = _t(key, lang)
assert value and value != key, (
f"render_tool_header would render the literal key "
f"{key!r} in {lang!r}"
)
class TestReconcilerAndPdfArePresent:
"""The two newest pages were the most likely to be forgotten in
the registry — pin them explicitly so a regression flagging