test: fix v3 branding drift, add reconcile CLI + registry coverage

GUI/lang-pack tests were asserting against pre-v3 strings ("Data
Cleaning Mastery", "Maestría en limpieza…") that the brand refresh
replaced with "UNALOGIX DataTools" + "Clean. Normalize. Transform."
Updated assertions to the current copy and switched the findings
panel tests to the redesigned flat-list layout (per-finding "Open
Tool →" buttons instead of per-tool expanders).

New coverage:
- tests/test_cli_reconcile.py (13) — preview/apply, tolerance flags,
  sign inversion, key flags, error paths, Excel input.
- tests/test_tools_registry.py (27) — unique tool_ids, page_slug →
  real file, valid sections/tiers, localized accessor fallbacks,
  explicit pins for PDF Extractor + Reconciler entries.
- tests/test_reconcile.py — one-side-empty, key-pass tagging,
  additional validation cases, input-DataFrame immutability.
- tests/gui/test_smoke.py — PAGE_SLUGS now includes 10_PDF_Extractor
  and 11_Reconciler in both en/es.
- tests/gui/test_workflows.py — TestPdfExtractorWorkflow and
  TestReconcilerWorkflow render checks.

Net: 2317 passed → 2418 passed, 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 19:30:02 +00:00
parent ea99e292d2
commit 6627895a10
9 changed files with 737 additions and 80 deletions

View File

@@ -85,8 +85,8 @@ class TestGatePassesWithTrialLicense:
home_app.run()
text = collected_text(home_app)
# With a valid license, the activation form should NOT be the
# primary content; we should see the home title + tool cards.
assert "Data Cleaning Mastery" in text
# primary content; we should see the home tagline + tool cards.
assert "Clean. Normalize. Transform." in text
assert "Activate DataTools" not in text # form not shown inline
def test_sidebar_shows_active_status(self, trial_license, home_app):
@@ -150,7 +150,7 @@ class TestActivationFormSubmission:
# After activation the page reruns and the activation form
# should be gone — we should see the home page proper.
text = collected_text(home_app)
assert "Data Cleaning Mastery" in text
assert "Clean. Normalize. Transform." in text
def test_trial_button_absent_paid_only(self, no_license_env, home_app):
"""v1.6 dropped the user-facing trial flow — paid licenses only.

View File

@@ -59,7 +59,7 @@ class TestLanguageSwitch:
lang = home_app.session_state["ui_lang"] if "ui_lang" in home_app.session_state else "en"
assert lang == "en"
text = collected_text(home_app)
assert "Data Cleaning Mastery" in text
assert "Clean. Normalize. Transform." in text
def test_selecting_spanish_persists_in_session(self, home_app):
home_app.run()
@@ -72,22 +72,22 @@ class TestLanguageSwitch:
selector = home_app.sidebar.selectbox[0]
selector.select("es").run()
text = collected_text(home_app)
assert "Maestría" in text, (
"after selecting Spanish, the home title should switch to "
f"'🧹 DataTools — Maestría…'; got:\n{text[:300]}"
assert "Limpia. Normaliza. Transforma." in text, (
"after selecting Spanish, the home tagline should switch to "
f"'Limpia. Normaliza. Transforma.'; got:\n{text[:300]}"
)
def test_selecting_back_to_english_reverts(self, home_app):
# Start in Spanish, then flip back.
with_language(home_app, "es")
home_app.run()
assert "Maestría" in collected_text(home_app)
assert "Limpia. Normaliza. Transforma." in collected_text(home_app)
selector = home_app.sidebar.selectbox[0]
selector.select("en").run()
text = collected_text(home_app)
assert "Data Cleaning Mastery" in text
assert "Maestría" not in text
assert "Clean. Normalize. Transform." in text
assert "Limpia. Normaliza. Transforma." not in text
# ---------------------------------------------------------------------------
@@ -96,26 +96,34 @@ class TestLanguageSwitch:
class TestLocalizedChrome:
"""A spot-check on the parts of the chrome that aren't the selector:
the bottom footer caption and the home-page hero text. Other strings
are pinned indirectly by ``TestEveryPageRenders.test_expected_*``."""
the home-page privacy pill (visible to AppTest) and the upload
section heading. The sticky footer caption is rendered via a
component-iframe and isn't visible through ``collected_text``."""
def test_footer_english(self, home_app):
def test_privacy_pill_english(self, home_app):
home_app.run()
text = collected_text(home_app)
assert "Your data never leaves" in text
assert "Runs 100% locally" in text
def test_footer_spanish(self, home_app):
def test_privacy_pill_spanish(self, home_app):
with_language(home_app, "es")
home_app.run()
text = collected_text(home_app)
assert "Tus datos nunca salen" in text
assert "Se ejecuta 100% en local" in text
def test_upload_section_heading_localizes(self, home_app):
with_language(home_app, "es")
home_app.run()
text = collected_text(home_app)
# ``📤 Sube uno o más archivos para empezar`` from the es pack.
assert "Sube uno o más archivos" in text
# The visible "Files" section heading is hard-coded English
# in the redesigned home page; what's still localized is the
# file_uploader widget's label (``upload.uploader_label_multi``).
# AppTest exposes uploaders separately from the text-bearing
# widget collections, so we check the uploader's label
# attribute directly.
labels = [u.label for u in home_app.file_uploader]
assert any("Importa archivos" in lbl for lbl in labels), (
f"Spanish uploader label missing; got: {labels}"
)
# ---------------------------------------------------------------------------

View File

@@ -98,11 +98,19 @@ class TestHeader:
# ---------------------------------------------------------------------------
# Per-tool grouping → one expander per tool id
# Per-finding row → one "Open Tool" button per targeted finding
# ---------------------------------------------------------------------------
#
# The findings panel was redesigned (mockup-v2): it now renders ONE
# severity-sorted flat list rather than per-tool expanders. Each finding
# with a known tool id gets a tertiary button labelled
# ``"{Tool display name} →"`` that switches pages on click. Findings
# with no tool id (file-level CSV-shape warnings, encoding flags, etc.)
# render without a button — the description still shows so the user
# isn't blind to them.
class TestGrouping:
def test_findings_grouped_into_per_tool_expanders(self):
class TestRowsRenderForFindings:
def test_one_button_per_targeted_finding(self):
findings = [
_make_finding(tool="02_text_cleaner", id="whitespace_padding"),
_make_finding(tool="02_text_cleaner", id="nbsp_padding"),
@@ -110,96 +118,96 @@ class TestGrouping:
]
app = _harness(findings)
app.run()
labels = [e.label for e in app.expander]
# Two unique tools → two expanders. Each label carries the
# tool's display name + finding count.
text_cleaner_expanders = [lbl for lbl in labels if "Clean Text" in lbl]
format_expanders = [lbl for lbl in labels if "Standardize Formats" in lbl]
assert len(text_cleaner_expanders) == 1, (
f"expected one Clean Text expander; got: {labels}"
labels = [b.label for b in app.button]
# Each targeted finding gets its own "Open Tool" button — three
# findings → three buttons (two pointing at Clean Text, one at
# Standardize Formats).
clean_text_buttons = [l for l in labels if l == "Clean Text →"]
format_buttons = [l for l in labels if l == "Standardize Formats →"]
assert len(clean_text_buttons) == 2, (
f"expected 2 Clean Text buttons; got: {labels}"
)
assert len(format_expanders) == 1, (
f"expected one Standardize Formats expander; got: {labels}"
assert len(format_buttons) == 1, (
f"expected 1 Standardize Formats button; got: {labels}"
)
def test_tool_names_localize_in_spanish(self):
findings = [_make_finding(tool="02_text_cleaner")]
app = _harness(findings, lang="es")
app.run()
labels = [e.label for e in app.expander]
labels = [b.label for b in app.button]
assert any("Limpiar texto" in lbl for lbl in labels), (
f"Spanish tool name missing; expanders: {labels}"
)
def test_finding_count_in_expander_label(self):
findings = [
_make_finding(tool="02_text_cleaner", id=f"f{i}")
for i in range(3)
]
app = _harness(findings)
app.run()
labels = [e.label for e in app.expander]
# Pack template: "{tool} — {n} finding(s)"
text_cleaner_label = next(l for l in labels if "Clean Text" in l)
assert "3" in text_cleaner_label, (
f"expected count '3' in expander label; got {text_cleaner_label!r}"
f"Spanish tool name missing; buttons: {labels}"
)
# ---------------------------------------------------------------------------
# Open-tool button localizes
# Open-tool button labels — confirm the arrow + name format
# ---------------------------------------------------------------------------
class TestOpenToolButton:
"""Each tool section has an ``st.page_link`` to jump to that tool's
page. AppTest exposes page_links as ``app.button`` entries with
label ``"Open {tool}"`` (English) / ``"Abrir {tool}"`` (Spanish)."""
"""Each finding with a known tool gets a tertiary button labelled
``"{Tool name} →"``. The arrow + spacing is the affordance that
distinguishes the row's primary action from the title text."""
def test_open_tool_label_english(self):
findings = [_make_finding(tool="02_text_cleaner")]
app = _harness(findings)
app.run()
# ``st.page_link`` may show up under ``app.button`` or in the
# raw markdown. We probe both.
text = collected_text(app)
# Pack template: "Open {tool} →"
assert "Open Clean Text" in text
labels = [b.label for b in app.button]
assert "Clean Text →" in labels, (
f"expected 'Clean Text →' button; got: {labels}"
)
def test_open_tool_label_spanish(self):
findings = [_make_finding(tool="02_text_cleaner")]
app = _harness(findings, lang="es")
app.run()
text = collected_text(app)
# Pack template: "Abrir {tool} →"
assert "Abrir Limpiar texto" in text
labels = [b.label for b in app.button]
assert "Limpiar texto →" in labels, (
f"expected 'Limpiar texto →' button; got: {labels}"
)
# ---------------------------------------------------------------------------
# Untargeted findings (file-level) go in the "Other" expander
# Untargeted findings (file-level) render without an action button
# ---------------------------------------------------------------------------
class TestUntargetedFindings:
def test_untargeted_goes_to_other_expander_en(self):
"""A finding with ``tool=""`` (e.g., CSV BOM stripped at read time)
is file-level — no tool page to jump to — and the redesigned panel
renders the description without a button. We assert that the row
contributes nothing to ``app.button`` while still appearing in the
rendered markdown."""
def test_untargeted_renders_no_button_en(self):
findings = [
_make_finding(tool="", id="csv_bom_stripped"),
_make_finding(tool="", id="csv_bom_stripped", description="BOM stripped"),
_make_finding(tool="02_text_cleaner", id="nbsp_padding"),
]
app = _harness(findings)
app.run()
labels = [e.label for e in app.expander]
# Pack template: "Other / file-level — {n} finding(s)"
assert any("Other / file-level" in lbl for lbl in labels), (
f"untargeted expander missing; got: {labels}"
labels = [b.label for b in app.button]
# Only the targeted finding contributed a button.
assert "Clean Text →" in labels
# The BOM finding's description must still be visible somewhere.
all_md = "\n".join(
m.body for m in app.markdown if hasattr(m, "body")
)
assert "BOM stripped" in all_md, (
"untargeted finding's description should still render"
)
def test_untargeted_label_spanish(self):
findings = [_make_finding(tool="", id="csv_bom_stripped")]
def test_untargeted_renders_no_button_es(self):
findings = [_make_finding(
tool="", id="csv_bom_stripped", description="BOM eliminado",
)]
app = _harness(findings, lang="es")
app.run()
labels = [e.label for e in app.expander]
# Spanish pack: "Otros / a nivel de archivo — {n} hallazgo(s)"
assert any("Otros / a nivel de archivo" in lbl for lbl in labels), (
f"Spanish 'Other' expander missing; got: {labels}"
labels = [b.label for b in app.button]
# No tool id → no tool-jump button at all.
assert not any("" in lbl for lbl in labels), (
f"untargeted finding should not render a tool button; got: {labels}"
)

View File

@@ -34,6 +34,8 @@ PAGE_SLUGS = [
"7_Multi_File_Merger",
"8_Validator_Reporter",
"9_Pipeline_Runner",
"10_PDF_Extractor",
"11_Reconciler",
"99_Close",
]
@@ -61,17 +63,28 @@ 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"},
"99_Close": {"en": "Shutting down", "es": "Cerrando"},
}
class TestHomePageRenders:
"""The home page is the only one with full EN/ES coverage in v1.6.
Pin it independently so its translation is non-regressable."""
"""Pin the home hero in both languages.
Since the v3 brand refresh the title is the literal wordmark
("UNALOGIX DataTools") in both packs; the localized tagline is
what shifts between en and es. We assert against the tagline
string, which lives in ``home.caption`` of each pack.
"""
@pytest.mark.parametrize("lang,expected", [
("en", "DataTools — Data Cleaning Mastery"),
("es", "DataTools — Maestría en limpieza de datos"),
("en", "Clean. Normalize. Transform."),
("es", "Limpia. Normaliza. Transforma."),
])
def test_home_renders_in_language(self, home_app, lang, expected):
with_language(home_app, lang)
@@ -81,11 +94,15 @@ class TestHomePageRenders:
)
assert expected in collected_text(home_app)
def test_home_renders_footer_in_es(self, home_app):
def test_home_renders_privacy_pill_in_es(self, home_app):
# The footer caption is rendered via a component-iframe so
# ``collected_text`` can't see it. The privacy pill on the
# home header IS visible to AppTest and carries the same
# locality story, so we pin that instead.
with_language(home_app, "es")
home_app.run()
text = collected_text(home_app)
assert "Tus datos nunca salen" in text or "Se ejecuta localmente" in text
assert "Se ejecuta 100% en local" in text
class TestEveryPageRenders:
"""Parametrize over (page, language). Failure tells you exactly which

View File

@@ -152,6 +152,48 @@ class TestPipelineRunnerWorkflow:
# ---------------------------------------------------------------------------
# PDF to CSV — file-uploader-driven so we can't fully exercise the
# scan flow through AppTest. Pin the initial render (which carries the
# dep-status banner when deps are missing) so a future regression in
# the dep guard shows up here.
# ---------------------------------------------------------------------------
class TestPdfExtractorWorkflow:
def test_page_renders_without_upload(self, app_factory):
app = app_factory("10_PDF_Extractor")
app.run()
assert not app.exception
text = collected_text(app)
assert "PDF to CSV" in text
# ---------------------------------------------------------------------------
# Reconcile Two Files — early-exits at ``st.stop()`` without both
# uploads. Pin both the no-upload state and the title.
# ---------------------------------------------------------------------------
class TestReconcilerWorkflow:
def test_page_renders_without_uploads(self, app_factory):
app = app_factory("11_Reconciler")
app.run()
assert not app.exception
text = collected_text(app)
assert "Reconcile" in text
def test_prompts_for_both_uploads_when_empty(self, app_factory):
# ``st.info("Upload both files to continue.")`` fires when
# either side is missing; that text is the contract we test
# against — if the prompt disappears the user has no idea
# what to do next.
app = app_factory("11_Reconciler")
app.run()
info_messages = [i.body for i in app.info if hasattr(i, "body")]
assert any("Upload both files" in m for m in info_messages), (
f"missing 'Upload both files' prompt; got: {info_messages}"
)
# ---------------------------------------------------------------------------
# Coming-Soon pages still render (just a stub) — pinned so we know if a
# Coming-Soon goes from "stub renders" to "import error".