114 lines
5.4 KiB
Python
114 lines
5.4 KiB
Python
"""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)))
|
|
app.session_state["authenticated_actor"] = "fixture-operator"
|
|
app = app.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)))
|
|
app.session_state["authenticated_actor"] = "fixture-operator"
|
|
app = app.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))
|
|
monkeypatch.setattr("dashboard.authentication.get_authenticated_actor", lambda: "fixture-operator")
|
|
app = AppTest.from_file(str(_app_wrapper(tmp_path)))
|
|
app.session_state["authenticated_actor"] = "fixture-operator"
|
|
app = app.run()
|
|
assert not app.exception
|
|
app.button[0].click().run()
|
|
assert not app.exception
|
|
next(button for button in app.button if button.label == "초안 만들기").click().run()
|
|
assert not app.exception
|
|
assert any(button.label == "검토 / 새로고침 대상" for button in app.button)
|
|
# 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
|