The call-flow learner finally gets fed: exploration mode records its IVR discoveries on the call (ActiveCall.exploration_steps) instead of throwing them away, persistence stores them in the call record's metadata, and the rebuilt learn_call_flow MCP tool turns a completed exploration call into a stored flow via CallFlowLearner — correct constructor (llm_client from get_llm, heuristic labels when the LLM is unavailable), build for a new number, merge/refine when a flow already exists. save_learned_flow/update_flow_from_model keep the CallFlow↔row mapping in call_persistence. Test gaps closed: tests/test_learner.py (discoveries→linked steps, exploration persistence, learn-then-refine through the in-memory MCP client, no-data and unknown-call answers) and tests/test_websocket.py (4401 without token, trunk-status-then-replay on connect, per-call stream filtering). Docs aligned to code: README (15 tools incl. learn_call_flow, HTTP not SSE, Python 3.12+, PostgreSQL+Alembic — no SQLite fallback, media pipeline marked stub-mode until pjsua2 installed, Alembic and honest /health checked off); docs/mcp-server.md rewritten against the actual tool surface (hangup not end_call, real params, 3 real resources, /mcp/ streamable HTTP + bearer auth); architecture/development/ configuration drift fixed. pyproject: pruned never-imported deps (websockets, librosa, soundfile, python-multipart). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
"""
|
|
MCP server tests — tool surface, lazy gateway resolution, call safety.
|
|
|
|
Uses the FastMCP in-memory client (no network, no mounted app).
|
|
"""
|
|
|
|
import pytest
|
|
from fastmcp import Client
|
|
|
|
from config import Settings
|
|
from core.dial_plan import is_emergency_number
|
|
from core.gateway import AIPSTNGateway
|
|
from mcp_server.server import create_mcp_server
|
|
|
|
EXPECTED_TOOLS = {
|
|
"make_call",
|
|
"get_call_status",
|
|
"transfer_call",
|
|
"hangup",
|
|
"list_active_calls",
|
|
"get_call_flow",
|
|
"create_call_flow",
|
|
"send_dtmf",
|
|
"get_call_transcript",
|
|
"get_call_recording",
|
|
"get_call_summary",
|
|
"search_call_history",
|
|
"learn_call_flow",
|
|
"list_devices",
|
|
"gateway_status",
|
|
}
|
|
|
|
|
|
def _make_gateway(max_calls: int = 4) -> AIPSTNGateway:
|
|
"""Unstarted gateway on the in-memory MockSIPEngine — no network, no DB."""
|
|
return AIPSTNGateway(settings=Settings(max_concurrent_calls=max_calls))
|
|
|
|
|
|
class TestToolSurface:
|
|
async def test_tool_listing_matches_expected(self):
|
|
mcp = create_mcp_server(lambda: None)
|
|
async with Client(mcp) as client:
|
|
tools = {t.name for t in await client.list_tools()}
|
|
assert tools == EXPECTED_TOOLS
|
|
|
|
async def test_auth_configured_when_token_given(self):
|
|
assert create_mcp_server(lambda: None, api_token="sekrit").auth is not None
|
|
assert create_mcp_server(lambda: None).auth is None
|
|
|
|
|
|
class TestGatewayResolution:
|
|
async def test_tool_errors_cleanly_before_gateway_ready(self):
|
|
mcp = create_mcp_server(lambda: None)
|
|
async with Client(mcp) as client:
|
|
with pytest.raises(Exception, match="starting up"):
|
|
await client.call_tool("list_active_calls", {})
|
|
|
|
async def test_make_call_happy_path(self):
|
|
gateway = _make_gateway()
|
|
mcp = create_mcp_server(lambda: gateway)
|
|
async with Client(mcp) as client:
|
|
result = await client.call_tool(
|
|
"make_call", {"number": "+15551234567", "mode": "direct"}
|
|
)
|
|
text = result.content[0].text
|
|
assert "initiated" in text
|
|
assert "+15551234567" in text
|
|
assert len(gateway.call_manager.active_calls) == 1
|
|
|
|
|
|
class TestCallSafety:
|
|
def test_emergency_number_detection(self):
|
|
for number in ("911", "9911", "112", "+1911", "+112", " 911 ", "9-1-1"):
|
|
assert is_emergency_number(number), number
|
|
for number in ("+19115551234", "+18005551234", "211", "999"):
|
|
assert not is_emergency_number(number), number
|
|
|
|
async def test_gateway_refuses_emergency_numbers(self):
|
|
gateway = _make_gateway()
|
|
with pytest.raises(ValueError, match="emergency"):
|
|
await gateway.make_call("911")
|
|
assert gateway.call_manager.active_calls == {}
|
|
|
|
async def test_mcp_make_call_refuses_emergency(self):
|
|
gateway = _make_gateway()
|
|
mcp = create_mcp_server(lambda: gateway)
|
|
async with Client(mcp) as client:
|
|
with pytest.raises(Exception, match="[Ee]mergency"):
|
|
await client.call_tool("make_call", {"number": "911"})
|
|
|
|
async def test_concurrent_call_cap(self):
|
|
gateway = _make_gateway(max_calls=1)
|
|
await gateway.make_call("+15551234567")
|
|
with pytest.raises(ValueError, match="limit"):
|
|
await gateway.make_call("+15557654321")
|