Files
jarca-ledger-mcp/tests/test_asset_snapshot_idempotency.py

256 lines
11 KiB
Python

from __future__ import annotations
import json
import sqlite3
from jarca_ledger_mcp import service
def test_replay_preserves_redacted_unverified_candidates_after_correction(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
account = service.create_ledger_account("투자 계좌", type="investment", db_path=db_path)
account_id = account["account"]["id"]
verified = service.register_account_valuation(
account_id, "2026-06-30", 100, verification_status="verified",
idempotency_key="valuation-verified", db_path=db_path,
)
candidate = service.register_account_valuation(
account_id, "2026-06-30", 110, verification_status="unverified",
idempotency_key="valuation-candidate", db_path=db_path,
)
first = service.close_asset_snapshot(
"2026-06-30", idempotency_key="candidate-close-a", db_path=db_path,
)
assert first["success"] is True
assert first["included_items"][0]["unverified_candidates"] == [{
"id": candidate["valuation"]["id"],
"basis_date": "2026-06-30",
"currency": "KRW",
"valuation_kind": "balance",
"verification_status": "unverified",
}]
assert {warning["blocking"] for warning in first["warnings"] if warning["code"] == "unverified_candidate"} == {False}
assert first["included_items"][0]["valuation_id"] == verified["valuation"]["id"]
with sqlite3.connect(db_path) as conn:
persisted = conn.execute("SELECT applied_evidence, excluded_evidence FROM asset_snapshot_runs WHERE id=?", (first["snapshot_run_id"],)).fetchone()
applied_evidence, excluded_evidence = map(json.loads, persisted)
assert applied_evidence[0]["unverified_candidates"] == first["included_items"][0]["unverified_candidates"]
candidate_evidence = [item for item in excluded_evidence if item.get("reason") == "unverified_candidate"]
assert candidate_evidence[0]["candidate"] == first["included_items"][0]["unverified_candidates"][0]
assert candidate_evidence[0]["blocking"] is False
assert candidate_evidence[0]["state_hash"]
corrected = service.close_asset_snapshot(
"2026-06-30", correction_reason="preserve evidence", idempotency_key="candidate-close-b",
db_path=db_path,
)
assert corrected["success"] is True
with sqlite3.connect(db_path) as conn:
before_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
before_runs = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0]
replay = service.close_asset_snapshot(
"2026-06-30", idempotency_key="candidate-close-a", db_path=db_path,
)
with sqlite3.connect(db_path) as conn:
after_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
after_runs = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0]
assert replay == first
assert replay["included_items"] == first["included_items"]
assert replay["warnings"] == first["warnings"]
assert after_replay == before_replay
assert after_runs == before_runs
def test_replay_preserves_blocking_candidate_warning_shape(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
account_id = service.create_ledger_account(
"미확인 계좌", type="investment", db_path=db_path,
)["account"]["id"]
service.register_account_valuation(
account_id, "2026-06-30", 110, verification_status="unverified",
idempotency_key="only-candidate", db_path=db_path,
)
first = service.close_asset_snapshot(
"2026-06-30", idempotency_key="blocking-candidate-close",
allow_missing_exclusions=True, approved_by="reviewer",
approval_reason="exclude unverified evidence",
approval_reference="synthetic-approval", db_path=db_path,
)
assert first["success"] is True
warning = next(item for item in first["warnings"] if item["code"] == "unverified_candidate")
assert "blocking" not in warning
replay = service.close_asset_snapshot(
"2026-06-30", idempotency_key="blocking-candidate-close",
allow_missing_exclusions=True, approved_by="reviewer",
approval_reason="exclude unverified evidence",
approval_reference="synthetic-approval", db_path=db_path,
)
assert replay == first
replay_warning = next(item for item in replay["warnings"] if item["code"] == "unverified_candidate")
assert "blocking" not in replay_warning
def test_malformed_persisted_evidence_fails_closed_without_snapshot_writes(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
service.create_manual_asset("현금", 100, "2026-06-30", "cash", db_path=db_path)
first = service.close_asset_snapshot(
"2026-06-30", idempotency_key="malformed-evidence-close", db_path=db_path,
)
assert first["success"] is True
with sqlite3.connect(db_path) as conn:
conn.execute(
"UPDATE asset_snapshot_runs SET excluded_evidence=? WHERE id=?",
("{malformed", first["snapshot_run_id"]),
)
before_rows = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
before_runs = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0]
report = service.get_asset_snapshot_report("2026-06-30", db_path=db_path)
replay = service.close_asset_snapshot(
"2026-06-30", idempotency_key="malformed-evidence-close", db_path=db_path,
)
timeseries = service.get_net_worth_timeseries(
"2026-06-01", "2026-06-30", db_path=db_path,
)
for result in (report, replay, timeseries):
assert result["success"] is False
assert result["error_code"] == "data_integrity_error"
with sqlite3.connect(db_path) as conn:
assert conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0] == before_rows
assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == before_runs
def test_exact_run_replay_after_correction_reconstructs_original_persisted_run(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
service.create_manual_asset("현금", 100, "2026-06-30", "cash", db_path=db_path)
service.create_manual_asset("평가 대기", None, "2026-06-30", "other", db_path=db_path)
first = service.close_asset_snapshot(
"2026-06-30",
source_note="synthetic initial close",
memo="synthetic initial memo",
idempotency_key="snapshot-close-a",
allow_missing_exclusions=True,
approved_by="reviewer-a",
approval_reason="synthetic initial approval",
approval_reference="approval-a",
db_path=db_path,
)
assert first["success"] is True
assert first["close_status"] == "conditional"
assert first["is_correction"] is False
assert first["warnings"]
assert first["approval"]["approval_reference"] == "approval-a"
assert first["provenance"]["snapshot_run_id"] == first["snapshot_run_id"]
corrected = service.close_asset_snapshot(
"2026-06-30",
source_note="synthetic correction close",
memo="synthetic correction memo",
correction_reason="synthetic correction",
idempotency_key="snapshot-close-b",
allow_missing_exclusions=True,
approved_by="reviewer-b",
approval_reason="synthetic correction approval",
approval_reference="approval-b",
db_path=db_path,
)
assert corrected["success"] is True
assert corrected["is_correction"] is True
assert corrected["snapshot_run_id"] != first["snapshot_run_id"]
assert corrected["correction"]["correction_of_run_id"] == first["snapshot_run_id"]
assert corrected["approval"]["approval_reference"] == "approval-b"
assert corrected["snapshot_run_id"] not in first["inserted_snapshot_ids"]
with sqlite3.connect(db_path) as conn:
before_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
replay = service.close_asset_snapshot(
"2026-06-30",
source_note="synthetic initial close",
memo="synthetic initial memo",
idempotency_key="snapshot-close-a",
allow_missing_exclusions=True,
approved_by="reviewer-a",
approval_reason="synthetic initial approval",
approval_reference="approval-a",
db_path=db_path,
)
with sqlite3.connect(db_path) as conn:
after_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
assert replay == first
assert replay["snapshot_run_id"] == first["snapshot_run_id"]
assert replay["inserted_snapshot_ids"] == first["inserted_snapshot_ids"]
assert replay["correction"] == first["correction"]
assert replay["approval"] == first["approval"]
assert replay["warnings"] == first["warnings"]
assert replay["provenance"] == first["provenance"]
assert replay["included_items"] == first["included_items"]
assert replay["excluded_items"] == first["excluded_items"]
assert replay["total_assets"] == first["total_assets"]
assert replay["total_liabilities"] == first["total_liabilities"]
assert replay["net_worth"] == first["net_worth"]
assert replay["snapshot_run_id"] != corrected["snapshot_run_id"]
assert corrected["snapshot_run_id"] not in replay["inserted_snapshot_ids"]
assert after_replay == before_replay
def test_empty_run_replay_after_nonempty_correction_preserves_original_empty_payload(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
service.create_ledger_account("누락 계좌", db_path=db_path)
first = service.close_asset_snapshot(
"2026-06-30",
idempotency_key="empty-close-a",
allow_missing_exclusions=True,
approved_by="reviewer-a",
approval_reason="approve empty",
approval_reference="approval-a",
db_path=db_path,
)
assert first["success"] is True
assert first["inserted_count"] == 0
assert first["included_items"] == []
service.create_manual_asset("후속 자산", 100, "2026-06-30", "cash", db_path=db_path)
corrected = service.close_asset_snapshot(
"2026-06-30",
correction_reason="add verified asset",
idempotency_key="nonempty-close-b",
allow_missing_exclusions=True,
approved_by="reviewer-b",
approval_reason="approve correction",
approval_reference="approval-b",
db_path=db_path,
)
assert corrected["success"] is True
assert corrected["inserted_count"] == 1
# Restore the original live authority while retaining the later immutable
# correction run, so replay reaches the original-key reconstruction path.
with sqlite3.connect(db_path) as conn:
conn.execute("DELETE FROM manual_assets WHERE name=?", ("후속 자산",))
replay = service.close_asset_snapshot(
"2026-06-30",
idempotency_key="empty-close-a",
allow_missing_exclusions=True,
approved_by="reviewer-a",
approval_reason="approve empty",
approval_reference="approval-a",
db_path=db_path,
)
assert replay == first
assert replay["inserted_count"] == 0
assert replay["inserted_snapshot_ids"] == []
assert replay["included_items"] == []