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_golden.py6# Description : Golden tests — AIR documents compiled against expected journal entries.7# =============================================================================8"""Golden tests.910Each case in tests/golden/cases/*.yaml holds an AIR document and the exact11journal entries the compiler must produce under the ca-qc-2026 policy set.12Expected figures are derived from cited official sources13(docs/research/canada-gst-qst.md, fx-handling.md) — never from memory.14"""15from __future__ import annotations1617from decimal import Decimal18from pathlib import Path1920import pytest21import yaml2223from aic.compiler import compile_document24from alsl.loader import load_policy_set25from core.events import AirDocument2627ROOT = Path(__file__).resolve().parents[2]28CASES_DIR = Path(__file__).parent / "cases"29POLICY_FILE = ROOT / "alsl" / "policies" / "ca-qc-2026.yaml"303132def _load_cases() -> list[dict]:33 cases: list[dict] = []34 for path in sorted(CASES_DIR.glob("*.yaml")):35 data = yaml.safe_load(path.read_text(encoding="utf-8"))36 cases.extend(data["cases"])37 return cases3839CASES = _load_cases()404142@pytest.fixture(scope="module")43def policies():44 return load_policy_set(POLICY_FILE)454647def _lines_as_tuples(lines) -> list[tuple[str, str, Decimal, str]]:48 return sorted(49 (l.account.code, l.side.value, l.amount.amount, l.amount.currency)50 for l in lines51 )525354def _expected_as_tuples(raw) -> list[tuple[str, str, Decimal, str]]:55 return sorted(56 (str(acct), str(side), Decimal(str(amount)), str(ccy))57 for acct, side, amount, ccy in raw58 )596061@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES])62def test_golden(case: dict, policies) -> None:63 document = AirDocument.model_validate(case["air"])64 journal, _diags = compile_document(document, policies)6566 entries = {e.id: e for e in journal.entries}67 expected = {e["id"]: e for e in case["expected_entries"]}6869 assert sorted(entries) == sorted(expected), (70 f"{case['name']}: produced entries {sorted(entries)} "71 f"!= expected {sorted(expected)}"72 )73 for entry_id, exp in expected.items():74 got = _lines_as_tuples(entries[entry_id].lines)75 want = _expected_as_tuples(exp["lines"])76 assert got == want, (77 f"{case['name']} / {entry_id}:\n got: {got}\n want: {want}"78 )798081def test_all_golden_lines_have_provenance(policies) -> None:82 """Traceability: every posted line points into the provenance graph and83 traces back to a subtotal rooted in the source event."""84 for case in CASES:85 document = AirDocument.model_validate(case["air"])86 journal, _ = compile_document(document, policies)87 for entry in journal.entries:88 for line in entry.lines:89 assert line.provenance_id is not None90 chain = journal.provenance.trace(line.provenance_id)91 roots = [n for n in chain if n.kind == "event_subtotal"]92 assert roots, f"{case['name']}: line has no event_subtotal root"93 assert roots[-1].source_ref == entry.source_event_id94