from __future__ import annotations import json import re from pathlib import Path ROOT = Path(__file__).parents[1] DOCS = ROOT / "docs" REQ = DOCS / "requirements" / "req-003_account-valuation-history.md" def _text(path: str) -> str: return (ROOT / path).read_text(encoding="utf-8") def test_requirement_docs_are_canonical_and_traceable() -> None: text = REQ.read_text(encoding="utf-8") _, raw, body = text.split("---", 2) metadata = json.loads(raw) assert metadata == { "schema": "gitea-agent.requirement/v1", "id": "REQ-003", "title": "계좌 기준일 평가 이력과 자산 마감 연동", "revision": "1.1", "status": "approved", "depends_on": [], "applies_to": ["ledger", "dashboard", "mcp"], "language": "ko", "issue": "#15", "implementation_pr": "미정", } assert re.search(r"^# REQ-003 — ", body, re.M) assert "${" not in text requirements = re.findall(r"^### REQ-003\.(\d+) —", body, re.M) assert requirements == [f"{n:02d}" for n in range(1, 12)] acs = re.findall(r"^### REQ-003\.\d+-AC(\d+) —", body, re.M) assert len(acs) == 27 baseline_acs = re.findall(r"^### REQ-003\.(0[1-9]|10)-AC01 —", body, re.M) assert baseline_acs == [f"{n:02d}" for n in range(1, 11)] assert body.count("**영향 범위**") == 11 # Impact is per requirement, not per AC. assert body.count("**Cases (정상/경계/실패)**") == 10 assert body.count("**State Invariants**") == 10 assert body.count("**판정 기준**") == 27 assert body.count("**검증**") == 27 classification = re.findall( r"^\*\*검증 분류\*\*: level=(unit|integration|static); backend=(sqlite|application|dashboard)$", body, re.M, ) assert len(classification) == 10 assert body.count("- 수준: integration") == 17 assert len(re.findall(r"^- 백엔드: (sqlite|application|dashboard)$", body, re.M)) == 17 assert {level for level, _ in classification} >= {"unit", "integration", "static"} assert {backend for _, backend in classification} >= {"sqlite", "application", "dashboard"} for marker in ( "v12", "idempotent", "append-only", "future", "unverified", "unknown", "idempotency_conflict", "correction_conflict", "NULL", "VALUATION_MANAGED", "conditional_approval_required", "MCP", "Dashboard", "auth-before-DB", "read-only", "monthly", "synthetic", "실제 credential", "호환성", "reported principal amount", "outstanding principal", "interest", "fee", "same account/date/currency/purpose", "correction_reason", "no-write", "source_ref", "evidence idempotency", "cannot roll back newer current cache", "incomplete_draft", "explicit dated manual asset", "future unknown", "reconciliation completeness", "unavailable investment", "loan purpose", "no fake writes", "provenance", "writer", "bootstrap", "init_db", "read_db", "mode=ro", "db_not_initialized", "no mkdir", "no migrate", "no commit", ): assert marker in text, marker def test_canonical_docs_have_required_sections_and_no_placeholders() -> None: paths = [ "docs/data-model.md", "docs/interface-contract.md", "docs/project-structure.md", "docs/runtime-flow.md", ] for rel in paths: text = _text(rel) assert "${" not in text, rel data = _text("docs/data-model.md") for marker in ( "## 목적과 범위", "## 정본 엔터티", "## 관계와 불변조건", "## 저장과 마이그레이션", "schema 12", "account-valuations", "asset-snapshot-runs", "reported principal amount", "outstanding principal", "NULL", "0", "source_ref", "evidence idempotency", "correction parent", "same account/date/currency/purpose", "provenance", "reconciliation completeness", "unavailable investment", "loan purpose", "no fake writes", ): assert marker in data interface = _text("docs/interface-contract.md") for marker in ("## 목적과 범위", "## 계약", "## 호환성", "## 예시", "## 검증", "REQ-003", "register_account_valuation", "preview_asset_snapshot_close", "conditional_approval_required"): assert marker in interface structure = _text("docs/project-structure.md") for marker in ("## 목적과 경계", "## 디렉터리 구조", "## 의존 방향", "## 변경 규칙", "## 검증", "account_valuation_service.py", "assets_service.py", "Missing semantic items: 0", "Ambiguous semantic items: 0"): assert marker in structure runtime = _text("docs/runtime-flow.md") for marker in ("## 목적과 범위", "## 흐름", "## 실패와 복구", "### 동시성과 순서", "## 관찰 가능성", "## 검증", "valuation-register", "snapshot-preview-close", "close-04", "prop-03"): assert marker in runtime def test_documented_paths_and_symbols_exist() -> None: paths = [ "jarca_ledger_mcp/application/account_valuation_service.py", "jarca_ledger_mcp/application/assets_service.py", "jarca_ledger_mcp/application/accounts_write_services.py", "jarca_ledger_mcp/application/imports_service.py", "jarca_ledger_mcp/application/reconciliation_service.py", "jarca_ledger_mcp/infrastructure/sqlite/migrations.py", "jarca_ledger_mcp/adapters/mcp/server.py", "jarca_ledger_mcp/adapters/streamlit/accounts_gateway.py", "jarca_ledger_mcp/adapters/streamlit/assets_gateway.py", "dashboard/page_views/accounts.py", "dashboard/page_views/assets.py", "tests/test_account_valuations.py", "tests/test_asset_snapshots.py", "tests/test_contract_regression.py", ] for rel in paths: assert (ROOT / rel).exists(), rel service = _text("jarca_ledger_mcp/application/account_valuation_service.py") for symbol in ("def register_account_valuation(", "def list_account_valuations(", "def select_account_valuation(", "def correct_account_valuation("): assert symbol in service assets = _text("jarca_ledger_mcp/application/assets_service.py") for symbol in ("def preview_asset_snapshot_close(", "def close_asset_snapshot(", "def get_asset_snapshot_report("): assert symbol in assets migrations = _text("jarca_ledger_mcp/infrastructure/sqlite/migrations.py") for marker in ("CREATE TABLE IF NOT EXISTS account_valuations", "CREATE TABLE IF NOT EXISTS asset_snapshot_runs", "SCHEMA_VERSION"): assert marker in migrations def test_existing_auth_menu_evidence_inventory_is_preserved() -> None: interface = _text("docs/interface-contract.md") runtime = _text("docs/runtime-flow.md") structure = _text("docs/project-structure.md") for marker in ( "POST /api/auth/login", "GET /api/auth/cleanup", "POST /api/auth/logout", "require_dashboard_authentication()", "__Host-jarca_dashboard_auth", "월정산", "요약", "자산/마감", "거래내역", "확인필요 큐", "미분류", "분류 규칙", "카테고리", "계좌", "Import", "st.query_params[\"page\"]", "streamlit_browser_evidence.py", "auth-before-DB", "cookie/cleanup/logout", ): assert marker in interface + runtime + structure, marker def test_req003_has_exact_canonical_numbered_sections() -> None: body = REQ.read_text(encoding="utf-8").split("---", 2)[2] headings = re.findall(r"^## (\d+)\. ", body, re.M) assert headings == [str(n) for n in range(1, 12)] assert not re.search(r"^## (?:8|9)\.(?: D| C)", body, re.M) assert body.count("### REQ-003.") == 38 assert "### REQ-003.01-AC01" in body assert "### REQ-003.10-AC01" in body def test_d01_d06_semantics_are_explicit_and_canonical() -> None: req = REQ.read_text(encoding="utf-8") data = _text("docs/data-model.md") interface = _text("docs/interface-contract.md") runtime = _text("docs/runtime-flow.md") all_docs = req + data + interface + runtime for marker in ( "principal", "reported_principal_kind", "reported_principal_label", "no generic label", "parent scope", "cyclic", "self parent", "one child", "reason", "no-write", "account-scoped evidence identity", "explicit key", "key/source mismatch", "last_balance", "external current comparison", "opening evidence", "INCOMPLETE_CALCULATION", "investment unavailable", "loan purpose", "loan definition", "ambiguity", "actual valuation_date", "age", "carry", "verification", "run", "approval", "correction", "warnings", "exclusions", "close response", "closed", "live", "timeseries", "monthly", "monthly_source_snapshot_id", "read-model alias", "not a persistence field", "init_db()", "read_db()", "mode=ro", "db_not_initialized", "no mkdir", "no migrate", "no commit", ): assert marker in all_docs, marker assert "generic `label`" not in all_docs assert "`monthly_source_snapshot_id`" in all_docs assert "missing database returns `db_not_initialized`" in interface assert "initialized database read failures return `db_error`" in interface assert "top-level `approval`, `correction`, `warnings`, `excluded_items`, and `carried_items`" in interface assert "nested `provenance`" in interface def test_documented_functions_fields_and_regression_tests_are_real() -> None: import ast expected = { "jarca_ledger_mcp/application/account_valuation_service.py": { "register_account_valuation", "list_account_valuations", "select_account_valuation", "correct_account_valuation", }, "jarca_ledger_mcp/application/assets_service.py": { "preview_asset_snapshot_close", "close_asset_snapshot", "get_asset_snapshot_report", }, } structure = _text("docs/project-structure.md") interface = _text("docs/interface-contract.md") runtime = _text("docs/runtime-flow.md") for rel, names in expected.items(): tree = ast.parse((ROOT / rel).read_text(encoding="utf-8")) actual = {n.name: n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)} assert set(names) <= set(actual), rel for name in names: documented = interface node = actual[name] parameter_names = [a.arg for a in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs)] if node.args.vararg: parameter_names.append(node.args.vararg.arg) if node.args.kwarg: parameter_names.append(node.args.kwarg.arg) signature_line = next((line for line in documented.splitlines() if line.startswith(name + "(") and ": " in line), "") assert signature_line, name assert all(param in signature_line for param in parameter_names), name assert "jarca_ledger_mcp/application/asset_account_services.py" in structure assert (ROOT / "jarca_ledger_mcp/application/asset_account_services.py").exists() tests = "\n".join(p.read_text(encoding="utf-8") for p in (ROOT / "tests").glob("test_issue_12_review59_*.py")) for name in ( "test_valuation_round_trip_preserves_null_zero_and_physical_amount_mapping", "test_source_ref_is_account_scoped_evidence_identity_without_explicit_key", "test_direct_correction_requires_matching_existing_parent_and_reason", "test_investment_without_verified_valuation_and_without_opening_balance_is_incomplete", "test_timeseries_and_monthly_expose_canonical_asset_provenance", "test_read_queries_missing_nested_db_return_stable_error_without_creating_path", ): assert f"def {name}(" in tests, name for marker in ( "success", "error_code", "error", "close_status", "warnings", "exclusions", "snapshot_run_id", "valuation_date", "monthly_source_snapshot_id", ): assert marker in interface + runtime def test_d01_preservation_matrix_is_item_by_item_and_evidenced() -> None: structure = _text("docs/project-structure.md") required_items = { "schema 11 legacy rows", "auth gate and cookie lifecycle", "REQ-002 menu labels/order", "MCP envelope and tools", "reconciliation and monthly read models", } matrix = structure.split("## D01 preservation matrix", 1)[1].split("## ", 1)[0] assert "| item | source evidence | test evidence |" in matrix for item in required_items: rows = [line for line in matrix.splitlines() if item in line] assert len(rows) == 1, item assert rows[0].count("|") >= 4, item assert "`" in rows[0], item def test_d04_close_contract_enumerates_all_fields_and_aliases() -> None: interface = _text("docs/interface-contract.md") section = interface.split("## D04 snapshot close output contract", 1)[1].split("## ", 1)[0] top_level = { "snapshot_run_id", "close_status", "approval", "correction", "warnings", "excluded_items", "carried_items", "provenance", "valuation_date", "age_days", "verification_status", "evidence_kind", "monthly_source_snapshot_id", } nested = {"approval", "correction", "warnings", "excluded_items", "carried_items", "valuation_date", "age_days", "verification_status", "evidence_kind"} for field in top_level: assert f"`{field}`" in section, field assert "top-level fields" in section assert "nested `provenance` fields" in section for field in nested: assert f"`{field}`" in section, field assert "alias" in section and "not a persistence field" in section def test_d05_structure_maps_canonical_ownership_and_boundaries() -> None: structure = _text("docs/project-structure.md") section = structure.split("## D05 canonical ownership", 1)[1].split("## ", 1)[0] required = { "monthly settlement", "reconciliation", "policy", "read-only", "redaction", "monthly_settlement_service.py", "reconciliation_service.py", "assets_service.py", } for marker in required: assert marker in section, marker assert "owner" in section and "boundary" in section assert "preview" in section and "no write" in section def test_d06_runtime_flow_states_cache_reconciliation_and_preview_guards() -> None: runtime = _text("docs/runtime-flow.md") section = runtime.split("## D06 runtime decision guards", 1)[1].split("## ", 1)[0] for marker in ( "current cache", "no rollback", "reconciliation complete", "reconciliation unavailable", "preview", "drift", "revalidation", "close", "no partial write", ): assert marker in section, marker assert "complete" in section and "unavailable" in section def test_d04_contract_matches_final_close_and_replay_implementation() -> None: interface = _text("docs/interface-contract.md") req = _text("docs/requirements/req-003_account-valuation-history.md") section = interface.split("## D04 snapshot close output contract", 1)[1].split("## ", 1)[0] for marker in ( "replay payload equality", "no replay-only reused", "recursive close_metadata allowlist", "redaction", "source_type", "source_id", "authority_source", "valuation_id", "state_hash", "applied_evidence", "excluded_evidence", "carried_evidence", ): assert marker in section + req, marker assert "replay response is reconstructed from the persisted run" in section assert "unknown nested keys are dropped" in section def test_d04_preview_fingerprint_signature_and_drift_are_exact() -> None: interface = _text("docs/interface-contract.md") runtime = _text("docs/runtime-flow.md") section = interface.split("## D04 snapshot close output contract", 1)[1].split("## ", 1)[0] assert "preview_fingerprint: str = \"\"" in interface assert "preview input includes basis_date, selection_policy, included/excluded evidence state hashes, warnings, and candidate_state_hashes" in section assert "preview output includes preview_fingerprint" in section assert "authority_drift" in section + runtime assert "no partial write" in section + runtime def test_d05_dashboard_read_contract_and_d06_reconciliation_evidence_are_exact() -> None: interface = _text("docs/interface-contract.md") structure = _text("docs/project-structure.md") runtime = _text("docs/runtime-flow.md") req = _text("docs/requirements/req-003_account-valuation-history.md") all_docs = interface + structure + runtime + req for marker in ( "missing DB stable unavailable", "no-create", "initialized DB read failure is db_error", "initialized read has no write", "opening evidence", "coverage", "INCOMPLETE_CALCULATION", "reconciliation unavailable", "no inferred balance", "no fake transaction", ): assert marker in all_docs, marker assert "Dashboard gateway" in interface + structure assert "read_db()" in interface and "mode=ro" in interface