Files
datatools-dev/tests/test_tools_registry.py
Michael 6627895a10 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>
2026-05-22 19:30:02 +00:00

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"