# ============================================================================= # Projet : AIR — Accounting Intermediate Representation # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : test_workspace.py # Description : Tests — the AIR home (managed local data) and the standalone CLI flows. # ============================================================================= """AIR home + CLI tests: init, compile into the home, archives, status, incremental recompile, statements, and offline QBO export — all without any third-party system.""" from __future__ import annotations import json from pathlib import Path import pytest from kernel.workspace import Workspace, WorkspaceError from sdk.cli import main ROOT = Path(__file__).resolve().parents[1] POLICY = str(ROOT / "alsl" / "policies" / "ca-qc-2026.yaml") DEMO = str(ROOT / "tests" / "fixtures" / "demo_document.yaml") def test_workspace_init_open_and_guardrails(tmp_path: Path) -> None: home = tmp_path / "books" ws = Workspace.init(home, name="acme", policies=POLICY) assert ws.meta["name"] == "acme" with pytest.raises(WorkspaceError, match="already initialized"): Workspace.init(home) with pytest.raises(WorkspaceError, match="no AIR home"): Workspace.open(tmp_path / "elsewhere") def test_cli_full_standalone_lifecycle(tmp_path: Path, capsys) -> None: home = str(tmp_path / "books") assert main(["init", "--home", home, "--policies", POLICY]) == 0 assert main(["compile", DEMO, "--home", home]) == 0 out = capsys.readouterr().out assert "8 entries" in out and "document archived" in out # replaying the same document is idempotent (0 appended) assert main(["compile", DEMO, "--home", home]) == 0 assert "+0 appended" in capsys.readouterr().out # data is managed inside the home ws = Workspace.open(home) status = ws.status() assert status["entries"] == 8 assert status["chain_valid"] is True assert status["documents_archived"] == 1 # content-addressed, no duplicate # statements straight from the home assert main(["report", "trial-balance", "--home", home]) == 0 tb = capsys.readouterr().out assert "TOTAL" in tb assert main(["status", "--home", home]) == 0 assert "chain_valid" in capsys.readouterr().out def test_cli_incremental_into_home(tmp_path: Path, capsys) -> None: home = str(tmp_path / "books") main(["init", "--home", home, "--policies", POLICY]) main(["compile", DEMO, "--home", home]) capsys.readouterr() corrected = tmp_path / "demo_v2.yaml" corrected.write_text( Path(DEMO).read_text().replace('amount: "333.33"', 'amount: "349.99"'), encoding="utf-8", ) assert main(["recompile", DEMO, str(corrected), "--home", home]) == 0 out = capsys.readouterr().out assert "~1 changed" in out and "1 reversal(s)" in out status = Workspace.open(home).status() assert status["entries"] == 10 # 8 + reversal + replacement assert status["documents_archived"] == 2 assert status["chain_valid"] is True def test_cli_qbo_export_needs_no_quickbooks(tmp_path: Path, capsys) -> None: home = str(tmp_path / "books") main(["init", "--home", home, "--policies", POLICY]) capsys.readouterr() assert main(["compile", DEMO, "--home", home, "--backend", "qbo-export"]) == 0 out = capsys.readouterr().out assert "QBO JSON export" in out exports = list(Workspace.open(home).exports_dir.glob("qbo_journal_*.json")) assert len(exports) == 1 data = json.loads(exports[0].read_text()) assert len(data["journal_entries"]) == 8 assert data["journal_entries"][0]["Line"] # QBO JournalEntry shape def test_cli_missing_policies_is_helpful(tmp_path: Path, capsys) -> None: home = str(tmp_path / "books") main(["init", "--home", home]) # no default policies capsys.readouterr() assert main(["compile", DEMO, "--home", home]) == 1 assert "no policy set" in capsys.readouterr().err