from __future__ import annotations """Acceptance contract for the approved monthly settlement draft lifecycle. These tests intentionally resolve the API at runtime: before implementation the suite must fail as an assertion (RED), not fail during pytest collection. All data is synthetic and each test owns a temporary SQLite database. """ import json import sqlite3 from concurrent.futures import ThreadPoolExecutor import pytest from jarca_ledger_mcp import service from jarca_ledger_mcp.db import init_db API = ( "create_settlement_draft", "list_settlement_drafts", "get_settlement_draft", "update_settlement_draft", "refresh_settlement_draft_targets", "set_settlement_draft_item", "review_settlement_draft", "approve_settlement_draft", "finalize_settlement_draft", "cancel_settlement_draft", ) def api(name): # Deliberately runtime checked so absent API produces a clear RED assertion. fn = getattr(service, name, None) assert callable(fn), f"required settlement-draft API missing: service.{name}" return fn def dbfile(tmp_path): path = str(tmp_path / "synthetic-ledger.sqlite3") init_db(path) return path def create(path, month="2026-09", key="create-1", **kwargs): return api("create_settlement_draft")(month, "tester", key, db_path=path, **kwargs) def assert_ok(result): assert result.get("success") is True, result return result def draft_of(result): assert isinstance(result.get("draft"), dict), result return result["draft"] def test_empty_draft_persists_without_closing_and_has_consistent_public_shape(tmp_path): path = dbfile(tmp_path) result = assert_ok(create(path)) draft = draft_of(result) assert draft["month"] == "2026-09" and draft["basis_date"] == "2026-09-30" assert draft["status"] == "active" and draft["version"] == 1 assert result["items"] == [] assert isinstance(result["review"], dict) with sqlite3.connect(path) as conn: assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0 assert conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0] == 0 fetched = assert_ok(api("get_settlement_draft")(draft["id"], db_path=path)) assert fetched["draft"]["id"] == draft["id"] listed = assert_ok(api("list_settlement_drafts")(month="2026-09", db_path=path)) assert any(row.get("id") == draft["id"] for row in listed.get("drafts", [])) @pytest.mark.parametrize("month", ["", "2026-9", "2026-13", "2026/09", "2026-09-30", " 2026-09"]) def test_month_and_basis_are_strictly_validated(tmp_path, month): path = dbfile(tmp_path) result = api("create_settlement_draft")(month, "tester", "strict", db_path=path) assert result.get("success") is False assert result.get("error_code") in {"validation_error", "invalid_argument"} def test_basis_must_be_calendar_month_end_and_create_key_is_idempotent(tmp_path): path = dbfile(tmp_path) bad = api("create_settlement_draft")("2026-09", "tester", "bad-date", basis_date="2026-09-29", db_path=path) assert bad.get("success") is False first = assert_ok(create(path, key="same-key", memo="first")) replay = assert_ok(create(path, key="same-key", memo="first")) assert draft_of(first)["id"] == draft_of(replay)["id"] conflict = create(path, key="same-key", memo="different payload") assert conflict.get("success") is False def test_one_active_draft_across_normal_and_correction_and_cancellation_reopens_month(tmp_path): path = dbfile(tmp_path) first = draft_of(assert_ok(create(path, key="one"))) duplicate = create(path, key="two") assert duplicate.get("success") is False assert api("cancel_settlement_draft")(first["id"], first["version"], "tester", "test cancellation", db_path=path).get("success") is True reopened = draft_of(assert_ok(create(path, key="three"))) assert reopened["id"] != first["id"] assert reopened["correction_of_run_id"] in (None, "") def test_input_validation_versions_actors_and_include_exclude_reason(tmp_path): path = dbfile(tmp_path) draft = draft_of(assert_ok(create(path))) update = api("update_settlement_draft") for bad_version in (0, -1, True, 1.5, None): result = update(draft["id"], bad_version, "tester", "memo", db_path=path) assert result.get("success") is False for actor in ("", " "): result = update(draft["id"], 1, actor, "memo", db_path=path) assert result.get("success") is False assert api("refresh_settlement_draft_targets")(draft["id"], 1, "tester", db_path=path).get("success") is True current = assert_ok(api("get_settlement_draft")(draft["id"], db_path=path))["draft"] assert current["version"] >= 2 def test_targets_are_frozen_until_explicit_refresh_and_review_is_read_only(tmp_path): path = dbfile(tmp_path) acct = service.create_ledger_account("Synthetic cash", last_balance=10, liquidity_bucket="cash", db_path=path) assert acct["success"] draft = draft_of(assert_ok(create(path))) before = assert_ok(api("get_settlement_draft")(draft["id"], db_path=path)) service.create_ledger_account("Added later", last_balance=20, liquidity_bucket="cash", db_path=path) with sqlite3.connect(path) as conn: snapshot = conn.execute("SELECT COUNT(*) FROM settlement_draft_items").fetchone()[0] if conn.execute("SELECT 1 FROM sqlite_master WHERE name='settlement_draft_items'").fetchone() else None review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) assert {"version", "review_fingerprint", "items", "included_items", "excluded_items", "missing_items", "warnings", "approval_required", "totals"} <= review.keys() assert {"total_assets", "total_liabilities", "net_worth"} <= review["totals"].keys() assert_ok(api("get_settlement_draft")(draft["id"], db_path=path)) assert before["draft"]["version"] == draft["version"] with sqlite3.connect(path) as conn: assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0 if snapshot is not None: assert conn.execute("SELECT COUNT(*) FROM settlement_draft_items").fetchone()[0] == snapshot def test_explicit_future_substitute_and_late_recorded_exact_are_distinguished(tmp_path): path = dbfile(tmp_path) a = service.create_ledger_account("Future account", type="investment", liquidity_bucket="investment", db_path=path)["account"]["id"] b = service.create_ledger_account("Late exact", type="investment", liquidity_bucket="investment", db_path=path)["account"]["id"] future = service.register_account_valuation(a, "2026-10-05", 500, valuation_kind="total_value", verification_status="verified", idempotency_key="future", db_path=path) late = service.register_account_valuation(b, "2026-09-30", 700, valuation_kind="total_value", verification_status="verified", idempotency_key="late-exact", db_path=path) assert late.get("success") with sqlite3.connect(path) as conn: conn.execute("UPDATE account_valuations SET recorded_at=? WHERE id=?", ("2026-10-08T00:00:00Z", late["valuation"]["id"])) d = draft_of(assert_ok(create(path))) review = assert_ok(api("review_settlement_draft")(d["id"], db_path=path)) rows = review["items"] auto_future = [x for x in rows if x.get("source_id") == a] assert not any(x.get("valuation_id") == future.get("valuation", {}).get("id") and x.get("selection_mode") == "exact" for x in auto_future) item = next(x for x in rows if x.get("source_id") == b) assert item.get("valuation_id") == late["valuation"]["id"] assert item.get("selection_mode", "exact") == "exact" assert api("set_settlement_draft_item")(d["id"], "account", a, d["version"], "tester", valuation_id=future["valuation"]["id"], selection_mode="manual_substitute", selection_reason="synthetic future evidence", db_path=path).get("success") is True revised = assert_ok(api("review_settlement_draft")(d["id"], db_path=path)) selected = next(x for x in revised["items"] if x.get("source_id") == a) assert selected.get("valuation_date") == "2026-10-05" assert selected.get("date_difference_days") == 5 assert selected.get("selection_mode") == "manual_substitute" def test_unverified_candidate_has_no_authority_and_verified_child_requires_reselection(tmp_path): path = dbfile(tmp_path) acct = service.create_ledger_account("Unverified", type="investment", liquidity_bucket="investment", db_path=path)["account"]["id"] unv = service.register_account_valuation(acct, "2026-09-30", 900, valuation_kind="total_value", verification_status="unverified", idempotency_key="unv", db_path=path) draft = draft_of(assert_ok(create(path))) review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) row = next(x for x in review["items"] if x.get("source_id") == acct) assert row.get("amount") is None assert row.get("candidate_amount") == 900 child = service.correct_account_valuation(unv["valuation"]["id"], amount=950, verification_status="verified", correction_reason="synthetic verification", idempotency_key="verified-child", db_path=path) assert child.get("success") changed = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) assert any(x.get("source_id") == acct for x in changed["missing_items"]) assert api("set_settlement_draft_item")(draft["id"], "account", acct, draft["version"], "tester", valuation_id=child["valuation"]["id"], selection_mode="exact", db_path=path).get("success") is True def test_manual_sources_drift_requires_explicit_refresh_and_future_not_auto_selected(tmp_path): path = dbfile(tmp_path) manual = service.create_manual_asset("Synthetic asset", 100, "2026-09-30", "real_estate", db_path=path)["manual_asset"] future = service.create_manual_asset("Future asset", 200, "2026-10-07", "deposit", db_path=path)["manual_asset"] draft = draft_of(assert_ok(create(path))) review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) assert not any(x.get("source_id") == future["id"] and x.get("amount") is not None for x in review["items"]) service.update_manual_asset(manual["id"], valuation_amount=110, db_path=path) drift = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) assert any("drift" in json.dumps(x).lower() or x.get("review_needed") for x in drift["items"]) refreshed = api("refresh_settlement_draft_targets")(draft["id"], draft["version"], "tester", db_path=path) assert refreshed.get("success") is True def test_approval_fingerprint_mutation_and_external_drift_block_finalize(tmp_path): path = dbfile(tmp_path) draft = draft_of(assert_ok(create(path))) review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) approved = api("approve_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "approver", "reviewed synthetic evidence", "ref-1", allow_missing_exclusions=True, db_path=path) assert approved.get("success") is True mutated = api("update_settlement_draft")(draft["id"], draft["version"], "tester", "changed after approval", db_path=path) assert mutated.get("success") is True stale = api("finalize_settlement_draft")(draft["id"], mutated["draft"]["version"], review["review_fingerprint"], "close-key", db_path=path) assert stale.get("success") is False assert stale.get("error_code") in {"conflict", "validation_error"} def test_finalize_persists_immutable_provenance_replays_exact_response_and_conflicts_key(tmp_path): path = dbfile(tmp_path) assert_ok(service.create_ledger_account("Synthetic cash", last_balance=321, liquidity_bucket="cash", db_path=path)) draft = draft_of(assert_ok(create(path))) review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) approval = api("approve_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "approver", "approved", "approval-1", allow_missing_exclusions=True, db_path=path) assert approval.get("success") final = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "final-key", db_path=path) assert final.get("success") replay = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "final-key", db_path=path) assert replay == final conflict = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "final-key", db_path=path) assert conflict == final with sqlite3.connect(path) as conn: run_count = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] row_count = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0] assert run_count == 1 and row_count >= 1 assert final["draft_id"] == draft["id"] assert final["draft_version"] == draft["version"] assert final["review_fingerprint"] == review["review_fingerprint"] report = service.get_asset_snapshot_report("2026-09-30", db_path=path) series = service.get_net_worth_timeseries(start_date="2026-09-01", end_date="2026-09-30", db_path=path) monthly = service.get_monthly_settlement_summary("2026-09", db_path=path) assert report["success"] and series["success"] and monthly["success"] assert report["close_metadata"].get("draft_id") == draft["id"] def test_sqlite_trigger_failure_rolls_back_run_rows_and_completion(tmp_path): path = dbfile(tmp_path) assert_ok(service.create_ledger_account("Synthetic", last_balance=5, liquidity_bucket="cash", db_path=path)) draft = draft_of(assert_ok(create(path))) review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) approved = api("approve_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "approver", "approved", "ref", allow_missing_exclusions=True, db_path=path) assert approved.get("success") with sqlite3.connect(path) as conn: conn.execute("CREATE TRIGGER fail_snapshot_insert BEFORE INSERT ON asset_snapshots BEGIN SELECT RAISE(ABORT, 'injected failure'); END") result = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "failure-key", db_path=path) assert result.get("success") is False with sqlite3.connect(path) as conn: assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0 assert conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0] == 0 after = assert_ok(api("get_settlement_draft")(draft["id"], db_path=path)) assert after["draft"]["status"] == "active" def test_schema13_migration_is_additive_and_idempotent(tmp_path): path = dbfile(tmp_path) with sqlite3.connect(path) as conn: conn.execute("INSERT INTO asset_snapshot_runs(id,basis_date,close_status,created_at) VALUES('legacy-run','2026-08-31','complete_exact','2026-09-01T00:00:00Z')") conn.commit() init_db(path) init_db(path) with sqlite3.connect(path) as conn: assert conn.execute("SELECT id FROM asset_snapshot_runs WHERE id='legacy-run'").fetchone()[0] == 'legacy-run' assert {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")} >= {'settlement_drafts','settlement_draft_items','asset_snapshot_runs','asset_snapshots'} assert conn.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == '15' def test_concurrent_create_and_stale_version_finalization_are_safe(tmp_path): path = dbfile(tmp_path) fn = api("create_settlement_draft") def attempt(i): return fn("2026-09", "tester", f"concurrent-{i}", db_path=path) with ThreadPoolExecutor(max_workers=4) as pool: results = list(pool.map(attempt, range(4))) successes = [r for r in results if r.get("success")] assert len(successes) == 1 assert all(r.get("success") or r.get("error_code") in {"conflict", "validation_error"} for r in results) d = draft_of(successes[0]) review = assert_ok(api("review_settlement_draft")(d["id"], db_path=path)) approved = api("approve_settlement_draft")(d["id"], d["version"], review["review_fingerprint"], "approver", "approved", "r", allow_missing_exclusions=True, db_path=path) assert approved.get("success") stale = api("finalize_settlement_draft")(d["id"], d["version"] + 1, review["review_fingerprint"], "stale", db_path=path) assert stale.get("success") is False def test_approval_is_mandatory_even_for_complete_exact_evidence(tmp_path): path = dbfile(tmp_path) draft = draft_of(assert_ok(create(path))) review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) finalized = api("finalize_settlement_draft")( draft["id"], draft["version"], review["review_fingerprint"], "unapproved", db_path=path, ) assert finalized.get("success") is False