from __future__ import annotations import sqlite3 from jarca_ledger_mcp.db import init_db from dashboard.page_views import monthly_settlement as view class _FakeStreamlit: def __init__(self, query=None): self.query_params = dict(query or {}) self.session_state = {} self.dataframes = [] self.errors = [] self.headers = [] self.titles = [] self.buttons = [] self.markdowns = [] def header(self, value): self.headers.append(value) def title(self, value): self.titles.append(value) def caption(self, value): pass def markdown(self, value, **kwargs): self.markdowns.append(value) def write(self, value): self.markdowns.append(str(value)) def selectbox(self, label, options, **kwargs): return options[0] def info(self, value): pass def error(self, value): self.errors.append(value) def dataframe(self, value, **kwargs): self.dataframes.append((value, kwargs)) class _Column: def __enter__(self): return self def __exit__(self, *args): return False def button(self, label, **kwargs): return False def popover(self, *args, **kwargs): return self def columns(self, spec): return [self._Column() for _ in spec] def container(self, **kwargs): return self._Column() def button(self, label, **kwargs): self.buttons.append((label, kwargs)) return False def popover(self, *args, **kwargs): return self._Column() def link_button(self, label, url, **kwargs): self.buttons.append((label, {"url": url, **kwargs})) return False def rerun(self): return None def _fixture_db(tmp_path): path = tmp_path / "monthly-navigation.sqlite3" init_db(path) with sqlite3.connect(path) as conn: conn.execute( """INSERT INTO monthly_settlement (id, month, basis_date, status, version, created_by, created_at, updated_by, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", ("settlement-2026-09", "2026-09", "2026-09-30", "draft", 1, "test", "2026-09-30T00:00:00Z", "test", "2026-09-30T00:00:00Z"), ) conn.commit() return path def test_month_text_link_uses_exact_canonical_id_and_direct_detail_reads_month(tmp_path, monkeypatch): db_path = _fixture_db(tmp_path) fake = _FakeStreamlit() monkeypatch.setattr(view, "st", fake) monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: { "success": True, "months": [{"settlement_id": "settlement-2026-09", "month": "2026-09", "status_label": "초안", "income": 100}], }) view._render_monthly_list(str(db_path)) month_buttons = [item for item in fake.buttons if item[0] == "2026-09"] assert len(month_buttons) == 1 assert month_buttons[0][1]["type"] == "tertiary" assert month_buttons[0][1]["on_click"] is view._set_monthly_detail assert month_buttons[0][1]["args"] == ("settlement-2026-09", "전체", "전체") assert "?page=monthly_settlement" not in "\n".join(fake.markdowns) month_buttons[0][1]["on_click"](*month_buttons[0][1]["args"]) assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-09"} assert not fake.dataframes detail_fake = _FakeStreamlit({"page": "monthly_settlement", "id": "settlement-2026-09"}) monkeypatch.setattr(view, "st", detail_fake) calls = [] monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: calls.append((db, month))) view.page_monthly_settlement(str(db_path)) assert calls == [(str(db_path), "2026-09")] assert detail_fake.titles == ["월정산 상세 · 2026-09"] assert [label for label, _ in detail_fake.buttons] == ["← 월정산 목록", "검토승인"] def test_unregistered_month_selection_does_not_create_none_link(monkeypatch): fake = _FakeStreamlit() monkeypatch.setattr(view, "st", fake) monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: { "success": True, "months": [{"settlement_id": None, "month": "2026-08", "status_label": "정산 미생성·상세 선택 불가·현재집계(미확정)"}], }) view._render_monthly_list("fixture.sqlite3") rendered = "\n".join(fake.markdowns) assert "2026-08" in rendered assert "[2026-08]" not in rendered assert "?page=monthly_settlement&id=None" not in rendered def test_direct_reload_unknown_id_does_not_fallback_to_month_or_list(tmp_path, monkeypatch): db_path = _fixture_db(tmp_path) fake = _FakeStreamlit({"page": "monthly_settlement", "id": "unknown-id", "month": "2026-09"}) monkeypatch.setattr(view, "st", fake) calls = [] monkeypatch.setattr(view, "_render_monthly_list", lambda db: calls.append(("list", db))) view.page_monthly_settlement(str(db_path)) assert calls == [] assert fake.errors == ["월정산을 찾을 수 없습니다."] def test_legacy_month_query_rewrites_to_canonical_id(tmp_path, monkeypatch): db_path = _fixture_db(tmp_path) fake = _FakeStreamlit({"page": "monthly_settlement", "month": "2026-09"}) monkeypatch.setattr(view, "st", fake) monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: { "success": True, "months": [{"settlement_id": "settlement-2026-09", "month": "2026-09", "status_label": "초안"}], }) monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: None) view.page_monthly_settlement(str(db_path)) assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-09"} def test_back_to_list_uses_canonical_page_and_preserves_filters(monkeypatch): fake = _FakeStreamlit({"page": "monthly_settlement", "id": "settlement-2026-09", "year": "2026", "status": "미마감"}) monkeypatch.setattr(view, "st", fake) view._clear_monthly_detail("2026", "미마감") assert fake.query_params == {"page": "monthly_settlement", "year": "2026", "status": "미마감"} assert fake.session_state["dashboard_page"] == "월정산" def test_add_button_opens_dialog_and_navigates_to_created_canonical_id(monkeypatch): class DialogFake(_FakeStreamlit): def __init__(self): super().__init__() self.reruns = 0 def dialog(self, title): assert title == "월정산 추가" return lambda fn: fn def text_input(self, label, **kwargs): assert label == "대상 월" return "2026-10" def button(self, label, **kwargs): self.buttons.append((label, kwargs)) return label in {"+", "추가"} def rerun(self): self.reruns += 1 fake = DialogFake() monkeypatch.setattr(view, "st", fake) monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {"success": True, "months": []}) monkeypatch.setattr(view.settlement_gateway, "create_monthly_settlement", lambda month, **kwargs: {"success": True, "settlement_id": "settlement-2026-10"}) view._render_monthly_list("fixture.sqlite3") assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-10"} assert fake.reruns == 1 def test_month_link_target_contains_no_write_operation(): source = view._render_monthly_list.__code__.co_names assert "create_settlement_draft" not in source assert "finalize_settlement_draft" not in source def test_back_button_single_click_clears_detail_query_and_reruns(monkeypatch): class ClickFake(_FakeStreamlit): def __init__(self): super().__init__({"page": "monthly_settlement", "id": "settlement-2026-09", "month": "2026-09", "year": "2026", "status": "미마감"}) self.session_state.update({"dashboard_page": "월정산", "monthly_settlement_detail_id": "settlement-2026-09", "monthly_settlement_detail_month": "2026-09"}) self.reruns = 0 def rerun(self): self.reruns += 1 fake = ClickFake() monkeypatch.setattr(view, "st", fake) view._clear_monthly_detail("2026", "미마감") assert fake.reruns == 1 assert fake.query_params == {"page": "monthly_settlement", "year": "2026", "status": "미마감"} assert "monthly_settlement_detail_id" not in fake.session_state assert "monthly_settlement_detail_month" not in fake.session_state def test_next_rerun_without_detail_query_does_not_restore_detail(monkeypatch): fake = _FakeStreamlit({"page": "monthly_settlement", "year": "2026", "status": "미마감"}) fake.session_state.update({"dashboard_page": "월정산"}) monkeypatch.setattr(view, "st", fake) rendered = [] monkeypatch.setattr(view, "_render_monthly_list", lambda db: rendered.append(db)) monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: (_ for _ in ()).throw(AssertionError("detail restored"))) view.page_monthly_settlement("fixture.sqlite3") assert rendered == ["fixture.sqlite3"]