118 lines
5.6 KiB
Python
118 lines
5.6 KiB
Python
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.0",
|
|
"status": "approved",
|
|
"depends_on": [],
|
|
"applies_to": ["ledger", "dashboard", "mcp"],
|
|
"language": "ko",
|
|
"issue": "#12",
|
|
"implementation_pr": "#13",
|
|
}
|
|
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, 11)]
|
|
acs = re.findall(r"^### REQ-003\.\d+-AC(\d+) —", body, re.M)
|
|
assert len(acs) == 10
|
|
assert body.count("**영향 범위**") == 10
|
|
assert body.count("**Cases (정상/경계/실패)**") == 10
|
|
assert body.count("**State Invariants**") == 10
|
|
assert body.count("**판정 기준**") == 10
|
|
assert body.count("**검증**") == 10
|
|
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", "호환성",
|
|
):
|
|
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"):
|
|
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
|