spb/air Public MIT
AIR — The Language of Accounting.
Python 100%
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