agent_deply/tests/test_mcp_client.py
dark 039a3e1bdc 支持云下载继承版本参数并调整回滚请求格式
- 新增 PARENT_VERSION_NUMBER 可选配置,默认空值不传
- create-download-task 非空时透传 parentVersionNumber
- 支持 LLM/规则从自然语言和 key=value 中抽取继承版本参数
- 将 rollback 接口参数从表单 body 改为 URL query 拼接
- 同步 README、打包说明和 Skill 文档
- 增加 MCP 参数透传、配置写入和 rollback query 调用测试
2026-06-05 10:33:53 +08:00

267 lines
7.8 KiB
Python

from pam_deploy_graph.mcp_client import (
FunctionMcpToolClient,
HttpMcpToolClient,
load_mcp_client_config,
normalize_mcp_tool_list,
OAuthTokenProvider,
SessionMcpToolClient,
StdioMcpToolClient,
normalize_mcp_sdk_result,
)
from pam_deploy_graph.mcp_factory import build_mcp_runner_from_config
from pam_deploy_graph.mcp_runner import McpActionRunner
def test_function_mcp_client_wraps_callable():
client = FunctionMcpToolClient(lambda name, args: {"tool": name, "args": args})
assert client.call_tool("pam_get_online_ips", {"airportCode": "HET"})["tool"] == "pam_get_online_ips"
def test_normalize_mcp_sdk_result_structured_content():
result = type("Result", (), {"structuredContent": {"ok": True}})()
assert normalize_mcp_sdk_result(result) == {"ok": True}
def test_session_mcp_client_normalizes_text_json_content():
content = [type("Text", (), {"text": '{"ok": true}'})()]
result = type("Result", (), {"content": content})()
class Session:
def call_tool(self, tool_name, arguments):
return result
client = SessionMcpToolClient(Session())
assert client.call_tool("tool", {}) == {"ok": True}
def test_normalize_mcp_tool_list():
result = type(
"Tools",
(),
{"tools": [type("Tool", (), {"name": "pam_get_online_ips"})(), {"name": "verify-ip"}]},
)()
assert normalize_mcp_tool_list(result) == ["pam_get_online_ips", "verify-ip"]
def test_load_mcp_client_config(tmp_path):
path = tmp_path / "mcp.json"
path.write_text(
(
'{"server_name": "pam-node-prod", "transport": "stdio", '
'"command": "python", "args": ["-m", "server"], '
'"env": {"PAM_ENV": "test"}, "cwd": "/tmp", "timeout_seconds": 3, '
'"tool_names": {"get-online-ips": "custom_ips"}}'
),
encoding="utf-8",
)
config = load_mcp_client_config(path)
assert config.server_name == "pam-node-prod"
assert config.transport == "stdio"
assert config.command == "python"
assert config.args == ["-m", "server"]
assert config.env == {"PAM_ENV": "test"}
assert config.cwd == "/tmp"
assert config.timeout_seconds == 3
assert config.tool_names["get-online-ips"] == "custom_ips"
def test_load_http_mcp_client_config_with_auth(tmp_path):
path = tmp_path / "mcp.json"
path.write_text(
"""
{
"server_name": "pam-node-prod",
"transport": "streamable_http",
"server_url": "https://pam-node.example.com/mcp",
"auth": {
"token_url": "https://pam-node-auth.example.com/oauth/token",
"client_id": "mcp-client",
"client_secret": "mcp-secret"
}
}
""",
encoding="utf-8",
)
config = load_mcp_client_config(path)
assert config.transport == "streamable_http"
assert config.server_url == "https://pam-node.example.com/mcp"
assert config.auth is not None
assert config.auth.client_id == "mcp-client"
assert config.auth.client_secret == "mcp-secret"
def test_build_mcp_runner_from_stdio_config(tmp_path):
path = tmp_path / "mcp.json"
path.write_text(
'{"transport": "stdio", "command": "python", "tool_names": {"verify-ip": "custom_verify"}}',
encoding="utf-8",
)
runner = build_mcp_runner_from_config(path)
assert isinstance(runner.client, StdioMcpToolClient)
assert runner.tool_names["verify-ip"] == "custom_verify"
def test_build_mcp_runner_from_http_config(tmp_path):
path = tmp_path / "mcp.json"
path.write_text(
"""
{
"transport": "sse",
"server_url": "https://pam-node.example.com/sse",
"auth": {
"token_url": "https://pam-node-auth.example.com/oauth/token",
"client_id": "mcp-client",
"client_secret": "mcp-secret"
}
}
""",
encoding="utf-8",
)
runner = build_mcp_runner_from_config(path)
assert isinstance(runner.client, HttpMcpToolClient)
assert runner.client.transport == "sse"
def test_oauth_token_provider_uses_home_style_form(monkeypatch, tmp_path):
config = load_mcp_client_config(
_write_json_config(
tmp_path,
{
"transport": "streamable_http",
"server_url": "https://pam-node.example.com/mcp",
"auth": {
"token_url": "https://pam-node-auth.example.com/oauth/token",
"client_id": "mcp-client",
"client_secret": "mcp-secret",
},
},
)
)
assert config.auth is not None
calls = []
class Response:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
return b'{"access_token": "token-1", "expires_in": 3600}'
def fake_urlopen(request, timeout):
calls.append((request, timeout))
return Response()
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
provider = OAuthTokenProvider(config.auth)
headers = provider.authorization_headers()
assert headers == {"Authorization": "Bearer token-1"}
body = calls[0][0].data.decode("utf-8")
assert "grant_type=client_credentials" in body
assert "client_id=mcp-client" in body
assert "client_secret=mcp-secret" in body
def test_mcp_runner_auto_discovers_tool_name():
class Client:
def list_tools(self):
return ["pam_get_online_ips"]
def call_tool(self, tool_name, arguments):
return {"IP": ["192.168.1.10"], "COUNT": 1, "TOOL": tool_name}
runner = McpActionRunner(client=Client())
result = runner.run("get-online-ips", params={})
assert result.ok is True
assert result.tool_name == "pam_get_online_ips"
def test_mcp_runner_passes_hash_code_and_node_url():
calls = []
class Client:
def call_tool(self, tool_name, arguments):
calls.append((tool_name, arguments))
return {"ACTION": "upgrade-ip", "SUCCESS": "true"}
runner = McpActionRunner(client=Client())
result = runner.run(
"upgrade-ip",
params={"HOME_BASE_URL": "https://pam.home", "AIRPORT_CODE": "HET"},
ip="192.168.1.10",
hash_code="hash-1",
node_url="https://pam.node",
)
assert result.ok is True
assert calls[0][1]["targetIp"] == "192.168.1.10"
assert calls[0][1]["hashCode"] == "hash-1"
assert calls[0][1]["nodeUrl"] == "https://pam.node"
def test_mcp_runner_passes_parent_version_only_for_download_task():
calls = []
class Client:
def call_tool(self, tool_name, arguments):
calls.append((tool_name, arguments))
return {"ACTION": "create-download-task", "SUCCESS": "true"}
runner = McpActionRunner(client=Client())
result = runner.run(
"create-download-task",
params={
"VERSION_NUMBER": "2.0.5",
"PARENT_VERSION_NUMBER": "2.0.4",
},
)
assert result.ok is True
assert calls[0][1]["versionNumber"] == "2.0.5"
assert calls[0][1]["parentVersionNumber"] == "2.0.4"
def test_mcp_runner_omits_blank_parent_version():
calls = []
class Client:
def call_tool(self, tool_name, arguments):
calls.append((tool_name, arguments))
return {"ACTION": "create-download-task", "SUCCESS": "true"}
runner = McpActionRunner(client=Client())
result = runner.run(
"create-download-task",
params={
"VERSION_NUMBER": "2.0.5",
"PARENT_VERSION_NUMBER": "",
},
)
assert result.ok is True
assert "parentVersionNumber" not in calls[0][1]
def _write_json_config(tmpdir, payload):
path = tmpdir / "mcp.json"
path.write_text(__import__("json").dumps(payload), encoding="utf-8")
return str(path)