Fix Zotero table models for Zotero 9:
- Remove stale Annotations/Highlights/Transaction* models
- Add ItemAnnotations, RetractedItems, DeletedCollections,
DeletedSearches, DbDebug1
- Fix ItemAttachments, Libraries, Users column mismatches
New test files covering all major modules:
- cli/{bib,prisma,rec,zot,mail,run} deep exercising tests
- mail/{droplet,postmark,resend,cloudflare} lifecycle tests
- bib/{iom,oig,pincite,sync,regulations_gov,email_ingest,format,store}
- prisma/{vpn,fetch,export,llm,screen,eligibility,extract,project,ingest,flow}
- aco/lake/{unity,quality,deploy} + api/aco coverage gaps
- zot/{ops,db,extract,duck} + rec/{report,engine,base,pricers}
- pfs/{pipe,rules,eq,files}
Add pytest-xdist for parallel test execution.
Tracks #353
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""Tests for rec.engine.reconcile with a hand-rolled fake pricer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from rec.engine import reconcile, reconcile_all_years
|
|
|
|
|
|
class TestReconcile:
|
|
def test_counts(self, fake_pricer) -> None:
|
|
r = reconcile(fake_pricer, con=None, year=2025)
|
|
# FakePricer:
|
|
# ground_truth: A001 (10.00), A002 (20.00), A003 (30.00)
|
|
# calculated: A001 (10.00), A002 (20.01), A004 (40.00)
|
|
# Matched: A001, A002 → 2
|
|
# gt_only: A003 → 1
|
|
# calc_only: A004 → 1
|
|
assert r.ground_truth_rows == 3
|
|
assert r.calculated_rows == 3
|
|
assert r.matched_rows == 2
|
|
assert r.ground_truth_only == 1
|
|
assert r.calculated_only == 1
|
|
|
|
def test_exact_vs_near(self, fake_pricer) -> None:
|
|
r = reconcile(fake_pricer, con=None, year=2025)
|
|
# A001 is exact; A002 is 1¢ off → near but not exact at tolerance=0.
|
|
assert r.exact_matches == 1
|
|
assert r.near_matches == 2
|
|
|
|
def test_tolerance_promotes_near_to_exact(self, fake_pricer) -> None:
|
|
r = reconcile(fake_pricer, con=None, year=2025, tolerance_cents=1)
|
|
assert r.exact_matches == 2
|
|
assert r.tolerance_cents == 1
|
|
|
|
def test_delta_table_shape(self, fake_pricer) -> None:
|
|
r = reconcile(fake_pricer, con=None, year=2025)
|
|
# All 4 distinct hcpcs appear in the outer join
|
|
assert r.deltas.height == 4
|
|
# Columns: join_keys + per-col suffixes + abs_max_delta + flags
|
|
cols = set(r.deltas.columns)
|
|
for k in fake_pricer.join_keys:
|
|
assert k in cols
|
|
assert "fee_gt" in cols
|
|
assert "fee_calc" in cols
|
|
assert "fee_delta" in cols
|
|
assert "abs_max_delta" in cols
|
|
assert "is_exact" in cols
|
|
assert "is_near" in cols
|
|
|
|
def test_delta_sorted_by_abs_max_desc(self, fake_pricer) -> None:
|
|
r = reconcile(fake_pricer, con=None, year=2025)
|
|
# The top rows should be the ones with largest abs_max_delta
|
|
# (A004 calc-only and A003 gt-only both have null deltas — with
|
|
# nulls_last they come last). Of the real deltas, A002 has 0.01
|
|
# and A001 has 0.00.
|
|
non_null = r.deltas.drop_nulls("abs_max_delta")
|
|
assert non_null.height == 2
|
|
# A002 (0.01) should be first
|
|
first = non_null.row(0, named=True)
|
|
assert first["hcpcs"] == "A002"
|
|
assert abs(first["abs_max_delta"] - 0.01) < 1e-9
|
|
|
|
def test_rejects_non_pricer(self) -> None:
|
|
class NotAPricer:
|
|
pass
|
|
|
|
with pytest.raises(TypeError, match="does not conform"):
|
|
reconcile(NotAPricer(), con=None, year=2025) # type: ignore[arg-type]
|
|
|
|
|
|
class TestReconcileAllYears:
|
|
def test_dispatch(self, fake_pricer) -> None:
|
|
results = reconcile_all_years(fake_pricer, con=None)
|
|
assert set(results.keys()) == {2025}
|
|
assert results[2025].matched_rows == 2
|
|
|
|
|
|
# ── Gap coverage — missed lines ────────────────────────────────
|
|
|
|
|
|
class TestCheckColumnsWarning:
|
|
"""Line 185: _check_columns appends warning when columns are missing."""
|
|
|
|
def test_missing_columns_warning(self) -> None:
|
|
import polars as pl
|
|
|
|
from rec.engine import _check_columns
|
|
|
|
df = pl.DataFrame({"a": [1]})
|
|
warnings: list[str] = []
|
|
_check_columns(df, ["a", "b", "c"], "test_side", warnings)
|
|
assert len(warnings) == 1
|
|
assert "missing required columns" in warnings[0]
|
|
assert "test_side" in warnings[0]
|
|
|
|
|
|
class TestCheckUniqueKeysEdges:
|
|
"""Lines 197, 200, 203: _check_unique_keys with edge cases."""
|
|
|
|
def test_empty_df(self) -> None:
|
|
import polars as pl
|
|
|
|
from rec.engine import _check_unique_keys
|
|
|
|
df = pl.DataFrame({"k": []}).cast({"k": pl.Utf8})
|
|
warnings: list[str] = []
|
|
_check_unique_keys(df, ["k"], "test_side", warnings)
|
|
assert len(warnings) == 0 # empty → early return
|
|
|
|
def test_no_matching_keys(self) -> None:
|
|
import polars as pl
|
|
|
|
from rec.engine import _check_unique_keys
|
|
|
|
df = pl.DataFrame({"a": [1]})
|
|
warnings: list[str] = []
|
|
_check_unique_keys(df, ["nonexistent"], "test_side", warnings)
|
|
assert len(warnings) == 0 # no key_cols → early return
|
|
|
|
def test_duplicate_keys_warning(self) -> None:
|
|
import polars as pl
|
|
|
|
from rec.engine import _check_unique_keys
|
|
|
|
df = pl.DataFrame({"k": ["A", "A", "B"]})
|
|
warnings: list[str] = []
|
|
_check_unique_keys(df, ["k"], "test_side", warnings)
|
|
assert len(warnings) == 1
|
|
assert "duplicate rows" in warnings[0]
|