"""Exercise settlement MCP lifecycle through the repository's real stdio server.""" import asyncio from contextlib import closing import inspect import json import os import sqlite3 import sys from pathlib import Path import pytest mcp = pytest.importorskip('mcp') from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client OPS = ('create_settlement_draft', 'list_settlement_drafts', 'get_settlement_draft', 'update_settlement_draft', 'refresh_settlement_draft_targets', 'set_settlement_draft_item', 'review_settlement_draft', 'approve_settlement_draft', 'finalize_settlement_draft', 'cancel_settlement_draft') ROOT = Path(__file__).resolve().parents[1] def payload(result): assert not result.isError, result assert len(result.content) == 1 and result.content[0].type == 'text' data = json.loads(result.content[0].text) assert isinstance(data, dict) and isinstance(data.get('success'), bool) return data def rows(db, table): assert table in {'asset_snapshot_runs', 'asset_snapshots', 'settlement_drafts', 'settlement_draft_items'} with closing(sqlite3.connect(db)) as connection: return connection.execute(f'SELECT * FROM {table} ORDER BY rowid').fetchall() def test_real_stdio_startup_and_all_ten_lifecycle_tools(tmp_path): asyncio.run(_exercise_stdio_lifecycle(tmp_path)) async def _exercise_stdio_lifecycle(tmp_path): from jarca_ledger_mcp import service as service db = tmp_path / 'mcp-runtime.sqlite3' env = os.environ.copy() env.update(JARCA_LEDGER_DB_PATH=str(db), JARCA_MCP_TRANSPORT='stdio', PYTHONDONTWRITEBYTECODE='1') # The test owns this bounded stdio child; stdio_client reaps it on context exit. params = StdioServerParameters(command=sys.executable, args=['-B', '-m', 'jarca_ledger_mcp.server'], env=env, cwd=str(ROOT)) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: initialized = await session.initialize() assert initialized.serverInfo.name == 'jarca-ledger-mcp' listed = await session.list_tools() tool_map = {tool.name: tool for tool in listed.tools} assert set(OPS) <= set(tool_map) for name in OPS: schema = tool_map[name].inputSchema props = schema.get('properties', {}) assert 'db_path' not in props parameters = inspect.signature(getattr(service, name)).parameters assert set(props) == set(parameters) - {'db_path'} assert set(schema.get('required', [])) == {key for key, value in parameters.items() if key != 'db_path' and value.default is inspect.Parameter.empty} # Seed through a real service database setup, never through MCP stubs. from jarca_ledger_mcp.db import init_db init_db(str(db)) account = service.create_ledger_account('Synthetic MCP cash', db_path=str(db))['account']['id'] service.register_account_valuation(account, '2026-09-30', 100, verification_status='verified', idempotency_key='mcp-value', db_path=str(db)) async def call(name, args): return payload(await session.call_tool(name, args)) made = await call('create_settlement_draft', {'month':'2026-09','created_by':'stdio-test','idempotency_key':'mcp-draft'}) assert made['success'] draft_id = made['draft']['id'] assert (await call('list_settlement_drafts', {'month':'2026-09'}))['drafts'][0]['id'] == draft_id fetched = await call('get_settlement_draft', {'draft_id':draft_id}) draft = fetched['draft'] updated = await call('update_settlement_draft', {'draft_id':draft_id,'expected_version':draft['version'],'updated_by':'stdio-test','memo':'transport lifecycle'}) draft = updated['draft'] refreshed = await call('refresh_settlement_draft_targets', {'draft_id':draft_id,'expected_version':draft['version'],'updated_by':'stdio-test'}) draft = refreshed['draft'] changed = await call('set_settlement_draft_item', {'draft_id':draft_id,'source_type':'account','source_id':account,'expected_version':draft['version'],'selected_by':'stdio-test','valuation_id':service.list_account_valuations(account, db_path=str(db))['valuations'][0]['id']}) draft = changed['draft'] reviewed = await call('review_settlement_draft', {'draft_id':draft_id}) assert reviewed['success'], reviewed fingerprint = reviewed['review_fingerprint'] approved = await call('approve_settlement_draft', {'draft_id':draft_id,'expected_version':draft['version'],'review_fingerprint':fingerprint,'approved_by':'stdio-test','approval_reason':'transport acceptance','approval_reference':'test'}) assert approved['success'] closed = await call('finalize_settlement_draft', {'draft_id':draft_id,'expected_version':draft['version'],'review_fingerprint':fingerprint,'idempotency_key':'mcp-close'}) assert closed['success'] and closed['totals']['net_worth'] == 100 before = (await call('get_settlement_draft', {'draft_id':draft_id})) tables = ('asset_snapshot_runs', 'asset_snapshots', 'settlement_drafts', 'settlement_draft_items') before_replay = {table: rows(db, table) for table in tables} assert await call('finalize_settlement_draft', {'draft_id':draft_id,'expected_version':draft['version'],'review_fingerprint':fingerprint,'idempotency_key':'mcp-close'}) == closed assert await call('get_settlement_draft', {'draft_id':draft_id}) == before assert {table: rows(db, table) for table in tables} == before_replay second = await call('create_settlement_draft', {'month':'2026-10','created_by':'stdio-test','idempotency_key':'mcp-cancel'}) assert second['success'] cancelled = await call('cancel_settlement_draft', {'draft_id':second['draft']['id'],'expected_version':second['draft']['version'],'cancelled_by':'stdio-test','reason':'synthetic cancellation'}) assert cancelled['success'] and cancelled['draft']['status'] == 'cancelled'