Files
jarca-ledger-mcp/tests/test_settlement_draft_mcp_runtime.py
T

105 lines
6.2 KiB
Python

"""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'