093a74953f
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / lint (pull_request) Successful in 21s
CI / quality (pull_request) Successful in 27s
CI / security (pull_request) Successful in 56s
CI / typecheck (pull_request) Successful in 1m2s
CI / coverage (pull_request) Has been cancelled
CI / unit_tests (pull_request) Has been cancelled
CI / integration_tests (pull_request) Has been cancelled
CI / benchmark-regression (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
Add comprehensive E2E test suite for M2 (Actor Graphs + Tool Sources) epic: - Behave BDD: 10 scenarios covering actor YAML loading, skill registry, tool lifecycle (discover/activate/execute/deactivate), and MCP stub - Robot Framework: 6 integration tests via CLI helper script - ASV benchmarks: 12 benchmarks for actor loading, skill registry, tool lifecycle, and MCP stub performance baselines - MCP stub server mock: in-process fake with 3 tools (search/fetch/transform) - Fixtures: hierarchical graph actor YAML + skill pack with tool refs and inline tools - Docs: updated testing.md with M2 smoke suite section Closes #169
195 lines
6.1 KiB
Python
195 lines
6.1 KiB
Python
"""MCP stub server mock for M2 actor + tool source smoke tests.
|
|
|
|
Simulates MCP tool discovery and invocation in-process without requiring
|
|
a real MCP adapter. Returns deterministic tool descriptors and execution
|
|
results for Behave-level testing.
|
|
|
|
This mock lives in ``features/mocks/`` per ADR-022 (no mocks in src/).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, ClassVar
|
|
|
|
|
|
class McpStubTool:
|
|
"""Descriptor for a single tool exposed by the MCP stub server."""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
description: str,
|
|
input_schema: dict[str, Any] | None = None,
|
|
output_schema: dict[str, Any] | None = None,
|
|
) -> None:
|
|
self.name = name
|
|
self.description = description
|
|
self.input_schema = input_schema or {
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string"}},
|
|
}
|
|
self.output_schema = output_schema or {
|
|
"type": "object",
|
|
"properties": {"result": {"type": "string"}},
|
|
}
|
|
|
|
|
|
class McpStubServer:
|
|
"""In-process MCP stub server for deterministic testing.
|
|
|
|
Provides ``discover()`` and ``invoke()`` methods that mirror the
|
|
future MCP adapter interface (#159) without requiring network I/O.
|
|
|
|
Usage::
|
|
|
|
server = McpStubServer()
|
|
server.start()
|
|
tools = server.discover()
|
|
result = server.invoke("mcp/search", {"query": "hello"})
|
|
server.stop()
|
|
"""
|
|
|
|
#: Default tools returned by ``discover()``.
|
|
DEFAULT_TOOLS: ClassVar[list[McpStubTool]] = [
|
|
McpStubTool(
|
|
name="mcp/search",
|
|
description="Stub MCP search tool",
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string"}},
|
|
"required": ["query"],
|
|
},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"results": {"type": "array", "items": {"type": "string"}}
|
|
},
|
|
},
|
|
),
|
|
McpStubTool(
|
|
name="mcp/fetch",
|
|
description="Stub MCP fetch tool",
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {"url": {"type": "string"}},
|
|
"required": ["url"],
|
|
},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"content": {"type": "string"},
|
|
"status": {"type": "integer"},
|
|
},
|
|
},
|
|
),
|
|
McpStubTool(
|
|
name="mcp/transform",
|
|
description="Stub MCP transform tool",
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"data": {"type": "string"},
|
|
"format": {"type": "string"},
|
|
},
|
|
"required": ["data"],
|
|
},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {"transformed": {"type": "string"}},
|
|
},
|
|
),
|
|
]
|
|
|
|
def __init__(self, tools: list[McpStubTool] | None = None) -> None:
|
|
self._tools = tools or list(self.DEFAULT_TOOLS)
|
|
self._running = False
|
|
self._invocation_log: list[dict[str, Any]] = []
|
|
|
|
@property
|
|
def is_running(self) -> bool:
|
|
"""Whether the stub server is currently started."""
|
|
return self._running
|
|
|
|
@property
|
|
def invocation_log(self) -> list[dict[str, Any]]:
|
|
"""Log of all invocations since the last ``start()``."""
|
|
return list(self._invocation_log)
|
|
|
|
def start(self) -> None:
|
|
"""Start the stub server (in-process, no network)."""
|
|
self._running = True
|
|
self._invocation_log = []
|
|
|
|
def stop(self) -> None:
|
|
"""Stop the stub server."""
|
|
self._running = False
|
|
|
|
def discover(self) -> list[McpStubTool]:
|
|
"""Return the list of available tools.
|
|
|
|
Raises:
|
|
RuntimeError: If the server is not running.
|
|
"""
|
|
if not self._running:
|
|
msg = "McpStubServer is not running. Call start() first."
|
|
raise RuntimeError(msg)
|
|
return list(self._tools)
|
|
|
|
def invoke(self, tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
"""Invoke a stub tool by name with deterministic results.
|
|
|
|
Raises:
|
|
RuntimeError: If the server is not running.
|
|
KeyError: If the tool name is not found.
|
|
"""
|
|
if not self._running:
|
|
msg = "McpStubServer is not running. Call start() first."
|
|
raise RuntimeError(msg)
|
|
|
|
tool = self._find_tool(tool_name)
|
|
if tool is None:
|
|
msg = f"Tool '{tool_name}' not found in MCP stub server."
|
|
raise KeyError(msg)
|
|
|
|
# Generate deterministic result based on tool name
|
|
result = self._generate_result(tool, params)
|
|
self._invocation_log.append(
|
|
{
|
|
"tool_name": tool_name,
|
|
"params": params,
|
|
"result": result,
|
|
}
|
|
)
|
|
return result
|
|
|
|
def _find_tool(self, name: str) -> McpStubTool | None:
|
|
"""Find a tool by name."""
|
|
for tool in self._tools:
|
|
if tool.name == name:
|
|
return tool
|
|
return None
|
|
|
|
def _generate_result(
|
|
self, tool: McpStubTool, params: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
"""Generate a deterministic result for a tool invocation."""
|
|
if tool.name == "mcp/search":
|
|
query = params.get("query", "")
|
|
return {
|
|
"results": [f"result_1_for_{query}", f"result_2_for_{query}"],
|
|
}
|
|
if tool.name == "mcp/fetch":
|
|
url = params.get("url", "")
|
|
return {
|
|
"content": f"<html>stub content for {url}</html>",
|
|
"status": 200,
|
|
}
|
|
if tool.name == "mcp/transform":
|
|
data = params.get("data", "")
|
|
fmt = params.get("format", "upper")
|
|
transformed = data.upper() if fmt == "upper" else data.lower()
|
|
return {"transformed": transformed}
|
|
|
|
# Generic fallback
|
|
return {"result": f"stub result from {tool.name}"}
|