| """Sanity tests for the schema layer — verifies validators, defaults, and the registry. |
| |
| These are pure-Python tests (no LLM calls) so they run in milliseconds. |
| """ |
| from __future__ import annotations |
|
|
| from datetime import date |
|
|
| import pytest |
| from pydantic import ValidationError |
|
|
| from src.schemas import ( |
| ExtractionResult, |
| FieldConfidence, |
| Invoice, |
| LineItem, |
| Party, |
| Receipt, |
| ReceiptLineItem, |
| get_schema, |
| list_doc_types, |
| ) |
|
|
| |
|
|
|
|
| def test_invoice_minimum_required_fields(): |
| """An invoice needs vendor, invoice_number, total, currency — nothing else.""" |
| inv = Invoice( |
| invoice_number="INV-001", |
| vendor=Party(name="Acme Corp"), |
| total=100.00, |
| currency="USD", |
| ) |
| assert inv.invoice_number == "INV-001" |
| assert inv.total == 100.00 |
| assert inv.currency == "USD" |
| assert inv.line_items == [] |
|
|
|
|
| def test_invoice_rounds_money_to_2_decimals(): |
| inv = Invoice( |
| invoice_number="INV-002", |
| vendor=Party(name="Acme"), |
| total=99.999, |
| subtotal=87.4444, |
| currency="usd", |
| ) |
| assert inv.total == 100.00 |
| assert inv.subtotal == 87.44 |
| assert inv.currency == "USD" |
|
|
|
|
| def test_invoice_rejects_negative_total(): |
| with pytest.raises(ValidationError): |
| Invoice( |
| invoice_number="INV-003", |
| vendor=Party(name="Acme"), |
| total=-10.00, |
| currency="USD", |
| ) |
|
|
|
|
| def test_invoice_with_line_items(): |
| inv = Invoice( |
| invoice_number="INV-004", |
| vendor=Party(name="Widgets Inc"), |
| invoice_date=date(2026, 6, 15), |
| line_items=[ |
| LineItem(description="Widget", quantity=3, unit_price=9.99, total=29.97), |
| LineItem(description="Sprocket", quantity=1, unit_price=15.00, total=15.00), |
| ], |
| subtotal=44.97, |
| tax=3.60, |
| total=48.57, |
| currency="USD", |
| ) |
| assert len(inv.line_items) == 2 |
| assert inv.line_items[0].total == 29.97 |
|
|
|
|
| |
|
|
|
|
| def test_receipt_minimum_required_fields(): |
| r = Receipt(merchant="Corner Coffee", total=4.50, currency="USD") |
| assert r.merchant == "Corner Coffee" |
| assert r.total == 4.50 |
|
|
|
|
| def test_receipt_rejects_negative_total(): |
| with pytest.raises(ValidationError): |
| Receipt(merchant="X", total=-1.00, currency="USD") |
|
|
|
|
| def test_receipt_with_items_and_tip(): |
| r = Receipt( |
| merchant="Diner", |
| transaction_date=date(2026, 6, 20), |
| line_items=[ |
| ReceiptLineItem(description="Burger", quantity=1, total=12.00), |
| ReceiptLineItem(description="Fries", quantity=1, total=4.00), |
| ], |
| subtotal=16.00, |
| tax=1.28, |
| tip=3.00, |
| total=20.28, |
| currency="USD", |
| ) |
| assert r.tip == 3.00 |
| assert len(r.line_items) == 2 |
|
|
|
|
| |
|
|
|
|
| def test_registry_lookup(): |
| assert get_schema("invoice") is Invoice |
| assert get_schema("receipt") is Receipt |
| assert get_schema("INVOICE") is Invoice |
|
|
|
|
| def test_registry_unknown_raises(): |
| with pytest.raises(KeyError): |
| get_schema("bogus") |
|
|
|
|
| def test_list_doc_types(): |
| types = list_doc_types() |
| assert "invoice" in types |
| assert "receipt" in types |
|
|
|
|
| |
|
|
|
|
| def test_extraction_result_wraps_invoice(): |
| inv = Invoice( |
| invoice_number="INV-999", |
| vendor=Party(name="Acme"), |
| total=50.00, |
| currency="USD", |
| ) |
| result = ExtractionResult[Invoice]( |
| document_type="invoice", |
| data=inv, |
| field_confidences=[ |
| FieldConfidence(field="invoice_number", score=0.99), |
| FieldConfidence(field="total", score=0.95), |
| ], |
| overall_confidence=0.97, |
| raw_text_snippet="INVOICE INV-999...", |
| ) |
| assert result.data.invoice_number == "INV-999" |
| assert result.overall_confidence == 0.97 |
| assert len(result.field_confidences) == 2 |
|
|
|
|
| def test_extraction_result_rejects_bad_confidence(): |
| inv = Invoice( |
| invoice_number="INV-1", |
| vendor=Party(name="A"), |
| total=1.0, |
| currency="USD", |
| ) |
| with pytest.raises(ValidationError): |
| ExtractionResult[Invoice]( |
| document_type="invoice", |
| data=inv, |
| overall_confidence=1.5, |
| ) |
|
|