← back to Unclaimed Property Platform

tests/test_ingest_and_match.py

113 lines

"""End-to-end smoke test — proves the prototype runs at $0 on synthetic data (stdlib only).

Run:  python -m tests.test_ingest_and_match

Asserts:
  1. Ingestion loads the synthetic feed and rejects the empty-owner row.
  2. Ingestion is IDEMPOTENT — re-running the same file is a no-op ('duplicate'), and
     record count does not grow.
  3. Masking works — no raw owner name leaks into the search projection.
  4. Entity matching links spelling variants (person + business) while keeping distinct
     people distinct.
"""
from __future__ import annotations

import sys
from pathlib import Path

# make repo root importable when run as a script
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from services.common.sqlite_repo import FileObjectStore, SqliteRepository
from services.ingestion.ingest import FeedDefinition, ingest_authorized_feed
from services.matching.entity_match import MatchInput, entity_match

REPO_ROOT = Path(__file__).resolve().parents[1]
SAMPLE = REPO_ROOT / "data" / "sample" / "sample_state_feed.csv"


def _passed(msg: str) -> None:
    print(f"  ✓ {msg}")


def main() -> int:
    import tempfile

    tmp = Path(tempfile.mkdtemp(prefix="upp-proto-"))
    store = FileObjectStore(tmp / "objstore")
    # stage the synthetic feed into the object store under a relative uri
    (tmp / "objstore").mkdir(parents=True, exist_ok=True)
    store.write_bytes("incoming/sample.csv", SAMPLE.read_bytes())
    repo = SqliteRepository(str(tmp / "proto.db"))

    feed = FeedDefinition(jurisdiction="SAMPLE", source_uri="incoming/sample.csv")

    print("1) Ingestion + rejection")
    r1 = ingest_authorized_feed(feed, store, repo)
    assert r1["status"] in ("completed", "completed_with_errors"), r1
    # 8 rows, 1 has empty owner_name -> rejected
    assert r1["accepted"] == 7, f"expected 7 accepted, got {r1['accepted']}"
    assert r1["rejected"] == 1, f"expected 1 rejected, got {r1['rejected']}"
    _passed(f"accepted={r1['accepted']} rejected={r1['rejected']} (empty-owner row rejected)")

    print("2) Idempotency")
    count_after_first = repo.count_properties("SAMPLE")
    r2 = ingest_authorized_feed(feed, store, repo)
    assert r2["status"] == "duplicate", r2
    assert repo.count_properties("SAMPLE") == count_after_first, "re-ingest changed count!"
    _passed(f"re-ingest -> 'duplicate', property count stable at {count_after_first}")

    print("3) Masking (no raw name leaks into search projection)")
    hits = repo.masked_search("Catherine")
    assert hits, "expected at least one masked hit for 'Catherine'"
    leaked = [h for h in hits if "CATHERINE" in (h["owner_name_masked"] or "").upper()]
    assert not leaked, f"raw name leaked into masked projection: {leaked}"
    _passed(f"masked hit sample: {hits[0]['owner_name_masked']} | {hits[0]['amount_band']}")

    print("4) Entity matching")
    person = entity_match(
        MatchInput("Catherine O'Neil", city="Springfield", region="SAMPLE",
                   postal_code="00001", address="100 Test St"),
        MatchInput("Kathryn ONeill", city="Springfield", region="SAMPLE",
                   postal_code="00001", address="100 Test Street"),
    )
    assert person.disposition in ("manual_review", "likely_duplicate_candidate"), person
    _passed(f"person variants -> {person.disposition} (p={person.probability})")

    business = entity_match(
        MatchInput("Acme Widgets Inc", city="Rivertown", region="SAMPLE",
                   postal_code="00002", address="300 Nowhere Blvd", is_business=True),
        MatchInput("Acme Widgets Incorporated", city="Rivertown", region="SAMPLE",
                   postal_code="00002", address="300 Nowhere Blvd", is_business=True),
    )
    assert business.disposition in ("manual_review", "likely_duplicate_candidate"), business
    _passed(f"business variants -> {business.disposition} (p={business.probability})")

    distinct = entity_match(
        MatchInput("Catherine O'Neil", city="Springfield", region="SAMPLE", postal_code="00001"),
        MatchInput("Jonathan Doe", city="Springfield", region="SAMPLE", postal_code="00001"),
    )
    assert distinct.disposition == "distinct", distinct
    _passed(f"different people -> {distinct.disposition} (p={distinct.probability})")

    # person vs business must never link
    cross = entity_match(
        MatchInput("Acme Widgets Inc", is_business=True),
        MatchInput("Acme Widgets Inc", is_business=False),
    )
    assert cross.disposition == "distinct", cross
    _passed("person/business type conflict -> distinct (never linked)")

    # Robust contract (survives weight re-tuning): a same-owner variant must ALWAYS
    # out-score two clearly-different people at the same address.
    assert person.probability > distinct.probability, (person, distinct)
    assert business.probability > distinct.probability, (business, distinct)
    _passed(f"ordering: variant p={person.probability} > distinct p={distinct.probability}")

    print("\nALL SMOKE-TEST ASSERTIONS PASSED ✅")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())