Tests necessary for the runtime protocol

This commit is contained in:
lda
2026-05-19 23:03:03 +07:00 Verified
parent 4e3de7b91b
commit 252ebab1f7
7 changed files with 124 additions and 8 deletions
+3 -2
View File
@@ -11,6 +11,7 @@ from wf_authoring import NodeSpec
from ..capabilities import DiscoveredPrompt, DiscoveredResource, DiscoveredTool from ..capabilities import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from ..models import AuthRecord, ConnectionConfig from ..models import AuthRecord, ConnectionConfig
from ..runtime import ToolExecutor
from ..sdk import BackendAdapter from ..sdk import BackendAdapter
from ..shared import root_exception from ..shared import root_exception
from ..workflow import wrap_discovered_tool from ..workflow import wrap_discovered_tool
@@ -72,7 +73,7 @@ def specs_from_discovered_tools(
*, *,
connection: ConnectionConfig, connection: ConnectionConfig,
auth: AuthRecord | None, auth: AuthRecord | None,
adapter: BackendAdapter, executor: ToolExecutor,
tools: list[DiscoveredTool], tools: list[DiscoveredTool],
emit_event: Callable[[McpEvent], None] | None = None, emit_event: Callable[[McpEvent], None] | None = None,
) -> list[NodeSpec[Any, Any]]: ) -> list[NodeSpec[Any, Any]]:
@@ -80,7 +81,7 @@ def specs_from_discovered_tools(
wrap_discovered_tool( wrap_discovered_tool(
connection=connection, connection=connection,
auth=auth, auth=auth,
adapter=adapter, executor=executor,
tool=tool, tool=tool,
emit_event=emit_event, emit_event=emit_event,
) )
+1 -1
View File
@@ -606,7 +606,7 @@ class WfMcpService:
specs = specs_from_discovered_tools( specs = specs_from_discovered_tools(
connection=connection, connection=connection,
auth=auth, auth=auth,
adapter=adapter, executor=adapter,
tools=capabilities.tools, tools=capabilities.tools,
emit_event=self._record_event, emit_event=self._record_event,
) )
+3
View File
@@ -0,0 +1,3 @@
from .protocols import ToolExecutor
__all__ = ["ToolExecutor"]
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
from typing import Any, Protocol
from ..models import AuthRecord, ConnectionConfig
from ..sdk import ToolCallResult
class ToolExecutor(Protocol):
"""Runtime boundary for executing MCP tools from workflow nodes.
Discovery can stay one-shot, but workflow execution needs this smaller
protocol so a future persistent runtime pool can replace the current
adapter without changing generated NodeSpecs.
"""
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult: ...
+3 -3
View File
@@ -12,7 +12,7 @@ from wf_mcp.broker.events import McpEvent, make_event
from ..capabilities import DiscoveredTool from ..capabilities import DiscoveredTool
from ..models import AuthRecord, ConnectionConfig from ..models import AuthRecord, ConnectionConfig
from ..sdk import BackendAdapter from ..runtime import ToolExecutor
_JSON_TYPE_MAP: dict[str, object] = { _JSON_TYPE_MAP: dict[str, object] = {
@@ -103,7 +103,7 @@ def wrap_discovered_tool(
*, *,
connection: ConnectionConfig, connection: ConnectionConfig,
auth: AuthRecord | None, auth: AuthRecord | None,
adapter: BackendAdapter, executor: ToolExecutor,
tool: DiscoveredTool, tool: DiscoveredTool,
emit_event: Callable[[McpEvent], None] | None = None, emit_event: Callable[[McpEvent], None] | None = None,
) -> NodeSpec[BaseModel, BaseModel]: ) -> NodeSpec[BaseModel, BaseModel]:
@@ -129,7 +129,7 @@ def wrap_discovered_tool(
payload={"input": payload.model_dump(exclude_unset=True)}, payload={"input": payload.model_dump(exclude_unset=True)},
) )
) )
result = await adapter.call_tool( result = await executor.call_tool(
connection=connection, connection=connection,
auth=auth, auth=auth,
tool_name=tool.name, tool_name=tool.name,
+88
View File
@@ -0,0 +1,88 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from typing import Any
from wf_authoring import build_async_registry
from wf_core import RuntimeContext
from wf_mcp.capabilities import DiscoveredTool
from wf_mcp.models import AuthRecord, ConnectionConfig
from wf_mcp.sdk import ToolCallResult
from wf_mcp.workflow import wrap_discovered_tool
@dataclass(slots=True)
class FakeStatefulExecutor:
"""Executor fake that exposes why workflow calls need shared MCP runtime."""
page_open: bool = False
calls: list[tuple[str, dict[str, Any]]] = field(default_factory=list)
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult:
self.calls.append((tool_name, payload))
if tool_name == "browser_navigate":
self.page_open = True
return ToolCallResult(outcome="ok", output={"content": "opened"})
if tool_name == "browser_snapshot":
if not self.page_open:
return ToolCallResult(outcome="error", output={"content": "no page"})
return ToolCallResult(outcome="ok", output={"content": "snapshot"})
raise KeyError(tool_name)
def _tool(name: str) -> DiscoveredTool:
return DiscoveredTool(
name=name,
title=None,
description=None,
input_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
outcomes=("ok", "error"),
)
def test_generated_workflow_specs_share_injected_tool_executor() -> None:
"""Generated NodeSpecs use the injected executor, not a baked-in adapter."""
connection = ConnectionConfig(
id="playwright.default",
server="playwright",
account="default",
)
executor = FakeStatefulExecutor()
navigate = wrap_discovered_tool(
connection=connection,
auth=None,
executor=executor,
tool=_tool("browser_navigate"),
)
snapshot = wrap_discovered_tool(
connection=connection,
auth=None,
executor=executor,
tool=_tool("browser_snapshot"),
)
handlers = build_async_registry(navigate, snapshot)
async def run_workflow_calls() -> dict[str, Any]:
await handlers["browser_navigate"](
{},
RuntimeContext(current_node_id="navigate"),
)
return await handlers["browser_snapshot"](
{},
RuntimeContext(current_node_id="snapshot"),
)
result = asyncio.run(run_workflow_calls())
assert result["outcome"] == "ok"
assert result["output"]["content"] == "snapshot"
assert executor.calls == [("browser_navigate", {}), ("browser_snapshot", {})]
+3 -2
View File
@@ -7,7 +7,8 @@ from wf_authoring import build_async_registry
from wf_core import RuntimeContext from wf_core import RuntimeContext
from wf_mcp.capabilities import DiscoveredTool from wf_mcp.capabilities import DiscoveredTool
from wf_mcp.models import AuthRecord, ConnectionConfig from wf_mcp.models import AuthRecord, ConnectionConfig
from wf_mcp.sdk import BackendAdapter, ToolCallResult from wf_mcp.runtime import ToolExecutor
from wf_mcp.sdk import ToolCallResult
from wf_mcp.workflow import wrap_discovered_tool from wf_mcp.workflow import wrap_discovered_tool
@@ -37,7 +38,7 @@ def test_discovered_tool_wrapper_omits_unset_optional_arguments() -> None:
account="default", account="default",
), ),
auth=None, auth=None,
adapter=cast(BackendAdapter, adapter), executor=cast(ToolExecutor, adapter),
tool=DiscoveredTool( tool=DiscoveredTool(
name="browser_snapshot", name="browser_snapshot",
title=None, title=None,