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 ..models import AuthRecord, ConnectionConfig
from ..runtime import ToolExecutor
from ..sdk import BackendAdapter
from ..shared import root_exception
from ..workflow import wrap_discovered_tool
@@ -72,7 +73,7 @@ def specs_from_discovered_tools(
*,
connection: ConnectionConfig,
auth: AuthRecord | None,
adapter: BackendAdapter,
executor: ToolExecutor,
tools: list[DiscoveredTool],
emit_event: Callable[[McpEvent], None] | None = None,
) -> list[NodeSpec[Any, Any]]:
@@ -80,7 +81,7 @@ def specs_from_discovered_tools(
wrap_discovered_tool(
connection=connection,
auth=auth,
adapter=adapter,
executor=executor,
tool=tool,
emit_event=emit_event,
)
+1 -1
View File
@@ -606,7 +606,7 @@ class WfMcpService:
specs = specs_from_discovered_tools(
connection=connection,
auth=auth,
adapter=adapter,
executor=adapter,
tools=capabilities.tools,
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 ..models import AuthRecord, ConnectionConfig
from ..sdk import BackendAdapter
from ..runtime import ToolExecutor
_JSON_TYPE_MAP: dict[str, object] = {
@@ -103,7 +103,7 @@ def wrap_discovered_tool(
*,
connection: ConnectionConfig,
auth: AuthRecord | None,
adapter: BackendAdapter,
executor: ToolExecutor,
tool: DiscoveredTool,
emit_event: Callable[[McpEvent], None] | None = None,
) -> NodeSpec[BaseModel, BaseModel]:
@@ -129,7 +129,7 @@ def wrap_discovered_tool(
payload={"input": payload.model_dump(exclude_unset=True)},
)
)
result = await adapter.call_tool(
result = await executor.call_tool(
connection=connection,
auth=auth,
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_mcp.capabilities import DiscoveredTool
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
@@ -37,7 +38,7 @@ def test_discovered_tool_wrapper_omits_unset_optional_arguments() -> None:
account="default",
),
auth=None,
adapter=cast(BackendAdapter, adapter),
executor=cast(ToolExecutor, adapter),
tool=DiscoveredTool(
name="browser_snapshot",
title=None,