393 lines
27 KiB
Python
393 lines
27 KiB
Python
"""Issue 18 adapter/UI contracts; synthetic paths and identities only."""
|
|
import importlib
|
|
import inspect
|
|
from pathlib import Path
|
|
|
|
from jarca_ledger_mcp import service
|
|
from test_comprehensive_settlement import schema_thirteen_conditional_close # noqa: F401
|
|
|
|
POLICY = "set_settlement_cashflow_policy"
|
|
READER = "get_monthly_settlement_snapshot"
|
|
|
|
|
|
def _api(name):
|
|
fn = getattr(service, name, None)
|
|
assert callable(fn), f"required comprehensive settlement API missing: service.{name}"
|
|
return fn
|
|
|
|
|
|
def test_shared_adapters_expose_policy_and_saved_snapshot_contracts(monkeypatch, tmp_path):
|
|
gateway = importlib.import_module("jarca_ledger_mcp.adapters.streamlit.settlement_gateway")
|
|
contracts = importlib.import_module("jarca_ledger_mcp.adapters.mcp.tool_contracts")
|
|
server = importlib.import_module("jarca_ledger_mcp.adapters.mcp.server")
|
|
setter_owner = importlib.import_module("jarca_ledger_mcp.application.settlement_draft_service")
|
|
reader_owner = importlib.import_module("jarca_ledger_mcp.application.monthly_settlement_service")
|
|
expected = {POLICY: ["draft_id", "expected_version", "updated_by", "cashflow_policy", "db_path"], READER: ["month", "run_id", "db_path"]}
|
|
monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(tmp_path / "synthetic.sqlite3"))
|
|
calls = []
|
|
for name, parameters in expected.items():
|
|
fn = _api(name)
|
|
assert list(inspect.signature(fn).parameters) == parameters
|
|
gateway_fn = getattr(gateway, name, None)
|
|
assert callable(gateway_fn), f"gateway missing {name}"
|
|
assert inspect.signature(gateway_fn) == inspect.signature(fn)
|
|
marker = {"success": True, "operation": name}
|
|
def stub(*args, _name=name, _marker=marker, **kwargs):
|
|
calls.append((_name, kwargs.get("db_path")))
|
|
return _marker
|
|
monkeypatch.setattr(setter_owner if name == POLICY else reader_owner, name, stub)
|
|
kwargs = {key: "synthetic" for key in parameters if key != "db_path"}
|
|
if name == POLICY:
|
|
kwargs["expected_version"] = 1
|
|
kwargs["cashflow_policy"] = {"savings_categories": [], "category_decisions": []}
|
|
path = str(tmp_path / "synthetic.sqlite3")
|
|
kwargs["db_path"] = path
|
|
assert gateway_fn(**kwargs) == marker
|
|
assert calls[-1] == (name, path)
|
|
assert name in contracts.TOOL_CONTRACTS
|
|
tools = getattr(server, "mcp", getattr(server, "server", None))._tool_manager._tools
|
|
assert name in tools
|
|
tool = tools[name]
|
|
call = getattr(tool, "fn", getattr(tool, "function", None))
|
|
assert callable(call)
|
|
tool_kwargs = dict(kwargs)
|
|
tool_kwargs.pop("db_path")
|
|
boundary_calls = []
|
|
def boundary(*args, **forwarded):
|
|
bound = inspect.signature(fn).bind(*args, **forwarded)
|
|
boundary_calls.append(dict(bound.arguments))
|
|
return marker
|
|
# Existing MCP contracts bind facade callables at import time. Test the
|
|
# actual MCP/contract boundary, not a later owner-module monkeypatch.
|
|
monkeypatch.setattr(contracts, name, boundary)
|
|
assert call(**tool_kwargs) == marker
|
|
assert boundary_calls[-1] == tool_kwargs
|
|
schema = contracts.TOOL_CONTRACTS[name]
|
|
assert callable(schema)
|
|
tool_schema = tool.parameters if hasattr(tool, "parameters") else tool.inputSchema
|
|
assert "db_path" not in str(tool_schema)
|
|
|
|
|
|
def test_readonly_reader_on_missing_path_does_not_create_database(tmp_path):
|
|
path = tmp_path / "absent.sqlite3"
|
|
result = _api(READER)(month="2026-09", db_path=str(path))
|
|
assert result.get("success") is False
|
|
assert not path.exists()
|
|
|
|
|
|
def test_dashboard_exposes_additive_policy_and_saved_history_without_implicit_mutation():
|
|
root = Path(__file__).parents[1]
|
|
drafts = root / "dashboard/page_views/settlement_drafts.py"
|
|
monthly = root / "dashboard/page_views/monthly_settlement.py"
|
|
source = drafts.read_text(encoding="utf-8")
|
|
monthly_source = monthly.read_text(encoding="utf-8")
|
|
combined = source + "\n" + monthly_source
|
|
assert "종합 월정산 정책" in combined
|
|
assert "저장된 월정산 스냅샷" in combined
|
|
assert POLICY in source or POLICY in monthly_source
|
|
assert READER in source or READER in monthly_source
|
|
assert "settlement_gateway" in combined
|
|
assert "sqlite3.connect" not in combined and "CREATE TABLE" not in combined
|
|
assert "form_submit_button" in source or "if st.button" in source
|
|
|
|
|
|
def test_authenticated_dashboard_entrypoint_keeps_authentication_before_database_resolution():
|
|
app = (Path(__file__).parents[1] / "dashboard/app.py").read_text(encoding="utf-8")
|
|
assert app.index("require_dashboard_authentication()") < app.index("get_db_path()")
|
|
|
|
|
|
def test_new_contracts_keep_asset_direct_close_signatures_and_live_report_api():
|
|
assets = importlib.import_module("jarca_ledger_mcp.application.assets_service")
|
|
report = importlib.import_module("jarca_ledger_mcp.application.transaction_report_services")
|
|
direct = inspect.signature(assets.close_asset_snapshot)
|
|
assert "cashflow_policy" not in direct.parameters
|
|
assert list(inspect.signature(report.get_monthly_cashflow_report).parameters)[0] == "month"
|
|
assert callable(_api(POLICY)) and callable(_api(READER))
|
|
|
|
|
|
def _draft_page(monkeypatch, status="active"):
|
|
from streamlit.testing.v1 import AppTest
|
|
from jarca_ledger_mcp.adapters.streamlit import settlement_gateway
|
|
policy = {"savings_categories": [{"category": "Savings", "subcategory": "Monthly"}], "category_decisions": []}
|
|
draft = {"id": "synthetic-draft", "month": "2026-09", "basis_date": "2026-09-30", "status": status, "version": 1, "cashflow_policy": policy, "final_run_id": "synthetic-run"}
|
|
final = {"income_total": 101, "expense_total": 0, "net_cashflow": 101, "total_assets": 0, "total_liabilities": 0, "net_worth": 0, "aggregation_version": "comprehensive-v1", "cashflow_items": [{"cashflow_kind": "income", "category": "Salary", "subcategory": "Monthly", "amount": 101, "transaction_count": 1, "currency": "KRW"}]}
|
|
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": "synthetic-fp"}, "final_response": final if status == "completed" else None})
|
|
page = AppTest.from_string("from dashboard.page_views.settlement_drafts import render_settlement_drafts\nrender_settlement_drafts(db_path='synthetic-unused.sqlite3', month='2026-09')", default_timeout=30).run()
|
|
assert not page.exception
|
|
return page, policy
|
|
|
|
|
|
def test_draft_policy_editor_uses_saved_draft_policy(monkeypatch):
|
|
import json
|
|
page, policy = _draft_page(monkeypatch)
|
|
assert json.loads(page.text_area[0].value) == policy, "rendering must not replace persisted policy with an empty default"
|
|
|
|
|
|
def test_completed_draft_page_displays_saved_response_and_cashflow_kind(monkeypatch):
|
|
page, _ = _draft_page(monkeypatch, "completed")
|
|
assert len(page.dataframe) == 1, "completed get exposes saved final_response at the response root"
|
|
frame = page.dataframe[0].value
|
|
assert "cashflow_kind" in frame.columns
|
|
assert frame.iloc[0]["cashflow_kind"] == "income"
|
|
assert sum(item.value == "### 불변 이력" for item in page.markdown) == 1
|
|
|
|
|
|
# Real transport/UI acceptance coverage. All identities and ledger values are synthetic.
|
|
def test_real_comprehensive_stdio_policy_close_and_saved_snapshot(tmp_path):
|
|
import asyncio, json, os, sqlite3, sys
|
|
from contextlib import closing
|
|
from mcp import ClientSession, StdioServerParameters
|
|
from mcp.client.stdio import stdio_client
|
|
from jarca_ledger_mcp.db import init_db
|
|
|
|
db = tmp_path / "stdio-comprehensive.sqlite3"
|
|
init_db(str(db))
|
|
category = service.create_ledger_category("Synthetic salary", "Monthly", cashflow_kind="income", db_path=str(db))
|
|
assert category["success"]
|
|
assert service.create_ledger_transaction("2026-09-05", "income", 20, "Synthetic salary", "Monthly", db_path=str(db))["success"]
|
|
assert service.create_ledger_category("Synthetic food", "Meal", cashflow_kind="expense", db_path=str(db))["success"]
|
|
assert service.create_ledger_transaction("2026-09-06", "expense", 10, "Synthetic food", "Meal", db_path=str(db))["success"]
|
|
assert service.create_ledger_category("Synthetic refund", "Meal", cashflow_kind="expense", db_path=str(db))["success"]
|
|
assert service.create_ledger_transaction("2026-09-07", "refund", 3, "Synthetic refund", "Meal", db_path=str(db))["success"]
|
|
assert service.create_ledger_category("Synthetic savings", "Reserve", cashflow_kind="investment", db_path=str(db))["success"]
|
|
assert service.create_ledger_transaction("2026-09-08", "expense", 5, "Synthetic savings", "Reserve", db_path=str(db))["success"]
|
|
acct = service.create_ledger_account("Synthetic checking", last_balance=100, liquidity_bucket="cash", db_path=str(db))["account"]["id"]
|
|
assert service.register_account_valuation(acct, "2026-09-30", 100, verification_status="verified", idempotency_key="stdio-valuation", db_path=str(db))["success"]
|
|
root = Path(__file__).resolve().parents[1]
|
|
# Deliberately construct a minimal environment; no caller auth/Gitea variables survive.
|
|
env = {k: os.environ[k] for k in ("PATH", "HOME", "LANG") if k in os.environ}
|
|
env.update(JARCA_LEDGER_DB_PATH=str(db), JARCA_MCP_TRANSPORT="stdio", PYTHONDONTWRITEBYTECODE="1")
|
|
params = StdioServerParameters(command=sys.executable, args=["-B", "-m", "jarca_ledger_mcp.server"], env=env, cwd=str(root))
|
|
|
|
async def exercise():
|
|
async with stdio_client(params) as (read, write):
|
|
async with ClientSession(read, write) as session:
|
|
await session.initialize()
|
|
tools = {t.name: t for t in (await session.list_tools()).tools}
|
|
for name in (POLICY, READER):
|
|
assert name in tools
|
|
schema = tools[name].inputSchema
|
|
assert "db_path" not in schema.get("properties", {})
|
|
assert set(schema["properties"]) == set(inspect.signature(getattr(service, name)).parameters) - {"db_path"}
|
|
async def call(name, args):
|
|
result = await session.call_tool(name, args)
|
|
assert not result.isError
|
|
return json.loads(result.content[0].text)
|
|
made = await call("create_settlement_draft", {"month":"2026-09", "created_by":"synthetic", "idempotency_key":"stdio-draft"})
|
|
draft = made["draft"]
|
|
policy = {"savings_categories":[{"category":"Synthetic savings", "subcategory":"Reserve"}], "category_decisions":[]}
|
|
changed = await call(POLICY, {"draft_id":draft["id"], "expected_version":draft["version"], "updated_by":"synthetic", "cashflow_policy":policy})
|
|
draft = changed["draft"]
|
|
assert draft["version"] == 2 and draft["cashflow_policy"] == policy
|
|
review = await call("review_settlement_draft", {"draft_id":draft["id"]})
|
|
for key in ("income_total", "expense_total", "net_cashflow", "total_assets", "total_liabilities", "net_worth"):
|
|
assert key in review
|
|
assert review["cashflow_items"] and all("cashflow_kind" in row for row in review["cashflow_items"])
|
|
assert any(t.get("applied_amount") is not None for t in review.get("source_snapshot", {}).get("transactions", []))
|
|
approved = await call("approve_settlement_draft", {"draft_id":draft["id"], "expected_version":draft["version"], "review_fingerprint":review["review_fingerprint"], "approved_by":"synthetic", "approval_reason":"acceptance", "approval_reference":"synthetic-ref"})
|
|
closed = await call("finalize_settlement_draft", {"draft_id":draft["id"], "expected_version":approved["draft"]["version"], "review_fingerprint":review["review_fingerprint"], "idempotency_key":"stdio-close"})
|
|
run = closed["run_id"]
|
|
exact = await call(READER, {"run_id":run})
|
|
monthly = await call(READER, {"month":"2026-09"})
|
|
assert exact["run_id"] == run and monthly["run_id"] == run
|
|
before = (await call("get_settlement_draft", {"draft_id":draft["id"]}))
|
|
assert await call("finalize_settlement_draft", {"draft_id":draft["id"], "expected_version":approved["draft"]["version"], "review_fingerprint":review["review_fingerprint"], "idempotency_key":"stdio-close"}) == closed
|
|
assert (await call("get_settlement_draft", {"draft_id":draft["id"]})) == before
|
|
assert await call(READER, {"run_id":run}) == exact
|
|
# Later ledger writes must not rewrite either saved surface.
|
|
service.create_ledger_category("Later", "Synthetic", cashflow_kind="income", db_path=str(db))
|
|
service.create_ledger_transaction("2026-09-20", "income", 99, "Later", "Synthetic", db_path=str(db))
|
|
assert await call(READER, {"run_id":run}) == exact
|
|
assert (await call("get_settlement_draft", {"draft_id":draft["id"]})) == before
|
|
return run
|
|
run = asyncio.run(asyncio.wait_for(exercise(), timeout=60))
|
|
with closing(sqlite3.connect(db)) as conn:
|
|
columns = {r[1] for r in conn.execute("PRAGMA table_info(asset_snapshot_runs)")}
|
|
assert {"income_total", "expense_total", "net_cashflow", "total_assets", "total_liabilities", "net_worth", "cashflow_policy_json", "aggregation_version", "snapshot_schema_version", "source_snapshot_json"} <= columns
|
|
assert conn.execute("SELECT COUNT(*) FROM settlement_cashflow_snapshots WHERE settlement_run_id=?", (run,)).fetchone()[0] > 0
|
|
|
|
|
|
def test_authenticated_dashboard_renders_saved_comprehensive_snapshot(tmp_path, monkeypatch):
|
|
import sqlite3
|
|
from streamlit.testing.v1 import AppTest
|
|
from dashboard import authentication
|
|
from jarca_ledger_mcp.db import init_db
|
|
db = tmp_path / "dashboard-comprehensive.sqlite3"
|
|
init_db(str(db))
|
|
assert service.create_ledger_category("Synthetic salary", "Monthly", cashflow_kind="income", db_path=str(db))["success"]
|
|
assert service.create_ledger_transaction("2026-09-05", "income", 20, "Synthetic salary", "Monthly", db_path=str(db))["success"]
|
|
asset = service.create_manual_asset("Synthetic cash", 100, "2026-09-30", "cash", db_path=str(db))
|
|
assert asset["success"]
|
|
monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(db))
|
|
monkeypatch.setenv(authentication.ADMIN_ID_ENV, "test-admin")
|
|
monkeypatch.setenv(authentication.ADMIN_PASSWORD_ENV, "test-password")
|
|
monkeypatch.setenv(authentication.SIGNING_SECRET_ENV, "s" * 64)
|
|
settings = authentication.load_auth_settings()
|
|
assert settings is not None
|
|
token = authentication.issue_persistent_token(settings, remember_me=False).value
|
|
monkeypatch.setattr(authentication, "_request_cookie", lambda: token)
|
|
draft = service.create_settlement_draft("2026-09", "synthetic", "dashboard-draft", db_path=str(db))["draft"]
|
|
policy = {"savings_categories":[{"category":"Synthetic savings", "subcategory":"Reserve"}], "category_decisions":[]}
|
|
changed = service.set_settlement_cashflow_policy(draft["id"], draft["version"], "synthetic", policy, db_path=str(db))["draft"]
|
|
reviewed = service.review_settlement_draft(draft["id"], db_path=str(db))
|
|
assert reviewed["success"]
|
|
assert service.approve_settlement_draft(draft["id"], changed["version"], reviewed["review_fingerprint"], "synthetic", "acceptance", "synthetic-ref", db_path=str(db))["success"]
|
|
saved = service.finalize_settlement_draft(draft["id"], changed["version"], reviewed["review_fingerprint"], "dashboard-close", db_path=str(db))
|
|
assert saved["success"]
|
|
with sqlite3.connect(db) as conn:
|
|
before = conn.execute("SELECT name, sql FROM sqlite_master ORDER BY type,name").fetchall()
|
|
before_rows = conn.execute("SELECT * FROM asset_snapshot_runs ORDER BY id").fetchall()
|
|
before_detail = conn.execute("SELECT * FROM settlement_cashflow_snapshots ORDER BY id").fetchall()
|
|
app = AppTest.from_file(str(Path(__file__).resolve().parents[1] / "dashboard" / "app.py"), default_timeout=30)
|
|
app.query_params["page"] = "월정산"
|
|
app.query_params["month"] = "2026-09"
|
|
app.run()
|
|
assert not app.exception, app.exception
|
|
body = "\n".join(str(x.value) for x in list(app.text) + list(app.markdown) + list(app.caption))
|
|
assert "저장된 월정산 스냅샷" in body and f"run {saved['run_id']}" in body
|
|
assert any(metric.label == "자산 합계" for metric in app.metric)
|
|
frames = [item.value for item in app.dataframe]
|
|
assert any("cashflow_kind" in frame.columns and "income" in set(frame["cashflow_kind"]) for frame in frames)
|
|
with sqlite3.connect(db) as conn:
|
|
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_saved_monthly_panel_renders_real_conditional_exclusion_carry_and_approval(schema_thirteen_conditional_close, monkeypatch): # noqa: F811 - imported pytest fixture
|
|
import json
|
|
from streamlit.testing.v1 import AppTest
|
|
from jarca_ledger_mcp.db import init_db
|
|
from jarca_ledger_mcp.adapters.streamlit import overview_gateway, reconciliation_gateway, settlement_gateway
|
|
|
|
path, missing, carried, closed, _ = schema_thirteen_conditional_close
|
|
# Migrate the authentic schema-13 baseline in place, then exercise the real
|
|
# service through its public Streamlit gateway; no saved response is mocked.
|
|
init_db(path)
|
|
snapshot = settlement_gateway.get_monthly_settlement_snapshot(run_id=closed["snapshot_run_id"], db_path=path)
|
|
assert snapshot["success"]
|
|
excluded = next(row for row in snapshot["excluded_items"] if row["source_id"] == missing)
|
|
carry = next(row for row in snapshot["carried_items"] if row["source_id"] == carried)
|
|
approval = snapshot["approval"]
|
|
assert approval
|
|
|
|
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, "list_settlement_drafts", lambda *a, **k: {"success": True, "drafts": []})
|
|
app = AppTest.from_string(
|
|
f"import streamlit as st\n"
|
|
f"st.query_params.update(page='월정산', month='2026-09')\n"
|
|
f"from dashboard.page_views.monthly_settlement import page_monthly_settlement\n"
|
|
f"page_monthly_settlement({path!r})", default_timeout=30).run()
|
|
assert not app.exception, f"saved panel AppTest failed before field assertions: {app.exception}"
|
|
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 missing in body and excluded["reason"] in body, "saved panel must render excluded_items source_id and reason"
|
|
assert carried in body and carry["valuation_date"] in body, "saved panel must render carried_items source_id and historical valuation_date"
|
|
for key in ("approved_by", "approval_reason", "approval_reference"):
|
|
assert approval[key] in body, f"saved panel must render approval.{key} from the saved response"
|
|
|
|
|
|
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(
|
|
"import streamlit as st\n"
|
|
"st.query_params.update(page='월정산', month='2026-09')\n"
|
|
"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
|