# ============================================================================= # Projet : AIR — Accounting Intermediate Representation # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : test_incremental.py # Description : Tests — incremental compilation: diff, reversal + replacement entries. # ============================================================================= """Incremental compilation tests. The scenario CLAUDE.md names explicitly: "if an invoice changes, recompile only the delta (like Git), with reversal entries generated automatically." """ from __future__ import annotations from datetime import date from decimal import Decimal from pathlib import Path from aic.incremental import diff_documents, event_fingerprint, recompile from alsl.loader import load_policy_set from core.events import AirDocument from core.invariant import entry_imbalances from kernel.ledger import Ledger ROOT = Path(__file__).resolve().parents[1] POLICIES = load_policy_set(ROOT / "alsl" / "policies" / "ca-qc-2026.yaml") def _doc(*events: dict) -> AirDocument: return AirDocument.model_validate({"events": list(events)}) SALE = { "id": "evt_inv_001", "type": "Sale", "date": date(2026, 7, 15), "amount": {"amount": "1000.00", "currency": "CAD"}, "tax": {"jurisdiction": "CA-QC"}, } PURCHASE = { "id": "evt_bill_001", "type": "Purchase", "date": date(2026, 7, 16), "amount": {"amount": "400.00", "currency": "CAD"}, "tax": {"jurisdiction": "CA-QC"}, } SALE_CORRECTED = {**SALE, "amount": {"amount": "1200.00", "currency": "CAD"}} def test_fingerprint_is_content_sensitive() -> None: a = _doc(SALE).events[0] b = _doc(SALE_CORRECTED).events[0] c = _doc(SALE).events[0] assert event_fingerprint(a) != event_fingerprint(b) assert event_fingerprint(a) == event_fingerprint(c) def test_diff_classification() -> None: old = _doc(SALE, PURCHASE) new = _doc(SALE_CORRECTED, {**PURCHASE, "id": "evt_bill_002"}) diff = diff_documents(old, new) assert diff.changed == ("evt_inv_001",) assert diff.removed == ("evt_bill_001",) assert diff.added == ("evt_bill_002",) assert diff.unchanged == () def test_unchanged_document_produces_empty_delta() -> None: old = _doc(SALE, PURCHASE) new = _doc(SALE, PURCHASE) result = recompile(old, new, POLICIES) assert result.diff.is_empty() assert result.journal.entries == [] def test_corrected_invoice_yields_reversal_plus_replacement() -> None: result = recompile(_doc(SALE), _doc(SALE_CORRECTED), POLICIES) assert len(result.reversals) == 1 assert len(result.new_entries) == 1 reversal, replacement = result.reversals[0], result.new_entries[0] # reversal is the exact contra of the original 1000.00 compile assert reversal.id == "rev_je_evt_inv_001" assert reversal.reverses == "je_evt_inv_001" credit_ar = [l for l in reversal.lines if l.account.code == "1100" and l.side.value == "credit"] assert credit_ar and credit_ar[0].amount.amount == Decimal("1149.75") # replacement carries the corrected figures under a revisioned id assert replacement.id.startswith("je_evt_inv_001_r") debit_ar = [l for l in replacement.lines if l.account.code == "1100" and l.side.value == "debit"] assert debit_ar and debit_ar[0].amount.amount == Decimal("1379.70") # 1200 * 1.14975 # every delta entry balances for entry in result.journal.entries: assert entry_imbalances(entry) == {} def test_removed_event_yields_reversal_only() -> None: result = recompile(_doc(SALE, PURCHASE), _doc(SALE), POLICIES) assert [e.id for e in result.reversals] == ["rev_je_evt_bill_001"] assert result.new_entries == [] def test_delta_posts_cleanly_onto_the_ledger() -> None: """Full lifecycle: post v1, post the delta, net effect == direct v2 compile.""" from aic.compiler import compile_document old, new = _doc(SALE, PURCHASE), _doc(SALE_CORRECTED, PURCHASE) ledger = Ledger() v1, _ = compile_document(old, POLICIES) ledger.post_journal(v1, "v1") delta = recompile(old, new, POLICIES) ledger.post_journal(delta.journal, "v2-delta") direct = Ledger() v2, _ = compile_document(new, POLICIES) direct.post_journal(v2, "v2") assert ledger.balances() == direct.balances() assert ledger.verify_chain() def test_recompile_is_deterministic() -> None: a = recompile(_doc(SALE, PURCHASE), _doc(SALE_CORRECTED), POLICIES) b = recompile(_doc(SALE, PURCHASE), _doc(SALE_CORRECTED), POLICIES) ids_a = [e.id for e in a.journal.entries] ids_b = [e.id for e in b.journal.entries] assert ids_a == ids_b