# ============================================================================= # Projet : AIR — Accounting Intermediate Representation # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : test_golden.py # Description : Golden tests — AIR documents compiled against expected journal entries. # ============================================================================= """Golden tests. Each case in tests/golden/cases/*.yaml holds an AIR document and the exact journal entries the compiler must produce under the ca-qc-2026 policy set. Expected figures are derived from cited official sources (docs/research/canada-gst-qst.md, fx-handling.md) — never from memory. """ from __future__ import annotations from decimal import Decimal from pathlib import Path import pytest import yaml from aic.compiler import compile_document from alsl.loader import load_policy_set from core.events import AirDocument ROOT = Path(__file__).resolve().parents[2] CASES_DIR = Path(__file__).parent / "cases" POLICY_FILE = ROOT / "alsl" / "policies" / "ca-qc-2026.yaml" def _load_cases() -> list[dict]: cases: list[dict] = [] for path in sorted(CASES_DIR.glob("*.yaml")): data = yaml.safe_load(path.read_text(encoding="utf-8")) cases.extend(data["cases"]) return cases CASES = _load_cases() @pytest.fixture(scope="module") def policies(): return load_policy_set(POLICY_FILE) def _lines_as_tuples(lines) -> list[tuple[str, str, Decimal, str]]: return sorted( (l.account.code, l.side.value, l.amount.amount, l.amount.currency) for l in lines ) def _expected_as_tuples(raw) -> list[tuple[str, str, Decimal, str]]: return sorted( (str(acct), str(side), Decimal(str(amount)), str(ccy)) for acct, side, amount, ccy in raw ) @pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES]) def test_golden(case: dict, policies) -> None: document = AirDocument.model_validate(case["air"]) journal, _diags = compile_document(document, policies) entries = {e.id: e for e in journal.entries} expected = {e["id"]: e for e in case["expected_entries"]} assert sorted(entries) == sorted(expected), ( f"{case['name']}: produced entries {sorted(entries)} " f"!= expected {sorted(expected)}" ) for entry_id, exp in expected.items(): got = _lines_as_tuples(entries[entry_id].lines) want = _expected_as_tuples(exp["lines"]) assert got == want, ( f"{case['name']} / {entry_id}:\n got: {got}\n want: {want}" ) def test_all_golden_lines_have_provenance(policies) -> None: """Traceability: every posted line points into the provenance graph and traces back to a subtotal rooted in the source event.""" for case in CASES: document = AirDocument.model_validate(case["air"]) journal, _ = compile_document(document, policies) for entry in journal.entries: for line in entry.lines: assert line.provenance_id is not None chain = journal.provenance.trace(line.provenance_id) roots = [n for n in chain if n.kind == "event_subtotal"] assert roots, f"{case['name']}: line has no event_subtotal root" assert roots[-1].source_ref == entry.source_event_id