"""Dashboard lifecycle contract tests using Streamlit AppTest when available. All data is synthetic; renders must not mutate/create state or databases. This suite deliberately asserts the Issue 15 lifecycle UI is present before it exists. """ from __future__ import annotations from pathlib import Path import pytest VIEW = Path(__file__).parents[1] / "dashboard" / "page_views" / "settlement_drafts.py" def _app_wrapper(tmp_path: Path) -> Path: wrapper = tmp_path / "settlement_panel_app.py" wrapper.write_text( "import os\nfrom dashboard.page_views.settlement_drafts import render_settlement_drafts\n" "render_settlement_drafts(db_path=os.environ.get(\"JARCA_LEDGER_DB_PATH\"))\n", encoding="utf-8", ) return wrapper def test_reusable_settlement_draft_panel_exists_and_is_importable(tmp_path): assert VIEW.is_file(), "shared Dashboard settlement draft panel is missing" from streamlit.testing.v1 import AppTest app = AppTest.from_file(str(_app_wrapper(tmp_path))).run() assert not app.exception def test_monthly_and_assets_pages_invoke_shared_panel(): root = VIEW.parents[1] pages = [p for p in root.rglob("*.py") if p.name in ("monthly_settlement.py", "assets.py")] assert len(pages) == 2 for page in pages: assert "render_settlement_drafts" in page.read_text(encoding="utf-8") def test_streamlit_panel_renders_lifecycle_without_writing_or_creating_db(monkeypatch, tmp_path): pytest.importorskip("streamlit.testing.v1") from streamlit.testing.v1 import AppTest db_path = tmp_path / "dashboard-missing.sqlite3" # Explicit synthetic DB path and no authenticated actor: render only. monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(db_path)) app = AppTest.from_file(str(_app_wrapper(tmp_path))).run() assert not app.exception page = "\n".join([e.value for e in app.markdown] + [e.value for e in app.caption] + [e.value for e in app.info] + [e.label for e in app.button]) for expected in ("초안", "검토", "승인", "최종", "취소", "정정", "평가자료", "제외 사유"): assert expected in page assert not db_path.exists(), "render-only Dashboard created a database" def test_panel_actions_are_explicit_and_selection_state_bound_to_draft_version(): source = VIEW.read_text(encoding="utf-8") for action in ("create_settlement_draft", "update_settlement_draft", "cancel_settlement_draft", "refresh_settlement_draft_targets", "set_settlement_draft_item", "review_settlement_draft", "approve_settlement_draft", "finalize_settlement_draft", "register_account_valuation", "list_account_valuations", "correct_account_valuation"): assert action in source, f"missing explicit action {action}" assert "session_state" in source assert "review_fingerprint" in source assert "expected_version" in source or "version, actor" in source assert "transaction" in source.lower() and "거래" in source def test_closed_draft_view_exposes_immutable_evidence_and_correction_creation(): source = VIEW.read_text(encoding="utf-8") for immutable in ("draft_version", "review_fingerprint", "final_run_id", "correction_of_run_id", "basis_date", "created_by", "created_at", "updated_at", "memo"): assert immutable in source, f"closed view omits {immutable}" assert "correction_reason" in source assert "completion" in source or "completed" in source def test_render_path_contains_no_implicit_lifecycle_mutations(): source = VIEW.read_text(encoding="utf-8") # All lifecycle mutations must be behind explicit submit/button branches; # import/render must never invoke them unconditionally. assert "if st.button" in source or "form_submit_button" in source assert "render_settlement_drafts" in source def test_app_test_create_review_approve_finalize_roundtrip(tmp_path, monkeypatch): from streamlit.testing.v1 import AppTest from jarca_ledger_mcp.db import init_db from jarca_ledger_mcp.application.assets_service import create_manual_asset db_path = tmp_path / "lifecycle.sqlite3" init_db(str(db_path)) seeded = create_manual_asset("Synthetic asset", 125, "2026-09-30", "other", source_note="fixture", db_path=str(db_path)) assert seeded.get("success"), seeded monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(db_path)) app = AppTest.from_file(str(_app_wrapper(tmp_path))).run() assert not app.exception app.button[0].click().run() assert not app.exception assert app.button[0].label == "초안 생성 / 정정 초안" assert len(app.selectbox[0].options) == 1 assert "active" in " ".join(item.value for item in app.caption) # Exercise read-only review through the rendered explicit button. review = next(button for button in app.button if button.label == "검토 / 새로고침 대상") review.click().run(timeout=10) assert not app.exception assert any("review_fingerprint" in item.value for item in app.caption) assert not app.exception