north-scape/relayboard

Relayboard: accessible team boards with audited actions, dependency maps, and shared web/MCP use cases.

A remix template · 0 remixes · Remix north-scape/relayboard

tests/test_mcp_annotations.py

"""Inspect the public MCP wire metadata, not only decorator implementation details."""
import json

from fastapi.testclient import TestClient

from boards.application.use_cases import Boards
from boards.infrastructure.memory import MemoryBoards
from boards.main import create_app


READS = {'my_boards', 'show_board', 'my_cards', 'overdue', 'activity', 'blockers', 'resolve_board', 'resolve_card'}
AI = {'suggest_checklist', 'board_summary', 'similar_cards'}
CREATES = {'create_board', 'add_card'}
REPLACEMENTS = {'update_checklist', 'update_card', 'move_card', 'assign_card', 'add_member', 'remove_member', 'set_blockers'}


def test_tool_annotations_describe_effects_and_preserve_closed_input_domain():
    with TestClient(create_app(Boards(MemoryBoards()))) as client:
        response = client.post('/mcp', headers={
            'X-Dodil-User': json.dumps({'email': 'owner@example.test'}),
            'accept': 'application/json, text/event-stream',
        }, json={'jsonrpc': '2.0', 'id': 1, 'method': 'tools/list'})
    assert response.status_code == 200, response.text
    tools = {tool['name']: tool for tool in response.json()['result']['tools']}
    assert set(tools) == READS | AI | CREATES | REPLACEMENTS
    for name, tool in tools.items():
        hints = tool['annotations']
        assert hints['readOnlyHint'] is (name in READS | AI), name
        # Role changes can demote members and unassign their cards; not additive.
        assert hints['destructiveHint'] is (name in REPLACEMENTS), name
        # Creates can duplicate; writes update audits/revisions; AI consumes quota.
        assert hints['idempotentHint'] is (name in READS), name
        assert hints['openWorldHint'] is False, name
        properties = tool['inputSchema']['properties']
        # Identity remains injected; the caller cannot select a network endpoint.
        assert not {'ctx', 'person', 'actor', 'url', 'endpoint', 'base_url'} & properties.keys()
    assert 'expected_revision' in tools['update_card']['inputSchema']['properties']
    assert 'email' in tools['add_member']['inputSchema']['properties']
    assert 'expected_revision' in tools['update_checklist']['inputSchema']['required']


def test_create_retry_really_is_non_idempotent():
    from boards.domain.model import Person
    service = Boards(MemoryBoards())
    owner = Person('owner@example.test')
    first = service.create_board(owner, 'Same title')
    second = service.create_board(owner, 'Same title')
    assert first['id'] != second['id']
    one = service.add_card(owner, first['id'], 'Same card')
    two = service.add_card(owner, first['id'], 'Same card')
    assert one['id'] != two['id']