"""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 legacy_source = (Path(__file__).parents[1] / "dashboard/page_views/legacy_monthly_settlement.py").read_text() combined += legacy_source monthly_source += legacy_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