from __future__ import annotations import json from pathlib import Path from typing import Any import pytest from jarca_ledger_mcp import service SNAPSHOT_PATH = Path(__file__).parent / "contract_snapshots" / "response_contract_v1_0.json" TYPE_MAP = { "bool": bool, "dict": dict, "float": float, "int": int, "list": list, "none": type(None), "str": str, } def _load_contract() -> dict[str, Any]: with SNAPSHOT_PATH.open(encoding="utf-8") as f: return json.load(f) def _assert_type(value: Any, spec: str | list[str], path: str) -> None: names = spec if isinstance(spec, list) else [spec] expected = tuple(TYPE_MAP[name] for name in names) # bool is a subclass of int; contract int should not silently accept bool. if "int" in names and isinstance(value, bool): pytest.fail(f"{path} expected int-compatible contract type, got bool") assert isinstance(value, expected), f"{path} expected {names}, got {type(value).__name__}" def _assert_subset_schema(payload: dict[str, Any], schema: dict[str, Any], path: str) -> None: for key, spec in schema.get("required", {}).items(): assert key in payload, f"{path}.{key} missing from response contract" value = payload[key] if isinstance(spec, dict): _assert_type(value, spec["type"], f"{path}.{key}") if spec.get("type") == "dict": _assert_subset_schema(value, spec, f"{path}.{key}") else: _assert_type(value, spec, f"{path}.{key}") for list_key, item_schema in schema.get("items", {}).items(): items = payload.get(list_key, []) assert isinstance(items, list), f"{path}.{list_key} expected list for item schema" for index, item in enumerate(items): assert isinstance(item, dict), f"{path}.{list_key}[{index}] expected dict" _assert_subset_schema(item, item_schema, f"{path}.{list_key}[{index}]") def _build_contract_fixture(db_path: str) -> None: account = service.create_ledger_account( "Synthetic Checking", type="bank", institution="Synthetic Bank", account_number_masked="SYN-0001", last_balance=1_000_000, last_synced_at="2026-06-30T12:00:00+09:00", statement_status="manual_only", liquidity_bucket="cash", db_path=db_path, ) assert account["success"] is True account_id = account["account"]["id"] assert service.register_account_valuation( account_id, "2026-06-30", 1_000_000, verification_status="verified", source_type="manual", source_ref="contract-fixture", idempotency_key="contract-account-v1", db_path=db_path, )["success"] is True assert service.create_ledger_transaction( "2026-06-01", "income", 3_000_000, category="μˆ˜μž…", merchant="Synthetic Employer", account_id=account_id, db_path=db_path, )["success"] is True assert service.create_ledger_transaction( "2026-06-02", "expense", 50_000, category="식비", merchant="Synthetic Market", account_id=account_id, db_path=db_path, )["success"] is True def test_response_contract_metadata_versioning_policy() -> None: contract = _load_contract() assert contract["contract_version"] == "1.0" assert contract["runtime_exposure"] == "unexposed" assert "remove_required_key" in contract["policy"]["breaking_changes"] assert "add_optional_field" in contract["policy"]["additive_changes"] def test_health_and_status_success_response_contract(tmp_path: Path) -> None: contract = _load_contract()["scopes"] db_path = str(tmp_path / "contract-fixture.sqlite3") _build_contract_fixture(db_path) health = service.health_check(db_path) assert health["success"] is True _assert_subset_schema(health, contract["health"], "health") status = service.get_ledger_db_status(db_path, include_hash=False, include_integrity=False) assert status["success"] is True _assert_subset_schema(status, contract["status"], "status") def test_list_report_and_mutation_success_envelope_contract(tmp_path: Path) -> None: contract = _load_contract()["scopes"] db_path = str(tmp_path / "contract-fixture.sqlite3") _build_contract_fixture(db_path) mutation = service.create_ledger_account("Synthetic Savings", type="bank", liquidity_bucket="deposit", last_balance=0, last_synced_at="2026-06-30", db_path=db_path) assert mutation["success"] is True assert service.register_account_valuation(mutation["account"]["id"], "2026-06-30", 0, verification_status="verified", idempotency_key="savings-v1", db_path=db_path)["success"] _assert_subset_schema(mutation, contract["mutation_success_envelope"], "mutation_success_envelope") manual_asset = service.create_manual_asset("Synthetic Deposit", 2_000_000, "2026-06-30", "deposit", source_note="synthetic contract", db_path=db_path) assert manual_asset["success"] is True _assert_subset_schema(manual_asset, contract["manual_asset_mutation"], "manual_asset_mutation") preview = service.preview_asset_snapshot_close("2026-06-30", db_path=db_path) assert preview["success"] is True _assert_subset_schema(preview, contract["asset_snapshot_preview"], "asset_snapshot_preview") close = service.close_asset_snapshot("2026-06-30", source_note="synthetic contract close", db_path=db_path) assert close["success"] is True _assert_subset_schema(close, contract["asset_snapshot_close"], "asset_snapshot_close") accounts = service.list_ledger_accounts(db_path=db_path) assert accounts["success"] is True assert accounts["count"] >= 2 _assert_subset_schema(accounts, contract["list"], "list") report = service.get_monthly_cashflow_report("2026-06", db_path=db_path) assert report["success"] is True _assert_subset_schema(report, contract["report"], "report")