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>
179 lines
6.5 KiB
Python
179 lines
6.5 KiB
Python
"""Tests for src.gui.tools_registry — the per-tool manifest.
|
|
|
|
The registry is loaded at import time by the home page sidebar nav,
|
|
the home grid, and the findings panel's "Open Tool" links. A broken
|
|
entry would surface as a sidebar disappearance, a missing card, or a
|
|
``KeyError`` in the findings rendering. We pin the invariants those
|
|
call sites rely on:
|
|
|
|
- Every page_slug points at a file that actually exists.
|
|
- Every tool_id is unique (the analyzer keys findings on it).
|
|
- Every section is one of the declared literals.
|
|
- ``tool_by_id`` round-trips, ``display_name`` falls back gracefully.
|
|
- ``section_label`` resolves localized labels.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import get_args
|
|
|
|
import pytest
|
|
|
|
from src.gui.tools_registry import (
|
|
SECTION_LABELS,
|
|
TOOLS,
|
|
Section,
|
|
Tier,
|
|
Tool,
|
|
display_name,
|
|
section_label,
|
|
tool_by_id,
|
|
tool_description,
|
|
tool_name,
|
|
tools_for_tier,
|
|
tools_in_section,
|
|
)
|
|
|
|
PAGES_DIR = Path(__file__).resolve().parent.parent / "src" / "gui" / "pages"
|
|
|
|
|
|
class TestRegistryInvariants:
|
|
def test_all_tool_ids_are_unique(self):
|
|
ids = [t.tool_id for t in TOOLS]
|
|
assert len(ids) == len(set(ids)), (
|
|
f"duplicate tool_id in TOOLS: {sorted(ids)}"
|
|
)
|
|
|
|
def test_all_page_slugs_point_at_real_files(self):
|
|
for tool in TOOLS:
|
|
page_file = PAGES_DIR / f"{tool.page_slug}.py"
|
|
assert page_file.exists(), (
|
|
f"{tool.tool_id} → {tool.page_slug}.py does not exist"
|
|
)
|
|
|
|
def test_all_sections_are_declared_literals(self):
|
|
valid = set(get_args(Section))
|
|
for tool in TOOLS:
|
|
assert tool.section in valid, (
|
|
f"{tool.tool_id} has unknown section {tool.section!r}; "
|
|
f"valid: {sorted(valid)}"
|
|
)
|
|
|
|
def test_all_tiers_are_declared_literals(self):
|
|
valid = set(get_args(Tier))
|
|
for tool in TOOLS:
|
|
assert tool.tier in valid, (
|
|
f"{tool.tool_id} has unknown tier {tool.tier!r}; "
|
|
f"valid: {sorted(valid)}"
|
|
)
|
|
|
|
def test_every_section_has_a_display_label(self):
|
|
for section in get_args(Section):
|
|
assert section in SECTION_LABELS, (
|
|
f"section {section!r} has no SECTION_LABELS entry"
|
|
)
|
|
|
|
def test_no_orphan_section_labels(self):
|
|
# The other direction: a SECTION_LABELS key that isn't a
|
|
# declared Section literal is dead config.
|
|
valid = set(get_args(Section))
|
|
for key in SECTION_LABELS:
|
|
assert key in valid, (
|
|
f"SECTION_LABELS has stray key {key!r} not in Section"
|
|
)
|
|
|
|
|
|
class TestToolLookups:
|
|
def test_tool_by_id_round_trips_every_entry(self):
|
|
for tool in TOOLS:
|
|
found = tool_by_id(tool.tool_id)
|
|
assert found is tool, (
|
|
f"tool_by_id({tool.tool_id!r}) returned {found!r}"
|
|
)
|
|
|
|
def test_tool_by_id_returns_none_for_unknown(self):
|
|
assert tool_by_id("not_a_real_tool_id") is None
|
|
|
|
def test_display_name_falls_back_to_id(self):
|
|
# Documented behavior: unknown id returns the id itself so the
|
|
# bug is visible in the UI rather than crashing.
|
|
assert display_name("not_a_real_tool_id") == "not_a_real_tool_id"
|
|
|
|
def test_display_name_resolves_known_tool(self):
|
|
# Pick a tool we know ships in every build.
|
|
assert display_name("02_text_cleaner") == "Clean Text"
|
|
|
|
|
|
class TestTierAndSectionFilters:
|
|
def test_tools_for_tier_empty_returns_all(self):
|
|
assert tools_for_tier() == list(TOOLS)
|
|
|
|
def test_tools_for_tier_filters(self):
|
|
# Every tool is tier="core" today, so an explicit core filter
|
|
# should still match the full set. A "pro"-only call should
|
|
# return an empty list.
|
|
assert tools_for_tier("core") == list(TOOLS)
|
|
assert tools_for_tier("pro") == []
|
|
|
|
def test_tools_in_section_preserves_registry_order(self):
|
|
cleaners = tools_in_section("cleaners")
|
|
in_full_order = [t for t in TOOLS if t.section == "cleaners"]
|
|
assert cleaners == in_full_order
|
|
|
|
@pytest.mark.parametrize("section", list(get_args(Section)))
|
|
def test_every_section_has_at_least_one_tool(self, section):
|
|
assert tools_in_section(section), (
|
|
f"section {section!r} has zero tools — sidebar group would be empty"
|
|
)
|
|
|
|
|
|
class TestLocalizedAccessors:
|
|
def test_tool_name_falls_back_to_registry_default(self):
|
|
# An unknown tool id should return the literal id, not crash.
|
|
assert tool_name("not_a_real_tool_id") == "not_a_real_tool_id"
|
|
|
|
def test_tool_name_returns_localized_when_pack_has_key(self):
|
|
# The lang packs ship a "tools.{id}.name" key for every shipped
|
|
# tool. We don't assert the exact translation here (the lang
|
|
# pack parity test pins that); we just check the helper returns
|
|
# something non-empty and not the literal lookup key.
|
|
name = tool_name("02_text_cleaner")
|
|
assert name and name != "tools.02_text_cleaner.name"
|
|
|
|
def test_tool_description_returns_localized_or_fallback(self):
|
|
desc = tool_description("02_text_cleaner")
|
|
assert desc and desc != "tools.02_text_cleaner.description"
|
|
|
|
def test_tool_description_for_unknown_returns_empty(self):
|
|
# Unknown ids return the registry fallback (""), not a
|
|
# lookup-key string. The home grid avoids rendering empty
|
|
# descriptions, so this contract matters.
|
|
assert tool_description("not_a_real_tool_id") == ""
|
|
|
|
@pytest.mark.parametrize("section", list(get_args(Section)))
|
|
def test_section_label_returns_non_empty(self, section):
|
|
label = section_label(section)
|
|
assert label and label != f"nav.section_{section}"
|
|
|
|
|
|
class TestReconcilerAndPdfArePresent:
|
|
"""The two newest pages were the most likely to be forgotten in
|
|
the registry — pin them explicitly so a regression flagging
|
|
"Ready" tools as missing from nav is loud."""
|
|
|
|
def test_pdf_extractor_present(self):
|
|
tool = tool_by_id("10_pdf_extractor")
|
|
assert tool is not None
|
|
assert tool.page_slug == "10_PDF_Extractor"
|
|
assert tool.status == "Ready"
|
|
|
|
def test_reconciler_present(self):
|
|
tool = tool_by_id("11_reconciler")
|
|
assert tool is not None
|
|
assert tool.page_slug == "11_Reconciler"
|
|
assert tool.status == "Ready"
|
|
# The new "analysis" section was introduced with this tool;
|
|
# if the section disappears, the sidebar group goes empty.
|
|
assert tool.section == "analysis"
|