fix: address settlement display and documentation review findings

Rework Issue 18 / PR 19, source review 76: C11-F01 and D02-F02/D03-F03/D04-F04. Preserve the approved contract and add named display, schema, policy, ownership and signature regressions.
This commit is contained in:
2026-10-13 13:09:16 +00:00
parent 616bb7a744
commit ae4383ca2c
7 changed files with 296 additions and 3 deletions
@@ -252,3 +252,99 @@ def test_authenticated_dashboard_renders_saved_comprehensive_snapshot(tmp_path,
assert conn.execute("SELECT name, sql FROM sqlite_master ORDER BY type,name").fetchall() == before
assert conn.execute("SELECT * FROM asset_snapshot_runs ORDER BY id").fetchall() == before_rows
assert conn.execute("SELECT * FROM settlement_cashflow_snapshots ORDER BY id").fetchall() == before_detail
import pytest
def test_draft_cashflow_blocker_renders_safe_transaction_id_and_reason(monkeypatch):
from streamlit.testing.v1 import AppTest
from jarca_ledger_mcp.adapters.streamlit import settlement_gateway
# Public review contract: cashflow_blockers is a list of safe dictionaries;
# these fields are emitted by the service, not renderer-invented `code`.
draft = {"id": "red-draft", "month": "2026-09", "basis_date": "2026-09-30",
"status": "active", "version": 1, "cashflow_policy": {"savings_categories": [], "category_decisions": []}}
blocker = {"transaction_id": "txn-safe-17", "reason": "unknown cashflow classification requires an explicit decision"}
monkeypatch.setattr(settlement_gateway, "list_settlement_drafts",
lambda *a, **k: {"success": True, "drafts": [draft]})
monkeypatch.setattr(settlement_gateway, "get_settlement_draft",
lambda *a, **k: {"success": True, "draft": draft, "items": [], "review": {"review_fingerprint": "fp"}})
monkeypatch.setattr(settlement_gateway, "review_settlement_draft",
lambda *a, **k: {"success": True, "version": 1, "review_fingerprint": "fp",
"cashflow_blockers": [blocker], "cashflow_items": [], "missing_items": []})
app = AppTest.from_string(
"import streamlit as st\n"
"from dashboard.page_views.settlement_drafts import render_settlement_drafts\n"
"render_settlement_drafts(db_path='synthetic-unused.sqlite3', month='2026-09')",
default_timeout=30,
)
app.run()
assert not app.exception, f"synthetic renderer fixture failed before assertion: {app.exception}"
next(button for button in app.button if button.label == "검토 / 새로고침 대상").click().run()
assert not app.exception
assert next(button for button in app.button if button.label == "검토 결과 승인").disabled
shown = "\n".join(str(x.value) for x in list(app.warning) + list(app.error) + list(app.caption))
assert "txn-safe-17" in shown and blocker["reason"] in shown, (
"public renderer must show safe transaction_id and reason from each blocker; "
"a failure here is the renderer dropping the declared public fields"
)
@pytest.mark.parametrize("comprehensive", [False, True])
def test_saved_monthly_snapshot_renders_close_legacy_and_asset_evidence(monkeypatch, comprehensive):
from streamlit.testing.v1 import AppTest
from jarca_ledger_mcp.adapters.streamlit import overview_gateway, reconciliation_gateway, settlement_gateway
# Mirrors get_monthly_settlement_snapshot's declared response envelope/keys.
# Legacy/partial record is read-only; excluded assets carry reason in `items`.
snapshot = {"source": "settlement_snapshot", "run_id": "saved-run-9", "snapshot_run_id": "saved-run-9",
"settlement_month": "2026-09", "basis_date": "2026-09-30", "is_closed_snapshot": True,
"is_comprehensive": False, "legacy": True, "aggregation_version": None,
"snapshot_schema_version": None, "close_status": "legacy_read_only", "cashflow_items": [],
"items": [{"source_type": "manual_asset", "source_id": "asset-opaque-3", "name": "Synthetic asset",
"exclusion_reason": "valuation_missing", "verification_status": "unverified"}],
"source_snapshot": {"transactions": [], "assets": []}, "totals": {},
"income_total": None, "expense_total": None, "net_cashflow": None,
"total_assets": None, "total_liabilities": None, "net_worth": None,
"correction": {"is_correction": False, "correction_of_run_id": "", "correction_reason": ""}}
if comprehensive:
snapshot.update(is_comprehensive=True, legacy=False, aggregation_version="comprehensive-v1",
snapshot_schema_version=1, close_status="conditional")
for key in ("income_total", "expense_total", "net_cashflow", "total_assets", "total_liabilities", "net_worth"):
snapshot[key] = 0
snapshot["totals"] = {key:snapshot[key] for key in ("income_total", "expense_total", "net_cashflow", "total_assets", "total_liabilities", "net_worth")}
snapshot["source_snapshot"]["assets"] = [
{"source_type":"account", "source_id":"missing-source-7", "status":"missing",
"amount":None, "applied_amount":None, "valuation_id":None,
"verification_status":"unverified", "exclusion_reason":""},
{"source_type":"account", "source_id":"excluded-source-8", "status":"excluded",
"amount":10, "applied_amount":None, "exclusion_reason":"synthetic exclusion",
"unexpected_evidence":"not-public-evidence"},
]
monkeypatch.setattr(overview_gateway, "get_monthly_settlement_summary", lambda *a, **k: {"success": True, "month": "2026-09", "reports": {}})
monkeypatch.setattr(overview_gateway, "list_cross_transfer_review_queue", lambda *a, **k: {"success": True, "items": []})
monkeypatch.setattr(overview_gateway, "list_ledger_import_batches", lambda *a, **k: {"success": True, "import_batches": []})
monkeypatch.setattr(overview_gateway, "get_asset_snapshot_report", lambda *a, **k: {"success": False})
monkeypatch.setattr(reconciliation_gateway, "get_review_operating_indicators", lambda *a, **k: {"success": True, "indicators": {}})
monkeypatch.setattr(reconciliation_gateway, "get_balance_reconciliation_report", lambda *a, **k: {"success": True, "items": []})
monkeypatch.setattr(settlement_gateway, "get_monthly_settlement_snapshot", lambda *a, **k: {"success": True, **snapshot})
monkeypatch.setattr(settlement_gateway, "list_settlement_drafts", lambda *a, **k: {"success": True, "drafts": []})
app = AppTest.from_string(
"from dashboard.page_views.monthly_settlement import page_monthly_settlement\n"
"page_monthly_settlement('synthetic-unused.sqlite3')", default_timeout=30)
app.run()
assert not app.exception, f"synthetic renderer fixture failed before assertion: {app.exception}"
import json
body = "\n".join(str(x.value) for x in list(app.caption) + list(app.markdown) + list(app.text) + list(app.warning) + list(app.info))
body += "\n" + json.dumps([frame.value.to_dict("records") for frame in app.dataframe], ensure_ascii=False, default=str)
assert snapshot["close_status"] in body and (comprehensive or "legacy" in body.lower()), (
"saved snapshot renderer must expose close_status and legacy/partial provenance"
)
assert "asset-opaque-3" in body and "valuation_missing" in body and "제외" in body, (
"saved snapshot renderer must expose each missing/excluded asset status and reason"
)
if comprehensive:
assert "missing-source-7" in body and "missing" in body and "누락" in body
assert "excluded-source-8" in body and "synthetic exclusion" in body
assert "not-public-evidence" not in body
assert not app.exception