AI-PolicyTrace / tests /test_arbiter.py
teja141290's picture
Deploy PolicyTrace Hugging Face Space
be54038
"""
tests/test_arbiter.py — Unit tests for PolicyArbiter.
These tests exercise the merge logic in isolation using pure fixture data,
with no LLM calls or file I/O. Run with:
pytest tests/test_arbiter.py -v
(From project root with the virtual-env activated.)
"""
from __future__ import annotations
import sys
from pathlib import Path
# Allow importing from src/ without installing the package
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import pytest
from arbiter import PolicyArbiter
from schema import (
AdditionalRiskData,
ConflictEntry,
CoverAndExcesses,
Driver,
ExcessBreakdown,
FinancialSummary,
NoClaimsDiscount,
OptionalExtras,
PeriodOfCover,
PolicyHeader,
UKMotorGoldenRecord,
VehicleDetails,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _make_schedule(
policy_number: str = "POL-001",
insurer: str = "TestInsurer Ltd",
cover_type: str = "Comprehensive",
ncb_years: int = 3,
class_of_use: str | None = None,
drivers: list[dict] | None = None,
excess_compulsory: float = 250.0,
excess_voluntary: float = 150.0,
premium: float = 600.0,
vrm: str = "AB12 XYZ",
) -> UKMotorGoldenRecord:
drv_list = [
Driver(**d) for d in (drivers or [{"name": "ALICE SMITH", "is_main_driver": True}])
]
return UKMotorGoldenRecord(
policy_header=PolicyHeader(policy_number=policy_number, insurer=insurer),
vehicle_details=VehicleDetails(vrm=vrm, make="Toyota", model="Corolla"),
driver_details=drv_list,
cover_and_excesses=CoverAndExcesses(
cover_type=cover_type,
class_of_use=class_of_use,
no_claims_discount=NoClaimsDiscount(years=ncb_years, protected=False),
excess_breakdown=ExcessBreakdown(
standard_compulsory=excess_compulsory,
voluntary=excess_voluntary,
total_accidental_damage=excess_compulsory + excess_voluntary,
),
),
financial_summary=FinancialSummary(
total_annual_premium=premium,
optional_extras=OptionalExtras(),
),
additional_risk_data=AdditionalRiskData(home_ownership="Owned"),
)
def _make_certificate(
policy_number: str = "POL-001",
class_of_use: str = "Social, Domestic and Pleasure",
driving_other_cars: bool = False,
drivers: list[dict] | None = None,
insurer: str | None = None,
) -> UKMotorGoldenRecord:
drv_list = [
Driver(**d) for d in (drivers or [{"name": "ALICE SMITH", "is_main_driver": True}])
]
return UKMotorGoldenRecord(
policy_header=PolicyHeader(
policy_number=policy_number,
insurer=insurer,
),
driver_details=drv_list,
cover_and_excesses=CoverAndExcesses(
class_of_use=class_of_use,
driving_other_cars=driving_other_cars,
),
)
# ---------------------------------------------------------------------------
# Basic merge tests
# ---------------------------------------------------------------------------
class TestBasicMerge:
def test_returns_tuple_with_conflicts_list(self):
arbiter = PolicyArbiter()
sched = _make_schedule()
cert = _make_certificate()
result = arbiter.merge_records(sched, "sched.pdf", cert, "cert.pdf")
assert isinstance(result, tuple)
golden, conflicts = result
assert isinstance(conflicts, list)
def test_vehicle_details_from_schedule(self):
arbiter = PolicyArbiter()
sched = _make_schedule(vrm="AB12 XYZ")
cert = _make_certificate()
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.vehicle_details is not None
assert golden.vehicle_details.vrm == "AB12 XYZ"
def test_class_of_use_from_certificate(self):
arbiter = PolicyArbiter()
sched = _make_schedule(class_of_use="Social") # schedule has one
cert = _make_certificate(class_of_use="Social, Domestic and Pleasure") # cert is master
golden, conflicts = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.cover_and_excesses.class_of_use == "Social, Domestic and Pleasure"
def test_cover_type_from_schedule(self):
arbiter = PolicyArbiter()
sched = _make_schedule(cover_type="Comprehensive")
cert = _make_certificate()
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.cover_and_excesses.cover_type == "Comprehensive"
def test_financial_summary_from_schedule(self):
arbiter = PolicyArbiter()
sched = _make_schedule(premium=750.0)
cert = _make_certificate()
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.financial_summary.total_annual_premium == 750.0
def test_additional_risk_data_from_schedule(self):
arbiter = PolicyArbiter()
sched = _make_schedule()
cert = _make_certificate()
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.additional_risk_data.home_ownership == "Owned"
# ---------------------------------------------------------------------------
# One-sided merge (missing Schedule or Certificate)
# ---------------------------------------------------------------------------
class TestOneSidedMerge:
def test_empty_schedule_uses_certificate_drivers(self):
arbiter = PolicyArbiter()
sched = UKMotorGoldenRecord() # empty
cert = _make_certificate(
drivers=[{"name": "BOB JONES", "is_main_driver": True}]
)
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert len(golden.driver_details) == 1
assert golden.driver_details[0].name == "BOB JONES"
def test_empty_certificate_still_merges(self):
arbiter = PolicyArbiter()
sched = _make_schedule()
cert = UKMotorGoldenRecord() # empty
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.vehicle_details is not None
assert golden.cover_and_excesses is not None
def test_policy_number_fallback_to_certificate(self):
arbiter = PolicyArbiter()
sched = UKMotorGoldenRecord(policy_header=PolicyHeader(policy_number=None))
cert = _make_certificate(policy_number="CERT-999")
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.policy_header.policy_number == "CERT-999"
# ---------------------------------------------------------------------------
# Conflict detection
# ---------------------------------------------------------------------------
class TestConflictDetection:
def test_no_conflicts_when_values_match(self):
arbiter = PolicyArbiter()
sched = _make_schedule(policy_number="POL-001", insurer="Insurer A")
cert = _make_certificate(policy_number="POL-001")
_, conflicts = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
policy_number_conflicts = [c for c in conflicts if c.field == "policy_header.policy_number"]
assert policy_number_conflicts == []
def test_conflict_logged_for_differing_policy_numbers(self):
arbiter = PolicyArbiter()
sched = _make_schedule(policy_number="POL-001")
cert = _make_certificate(policy_number="POL-002")
golden, conflicts = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
conflict_fields = [c.field for c in conflicts]
assert "policy_header.policy_number" in conflict_fields
# Schedule wins
assert golden.policy_header.policy_number == "POL-001"
def test_conflict_entry_has_both_values(self):
arbiter = PolicyArbiter()
sched = _make_schedule(policy_number="SCHED-100")
cert = _make_certificate(policy_number="CERT-200")
_, conflicts = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
c = next(x for x in conflicts if x.field == "policy_header.policy_number")
assert c.schedule_value == "SCHED-100"
assert c.certificate_value == "CERT-200"
assert c.winner == "schedule"
def test_class_of_use_conflict_certificate_wins(self):
arbiter = PolicyArbiter()
sched = _make_schedule(class_of_use="Social Only")
cert = _make_certificate(class_of_use="Social, Domestic and Pleasure")
golden, conflicts = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
c = next((x for x in conflicts if x.field == "cover_and_excesses.class_of_use"), None)
assert c is not None
assert c.winner == "certificate"
assert golden.cover_and_excesses.class_of_use == "Social, Domestic and Pleasure"
# ---------------------------------------------------------------------------
# Driver merging
# ---------------------------------------------------------------------------
class TestDriverMerge:
def test_exact_name_match_enriches_driver(self):
arbiter = PolicyArbiter()
sched = _make_schedule(
drivers=[{"name": "ALICE SMITH", "is_main_driver": True, "dob": None, "relationship": None}]
)
cert = _make_certificate(
drivers=[{"name": "ALICE SMITH", "is_main_driver": True, "relationship": "Proposer"}]
)
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.driver_details[0].relationship == "Proposer"
def test_fuzzy_name_match_merges(self):
"""Names with minor differences (e.g. missing middle initial) should still match."""
arbiter = PolicyArbiter()
sched = _make_schedule(
drivers=[{"name": "ALICE J SMITH", "is_main_driver": True}]
)
cert = _make_certificate(
drivers=[{"name": "ALICE SMITH", "is_main_driver": True, "relationship": "Proposer"}]
)
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
assert golden.driver_details[0].relationship == "Proposer"
def test_unmatched_driver_has_no_cert_enrichment(self):
"""A driver with a completely different name gets no cert data."""
arbiter = PolicyArbiter()
sched = _make_schedule(
drivers=[{"name": "ALICE SMITH", "is_main_driver": True}]
)
cert = _make_certificate(
drivers=[{"name": "BOB JONES", "is_main_driver": True, "relationship": "Spouse"}]
)
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
alice = golden.driver_details[0]
assert alice.name == "ALICE SMITH"
assert alice.relationship is None # no cert match, so no enrichment
# ---------------------------------------------------------------------------
# field_citations merging
# ---------------------------------------------------------------------------
class TestFieldCitationsMerge:
def test_schedule_citations_win_on_conflict(self):
arbiter = PolicyArbiter()
sched = _make_schedule()
cert = _make_certificate()
sched.field_citations = {
"vehicle_details.vrm": "AB12 XYZ",
"policy_header.policy_number": "POL-001 from schedule",
}
cert.field_citations = {
"policy_header.policy_number": "POL-001 from cert",
"cover_and_excesses.class_of_use": "Social, Domestic and Pleasure",
}
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
fc = golden.field_citations or {}
# Schedule wins the shared key
assert fc.get("policy_header.policy_number") == "POL-001 from schedule"
# Cert-only key survives
assert fc.get("cover_and_excesses.class_of_use") == "Social, Domestic and Pleasure"
# Schedule-only key survives
assert fc.get("vehicle_details.vrm") == "AB12 XYZ"
def test_empty_citations_produce_none(self):
arbiter = PolicyArbiter()
sched = _make_schedule()
cert = _make_certificate()
golden, _ = arbiter.merge_records(sched, "s.pdf", cert, "c.pdf")
# Neither side has citations → merged record has None
assert golden.field_citations is None