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:
@@ -315,3 +315,117 @@ class TestResultShape:
|
||||
assert result.matched.empty
|
||||
assert result.unmatched_left.empty
|
||||
assert result.unmatched_right.empty
|
||||
|
||||
def test_one_side_empty_keeps_other_unmatched(self):
|
||||
# A reconcile against an empty ledger should surface every
|
||||
# left row as unmatched, not crash. Mirror case for the
|
||||
# other side.
|
||||
left = _bank([
|
||||
("2026-01-05", 100.00, "ACME"),
|
||||
("2026-01-06", 250.00, "WIDGET"),
|
||||
])
|
||||
right = _ledger([])
|
||||
result = reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="amt",
|
||||
left_date="date", right_date="posted",
|
||||
))
|
||||
assert result.stats["matched"] == 0
|
||||
assert result.stats["unmatched_left"] == 2
|
||||
assert result.stats["unmatched_right"] == 0
|
||||
|
||||
def test_match_pass_tagged_for_key_pass(self):
|
||||
# Pass name on each matched row tells the user *why* the engine
|
||||
# accepted the pair — verify the "key" label propagates.
|
||||
left = pd.DataFrame([
|
||||
{"date": "2026-01-05", "amount": 100.00, "check_no": "1042"},
|
||||
])
|
||||
right = pd.DataFrame([
|
||||
{"posted": "2099-12-31", "amt": 100.00, "ref": "1042"},
|
||||
])
|
||||
result = reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="amt",
|
||||
left_date="date", right_date="posted",
|
||||
left_keys=["check_no"], right_keys=["ref"],
|
||||
))
|
||||
assert result.stats["matched"] == 1
|
||||
assert result.matched.iloc[0]["match_pass"] == "key"
|
||||
|
||||
|
||||
class TestAdditionalValidation:
|
||||
"""Boundary cases for ``_validate_options`` not pinned elsewhere."""
|
||||
|
||||
def test_unknown_left_amount_column_raises(self):
|
||||
left = pd.DataFrame([{"date": "2026-01-05", "amount": 1.0}])
|
||||
right = pd.DataFrame([{"posted": "2026-01-05", "amt": 1.0}])
|
||||
with pytest.raises(ValueError, match="not in left DataFrame"):
|
||||
reconcile(left, right, ReconcileOptions(
|
||||
left_amount="NOT_A_COLUMN", right_amount="amt",
|
||||
))
|
||||
|
||||
def test_unknown_right_amount_column_raises(self):
|
||||
left = pd.DataFrame([{"date": "2026-01-05", "amount": 1.0}])
|
||||
right = pd.DataFrame([{"posted": "2026-01-05", "amt": 1.0}])
|
||||
with pytest.raises(ValueError, match="not in right DataFrame"):
|
||||
reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="NOT_A_COLUMN",
|
||||
))
|
||||
|
||||
def test_unknown_left_key_column_raises(self):
|
||||
left = pd.DataFrame([{"date": "2026-01-05", "amount": 1.0}])
|
||||
right = pd.DataFrame([{"posted": "2026-01-05", "amt": 1.0}])
|
||||
with pytest.raises(ValueError, match="left key column"):
|
||||
reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="amt",
|
||||
left_keys=["nope"], right_keys=["nope"],
|
||||
))
|
||||
|
||||
def test_negative_date_tolerance_rejected(self):
|
||||
left = pd.DataFrame([{"date": "2026-01-05", "amount": 1.0}])
|
||||
right = pd.DataFrame([{"posted": "2026-01-05", "amt": 1.0}])
|
||||
with pytest.raises(ValueError, match="date_tolerance_days"):
|
||||
reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="amt",
|
||||
left_date="date", right_date="posted",
|
||||
date_tolerance_days=-1,
|
||||
))
|
||||
|
||||
def test_desc_min_score_out_of_range_rejected(self):
|
||||
left = pd.DataFrame([{"date": "2026-01-05", "amount": 1.0}])
|
||||
right = pd.DataFrame([{"posted": "2026-01-05", "amt": 1.0}])
|
||||
with pytest.raises(ValueError, match="desc_min_score"):
|
||||
reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="amt",
|
||||
desc_min_score=150,
|
||||
))
|
||||
|
||||
|
||||
class TestImmutability:
|
||||
"""The engine must NOT mutate the caller's DataFrames — callers
|
||||
rely on holding onto their input frames after the call (the GUI
|
||||
Reconciler page re-renders previews from them)."""
|
||||
|
||||
def test_left_df_columns_unchanged(self):
|
||||
left = _bank([("2026-01-05", 100.00, "ACME")])
|
||||
right = _ledger([("2026-01-05", 100.00, "Acme Inc")])
|
||||
before_cols = list(left.columns)
|
||||
before_id = id(left)
|
||||
reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="amt",
|
||||
left_date="date", right_date="posted",
|
||||
))
|
||||
assert list(left.columns) == before_cols
|
||||
# And the caller's DataFrame object identity is preserved.
|
||||
assert id(left) == before_id
|
||||
|
||||
def test_amounts_preserved_when_invert_right_sign_set(self):
|
||||
# Even with --invert-right-sign, the original right amounts
|
||||
# must come back unchanged in the result.
|
||||
left = _bank([("2026-01-05", 100.00, "A")])
|
||||
right = _ledger([("2026-01-05", -100.00, "X")])
|
||||
original_right_amts = right["amt"].tolist()
|
||||
reconcile(left, right, ReconcileOptions(
|
||||
left_amount="amount", right_amount="amt",
|
||||
left_date="date", right_date="posted",
|
||||
invert_right_sign=True,
|
||||
))
|
||||
assert right["amt"].tolist() == original_right_amts
|
||||
|
||||
Reference in New Issue
Block a user