"""Synthetic regressions for frozen evidence and reviewed authority.""" import sqlite3 import pytest from jarca_ledger_mcp import service as s from jarca_ledger_mcp.db import init_db def ok(result): assert result['success'], result return result @pytest.fixture def case(tmp_path): path = str(tmp_path / 'authority.sqlite3') init_db(path) account = ok(s.create_ledger_account('Synthetic investment', type='investment', liquidity_bucket='investment', db_path=path))['account']['id'] val = ok(s.register_account_valuation(account, '2026-09-30', 500, valuation_kind='total_value', verification_status='verified', idempotency_key='evidence', db_path=path))['valuation']['id'] draft = ok(s.create_settlement_draft('2026-09', 'creator', 'create', db_path=path))['draft'] return path, account, val, draft def approve(path, draft, **flags): review = ok(s.review_settlement_draft(draft['id'], db_path=path)) ok(s.approve_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'reviewer', 'synthetic reviewed', 'ref', db_path=path, **flags)) return review def counts(path): with sqlite3.connect(path) as conn: return tuple(conn.execute('SELECT COUNT(*) FROM ' + t).fetchone()[0] for t in ('asset_snapshot_runs', 'asset_snapshots')) @pytest.mark.parametrize('sql', [ "UPDATE account_valuations SET amount=600 WHERE id=?", "UPDATE account_valuations SET memo='changed' WHERE id=?", "UPDATE account_valuations SET principal=123 WHERE id=?", ]) def test_full_valuation_drift_changes_fingerprint_and_blocks_write(case, sql): path, account, val, draft = case review = approve(path, draft) with sqlite3.connect(path) as conn: conn.execute(sql, (val,)) changed = ok(s.review_settlement_draft(draft['id'], db_path=path)) assert changed['review_fingerprint'] != review['review_fingerprint'] assert any(x.get('review_needed') for x in changed['items']) result = s.finalize_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'final', db_path=path) assert not result['success'], result assert counts(path) == (0, 0) def test_classification_drift_requires_explicit_reselection(case): path, account, val, draft = case review = approve(path, draft) with sqlite3.connect(path) as conn: conn.execute("UPDATE accounts SET liquidity_bucket='cash' WHERE id=?", (account,)) changed = ok(s.review_settlement_draft(draft['id'], db_path=path)) assert changed['review_fingerprint'] != review['review_fingerprint'] assert any(x.get('review_needed') for x in changed['items']) assert not s.finalize_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'final', db_path=path)['success'] assert counts(path) == (0, 0) def test_corrected_parent_cannot_finalize_without_explicit_child_decision(case): path, account, val, draft = case previous = approve(path, draft) child = ok(s.correct_account_valuation(val, amount=550, verification_status='verified', correction_reason='synthetic correction', idempotency_key='child', db_path=path))['valuation']['id'] review = ok(s.review_settlement_draft(draft['id'], db_path=path)) assert review['review_fingerprint'] != previous['review_fingerprint'] result = s.approve_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'reviewer', 'review again', 'ref2', db_path=path) if result['success']: assert not s.finalize_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'final', db_path=path)['success'] assert counts(path) == (0, 0) updated = ok(s.set_settlement_draft_item(draft['id'], 'account', account, draft['version'], 'selector', valuation_id=child, db_path=path))['draft'] fresh = approve(path, updated) final = ok(s.finalize_settlement_draft(updated['id'], updated['version'], fresh['review_fingerprint'], 'final2', db_path=path)) assert final['totals']['net_worth'] == 550 def test_create_replay_uses_original_payload_after_memo_edit(case): path, account, val, draft = case ok(s.update_settlement_draft(draft['id'], draft['version'], 'editor', 'edited', db_path=path)) replay = ok(s.create_settlement_draft('2026-09', 'creator', 'create', db_path=path)) assert replay['draft']['id'] == draft['id'] assert not s.create_settlement_draft('2026-09', 'creator', 'create', memo='different original request', db_path=path)['success'] def test_empty_draft_cannot_create_empty_close(tmp_path): path = str(tmp_path / 'empty.sqlite3') init_db(path) draft = ok(s.create_settlement_draft('2026-09', 'creator', 'empty', db_path=path))['draft'] review = approve(path, draft, allow_missing_exclusions=True) assert review['complete'] is False assert not s.finalize_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'empty-final', db_path=path)['success'] assert counts(path) == (0, 0) def test_manual_reselection_explicitly_refreezes_semantic_evidence(tmp_path): path = str(tmp_path / 'manual.sqlite3') init_db(path) asset = ok(s.create_manual_asset('Synthetic home', 100, '2026-09-30', 'real_estate', db_path=path))['manual_asset']['id'] draft = ok(s.create_settlement_draft('2026-09', 'creator', 'manual', db_path=path))['draft'] old = approve(path, draft) ok(s.update_manual_asset(asset, valuation_amount=110, db_path=path)) drift = ok(s.review_settlement_draft(draft['id'], db_path=path)) assert drift['review_fingerprint'] != old['review_fingerprint'] assert not s.finalize_settlement_draft(draft['id'], draft['version'], old['review_fingerprint'], 'stale', db_path=path)['success'] refreshed = ok(s.set_settlement_draft_item(draft['id'], 'manual', asset, draft['version'], 'selector', db_path=path))['draft'] new = approve(path, refreshed) assert not any(x.get('review_needed') for x in new['items']) final = ok(s.finalize_settlement_draft(refreshed['id'], refreshed['version'], new['review_fingerprint'], 'manual-final', db_path=path)) assert final['totals']['net_worth'] == 110 @pytest.mark.parametrize('operation,args', [ ('list_settlement_drafts', ()), ('get_settlement_draft', ('missing',)), ('review_settlement_draft', ('missing',)), ]) def test_draft_reads_preserve_missing_db_error_and_no_create(tmp_path, operation, args): path = tmp_path / 'absent-parent' / 'db.sqlite3' result = getattr(s, operation)(*args, db_path=str(path)) assert result['error_code'] == 'db_not_initialized', result assert not path.parent.exists() def test_target_refresh_refreezes_manual_retains_account_selection_and_retires_targets(tmp_path): path = str(tmp_path / 'refresh.sqlite3') init_db(path) manual = ok(s.create_manual_asset('Synthetic property', 100, '2026-09-30', 'real_estate', db_path=path))['manual_asset']['id'] account = ok(s.create_ledger_account('Synthetic investment', type='investment', liquidity_bucket='investment', db_path=path))['account']['id'] val = ok(s.register_account_valuation(account, '2026-09-30', 50, valuation_kind='total_value', verification_status='verified', idempotency_key='initial', db_path=path))['valuation']['id'] d = ok(s.create_settlement_draft('2026-09', 'creator', 'create', db_path=path))['draft'] ok(s.update_manual_asset(manual, valuation_amount=110, db_path=path)) child = ok(s.correct_account_valuation(val, amount=60, verification_status='verified', correction_reason='synthetic', idempotency_key='child', db_path=path))['valuation']['id'] added = ok(s.create_manual_asset('Synthetic new', 20, '2026-09-30', 'other', db_path=path))['manual_asset']['id'] result = ok(s.refresh_settlement_draft_targets(d['id'], d['version'], 'refresh actor', db_path=path)) review = result['review'] property_row = next(x for x in review['items'] if x['source_id'] == manual) assert property_row['amount'] == 110 and not property_row['review_needed'] account_row = next(x for x in review['items'] if x['source_id'] == account) assert account_row['valuation_id'] == val and account_row['valuation_id'] != child assert account_row['review_needed'] assert any(x['source_id'] == added and x['amount'] == 20 for x in review['items']) ok(s.deactivate_manual_asset(added, db_path=path)) refreshed = ok(s.refresh_settlement_draft_targets(d['id'], result['draft']['version'], 'refresh actor', db_path=path)) retired = next(x for x in refreshed['items'] if x['source_id'] == added) assert retired['included'] is False and retired['exclusion_reason'] assert retired['selected_by'] == 'refresh actor' @pytest.mark.parametrize('action', ['memo', 'cancel', 'refresh', 'selection']) def test_any_draft_mutation_clears_all_approval_version_state(case, action): path, account, val, draft = case approve(path, draft) before = ok(s.get_settlement_draft(draft['id'], db_path=path))['draft'] assert before['approval_version'] == draft['version'] and before['approved_at'] if action == 'memo': result = s.update_settlement_draft(draft['id'], draft['version'], 'editor', 'edited', db_path=path) elif action == 'cancel': result = s.cancel_settlement_draft(draft['id'], draft['version'], 'editor', 'synthetic cancellation', db_path=path) elif action == 'refresh': result = s.refresh_settlement_draft_targets(draft['id'], draft['version'], 'editor', db_path=path) else: result = s.set_settlement_draft_item(draft['id'], 'account', account, draft['version'], 'editor', valuation_id=val, db_path=path) changed = ok(result)['draft'] assert changed['version'] == draft['version'] + 1 assert changed['approval_version'] == 0 and changed['approved_at'] == '' assert all(changed[key] == '' for key in ('approved_by', 'approval_fingerprint', 'approval_reason', 'approval_reference')) assert changed['approval_flags'] == '{}' def test_finalize_same_key_different_payload_conflicts_without_partial_write(case): path, account, val, draft = case review = approve(path, draft) final = ok(s.finalize_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'final-key', db_path=path)) before = counts(path) assert s.finalize_settlement_draft(draft['id'], draft['version'], review['review_fingerprint'], 'final-key', db_path=path) == final conflict = s.finalize_settlement_draft(draft['id'], draft['version'] + 1, review['review_fingerprint'], 'final-key', db_path=path) assert not conflict['success'] and conflict['error_code'] == 'conflict' assert counts(path) == before