fix: harden valuation evidence selection

This commit is contained in:
2026-10-06 08:54:12 +00:00
parent 009c84331b
commit 921f130fb0
4 changed files with 195 additions and 16 deletions
+99 -1
View File
@@ -1,9 +1,13 @@
import sqlite3
import inspect
import json
from concurrent.futures import ThreadPoolExecutor
from jarca_ledger_mcp import service
from jarca_ledger_mcp.application.account_valuation_service import _public_candidate
from jarca_ledger_mcp.application.account_valuation_service import (
_public_candidate,
record_account_valuation_candidate,
)
from jarca_ledger_mcp.application.imports_service import _record_import_valuation_candidate
from jarca_ledger_mcp.db import init_db
@@ -158,6 +162,24 @@ def test_import_candidate_helper_reuses_deterministic_duplicate_on_same_connecti
assert first is None and second is None and count == 1
def test_candidate_idempotency_rejects_changed_payload(tmp_path):
path = str(tmp_path / "ledger.sqlite3")
account = service.create_ledger_account("계좌", db_path=path)["account"]["id"]
with sqlite3.connect(path) as conn:
conn.row_factory = sqlite3.Row
first = record_account_valuation_candidate(
conn, account, "2026-06-30", 100, "KRW", "import",
"source-a", "candidate-key", "memo-a",
)
conflict = record_account_valuation_candidate(
conn, account, "2026-07-01", 200, "KRW", "import",
"source-b", "candidate-key", "memo-b",
)
assert first["success"] is True
assert conflict["success"] is False
assert conflict["error_code"] == "idempotency_conflict"
def test_valuation_currency_and_loan_kind_are_rejected(tmp_path):
path = str(tmp_path / "ledger.sqlite3")
bank = service.create_ledger_account("은행", currency="KRW", db_path=path)["account"]["id"]
@@ -224,6 +246,82 @@ def test_snapshot_report_redacts_unverified_candidate_sensitive_fields(tmp_path)
assert set(candidates[0]) == {"id", "basis_date", "currency", "valuation_kind", "verification_status"}
def test_preview_warns_about_unverified_candidate_when_verified_value_is_selected(tmp_path):
path = str(tmp_path / "ledger.sqlite3")
account = service.create_ledger_account(
"투자", type="investment", db_path=path,
)["account"]["id"]
service.register_account_valuation(
account, "2026-06-30", 100, valuation_kind="total_value",
verification_status="verified", idempotency_key="verified", db_path=path,
)
service.register_account_valuation(
account, "2026-06-30", 999, valuation_kind="total_value",
verification_status="unverified", idempotency_key="candidate", db_path=path,
)
preview = service.preview_asset_snapshot_close("2026-06-30", db_path=path)
warning = next(item for item in preview["warnings"] if item["code"] == "unverified_candidate")
assert warning["source_id"] == account
assert warning["candidates"][0]["verification_status"] == "unverified"
closed = service.close_asset_snapshot("2026-06-30", db_path=path)
assert closed["success"] is True
with sqlite3.connect(path) as conn:
evidence = json.loads(conn.execute(
"SELECT excluded_evidence FROM asset_snapshot_runs WHERE id=?",
(closed["snapshot_run_id"],),
).fetchone()[0])
assert evidence[0]["reason"] == "unverified_candidate"
assert evidence[0]["candidate"]["verification_status"] == "unverified"
def test_snapshot_idempotency_binds_redacted_candidate_state(tmp_path):
path = str(tmp_path / "ledger.sqlite3")
account = service.create_ledger_account(
"투자", type="investment", db_path=path,
)["account"]["id"]
service.register_account_valuation(
account, "2026-06-30", 100, valuation_kind="total_value",
verification_status="verified", idempotency_key="verified", db_path=path,
)
candidate = service.register_account_valuation(
account, "2026-06-30", 999, valuation_kind="total_value",
verification_status="unverified", idempotency_key="candidate", db_path=path,
)["valuation"]
first = service.close_asset_snapshot(
"2026-06-30", idempotency_key="close-candidate", db_path=path,
)
with sqlite3.connect(path) as conn:
conn.execute(
"UPDATE account_valuations SET amount=?, source_ref=?, memo=? WHERE id=?",
(1000, "changed-source", "changed-memo", candidate["id"]),
)
retry = service.close_asset_snapshot(
"2026-06-30", idempotency_key="close-candidate", db_path=path,
)
assert first["success"] is True
assert retry["success"] is False
assert retry["error_code"] == "idempotency_conflict"
def test_reconciliation_fails_closed_on_ambiguous_verified_leaf(tmp_path):
path = str(tmp_path / "ledger.sqlite3")
account = service.create_ledger_account(
"투자", type="investment", last_balance=100,
last_synced_at="2026-06-30", db_path=path,
)["account"]["id"]
for key, amount in (("v1", 100), ("v2", 200)):
service.register_account_valuation(
account, "2026-06-30", amount, valuation_kind="total_value",
verification_status="verified", idempotency_key=key, db_path=path,
)
item = service.get_balance_reconciliation_report(
account_id=account, baseline_at="2026-06-30T00:00:00+09:00", db_path=path,
)["items"][0]
assert item["status"] == "AMBIGUOUS_VALUATION"
assert item["external_confirmed_value"] is None
assert item["valuation_id"] == ""
def test_snapshot_rejects_bogus_selection_policy_and_manual_usd_is_excluded(tmp_path):
path = str(tmp_path / "ledger.sqlite3")
assert service.preview_asset_snapshot_close("2026-06-30", selection_policy="bogus", db_path=path)["success"] is False