test: cover stdio mcp runtime reuse over rpc

This commit is contained in:
lda
2026-06-08 20:34:30 +07:00 Verified
parent 3f04ee6ace
commit 7582c1d6fb
@@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
import sys
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, cast
import httpx import httpx
import pytest
from mcp.client.session import ClientSession from mcp.client.session import ClientSession
from mcp.types import ( from mcp.types import (
CallToolResult, CallToolResult,
@@ -15,9 +17,11 @@ from mcp.types import (
Tool, Tool,
) )
from wf_api import file_workflow_stores from tests.wf_mcp.test_support import fixture_server_path
from wf_api import TraceRange, file_workflow_stores
from wf_api.models import RawWorkflowPlan from wf_api.models import RawWorkflowPlan
from wf_config import WorkflowConfigFile from wf_config import WorkflowConfigFile
from wf_mcp.broker.config import build_service_from_config
from wf_mcp.broker.server import ( from wf_mcp.broker.server import (
build_workflow_server_from_config, build_workflow_server_from_config,
build_workflow_server_from_workflow_config, build_workflow_server_from_workflow_config,
@@ -596,3 +600,206 @@ async def test_mcp_backed_rpc_workflow_reuses_runtime_session_direct_setup(
("counter", {}), ("counter", {}),
("counter", {}), ("counter", {}),
] ]
async def test_mcp_backed_rpc_deployment_becomes_unrunnable_after_source_removed(
tmp_path,
) -> None:
server, service, _factory = _runtime_reuse_server(tmp_path)
await service.refresh_connection_catalog("fixture.default")
await server.api.create_artifact_from_plan(
artifact_id="counter_removed_source",
version=1,
title="Counter Removed Source",
plan=RawWorkflowPlan.model_validate(
{
"name": "counter_removed_source",
"input_schema": {"type": "object", "properties": {}},
"state_schema": {
"fields": {
"count": {"type": "integer", "reducer": "wf.std.replace"}
}
},
"output_schema": {
"type": "object",
"properties": {"count": {"type": "integer"}},
},
"outcomes": ["ok", "error"],
"output": [
{
"path": {"root": "state", "parts": ["count"]},
"target": {"root": "local", "parts": ["count"]},
}
],
"start": "run_counter",
"nodes": [
{
"id": "run_counter",
"type": "node",
"node": "fixture.default.counter",
"input": [],
"output": [
{
"source": {"root": "local", "parts": ["count"]},
"target": {"root": "state", "parts": ["count"]},
}
],
},
{"id": "end_ok", "type": "end", "outcome": "ok"},
{"id": "end_error", "type": "end", "outcome": "error"},
],
"edges": [
{"from": "run_counter", "outcome": "ok", "to": "end_ok"},
{"from": "run_counter", "outcome": "error", "to": "end_error"},
],
}
),
outcomes=["ok", "error"],
)
await server.api.save_deployment(
{
"id": "counter_removed_source.default",
"artifact_id": "counter_removed_source",
"artifact_version": 1,
"bindings": [
{
"logical_source": "fixture.default",
"concrete_source": "fixture.default",
},
{"logical_source": "wf.std", "concrete_source": "wf.std"},
],
}
)
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
) as http_client:
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
before = await client.validate_deployment(
deployment_id="counter_removed_source.default"
)
service.connection_service.sync_connections_from_config(
BrokerConfig(store_root=tmp_path / "store", connections=[])
)
after = await client.validate_deployment(
deployment_id="counter_removed_source.default"
)
assert before["status"] == "runnable"
assert after["status"] == "unrunnable"
assert after["diagnostics"]
async def test_mcp_backed_rpc_workflow_reuses_real_stdio_fixture_session(
tmp_path,
) -> None:
"""The real stdio MCP fixture must keep server-local state across RPC runs."""
config = BrokerConfig(
store_root=tmp_path / "store",
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
service = build_service_from_config(config)
try:
await service.refresh_connection_catalog("fixture.personal")
except PermissionError as exc:
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
server = workflow_server_from_service(
service,
config=config,
source_registry_store=FileSourceRegistryStore(config.store_root),
)
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
) as http_client:
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
await client.create_draft_workspace_from_capability(
workspace_id="remember_ws",
capability_name="fixture.personal.remember_value_tool",
name="remember_workflow",
title="Remember Workflow",
)
await client.create_artifact_from_workspace(
workspace_id="remember_ws",
artifact_id="remember_workflow",
version=1,
title="Remember Workflow",
outcomes=["ok", "error"],
kind="workflow",
)
await client.save_deployment(
{
"id": "remember_workflow.default",
"artifact_id": "remember_workflow",
"artifact_version": 1,
"bindings": [
{
"logical_source": "fixture.personal",
"concrete_source": "fixture.personal",
},
{"logical_source": "wf.std", "concrete_source": "wf.std"},
],
}
)
await client.create_draft_workspace_from_capability(
workspace_id="recall_ws",
capability_name="fixture.personal.recall_value_tool",
name="recall_workflow",
title="Recall Workflow",
)
await client.create_artifact_from_workspace(
workspace_id="recall_ws",
artifact_id="recall_workflow",
version=1,
title="Recall Workflow",
outcomes=["ok", "error"],
kind="workflow",
)
await client.save_deployment(
{
"id": "recall_workflow.default",
"artifact_id": "recall_workflow",
"artifact_version": 1,
"bindings": [
{
"logical_source": "fixture.personal",
"concrete_source": "fixture.personal",
},
{"logical_source": "wf.std", "concrete_source": "wf.std"},
],
}
)
remembered = await client.run_deployment(
deployment_id="remember_workflow.default",
workflow_input={"value": "held-through-rpc"},
)
recalled = await client.run_deployment(
deployment_id="recall_workflow.default",
workflow_input={},
)
recall_trace = await client.read_run_trace(
run_id=recalled["run_id"],
trace_range=TraceRange(start=0, limit=5),
)
assert remembered["status"] == "completed"
assert recalled["status"] == "completed"
assert recall_trace["trace"][0]["output"]["remembered"] == "held-through-rpc"