feat: add persistent monthly settlement drafts and approved valuation selection
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""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'
|
||||
Reference in New Issue
Block a user