SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
3.0 KB · 75 lines python
Raw Blame History
1# =============================================================================2# Projet : AIR — Accounting Intermediate Representation3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : test_llm_live.py6# Description : LIVE integration tests for the Claude extractor — skipped without ANTHROPIC_API_KEY.7# =============================================================================8"""Live Claude extractor tests.910These hit the real Claude API and are SKIPPED unless ANTHROPIC_API_KEY is set11in the environment — the offline suite (mock extractor) never requires it.12The key is read from the environment only; it is never stored in the repo.1314Run:  ANTHROPIC_API_KEY=sk-ant-... .venv/bin/python -m pytest tests/test_llm_live.py -v15"""16from __future__ import annotations1718import os19from decimal import Decimal20from pathlib import Path2122import pytest2324pytestmark = pytest.mark.skipif(25    not os.environ.get("ANTHROPIC_API_KEY"),26    reason="ANTHROPIC_API_KEY not set — live LLM tests are opt-in",27)2829ROOT = Path(__file__).resolve().parents[1]30POLICY = ROOT / "alsl" / "policies" / "ca-qc-2026.yaml"31INVOICE = ROOT / "tests" / "fixtures" / "invoice_realistic.txt"323334def test_claude_extractor_reads_a_real_invoice() -> None:35    from ingestion.extractor import ClaudeExtractor36    from ingestion.pipeline import ingest3738    outcome = ingest(INVOICE.read_text(encoding="utf-8"), ClaudeExtractor())3940    # extraction must be schema-valid AIR (the LLM never emits journal entries)41    assert outcome.document is not None, outcome.validation_errors42    (event,) = outcome.document.events4344    # what a correct reading of this invoice must contain45    assert event.type.value in ("Sale", "Purchase")   # perspective may vary46    assert event.date.isoformat() == "2026-07-28"47    subtotal = event.subtotal()48    assert subtotal is not None49    # pre-tax amount, never the tax-included total, never a float artifact50    assert subtotal.amount == Decimal("999.99")51    assert subtotal.currency == "CAD"52    assert event.tax is not None and event.tax.jurisdiction.upper().endswith("QC")5354    # confidence must be an honest decimal in (0, 1]55    assert Decimal("0") < outcome.extraction.confidence <= Decimal("1")56    assert event.meta is not None and event.meta.llm is not None575859def test_live_extraction_compiles_to_a_balanced_journal() -> None:60    from aic.compiler import compile_document61    from alsl.loader import load_policy_set62    from core.invariant import entry_imbalances63    from ingestion.extractor import ClaudeExtractor64    from ingestion.pipeline import ingest6566    outcome = ingest(INVOICE.read_text(encoding="utf-8"), ClaudeExtractor())67    assert outcome.document is not None6869    journal, _ = compile_document(outcome.document, load_policy_set(POLICY))70    (entry,) = journal.entries71    assert entry_imbalances(entry) == {}72    # deterministic tax computation on the extracted subtotal: GST 50.00, QST 99.7573    amounts = sorted(str(l.amount.amount) for l in entry.lines)74    assert "50.00" in amounts and "99.75" in amounts75