Files
MoviePilot/tests/test_mcp_plugin_tools.py

99 lines
3.1 KiB
Python

import asyncio
import json
from types import SimpleNamespace
from typing import Iterator
from unittest.mock import patch
import pytest
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.factory import MoviePilotToolFactory
from app.agent.tools.manager import MoviePilotToolsManager
from app.api.endpoints import mcp
from app.core.plugin import PluginManager
from app.utils.singleton import Singleton
class DemoPluginTool(MoviePilotTool):
"""测试用插件 MCP 工具。"""
name: str = "demo_plugin_tool"
description: str = "测试插件动态注册的 MCP 工具"
async def run(self, **kwargs) -> str:
"""返回固定测试结果。"""
return "plugin-ok"
@pytest.fixture
def plugin_manager() -> Iterator[PluginManager]:
"""构造隔离的插件管理器并在测试后恢复原单例。"""
singleton_key = (PluginManager, (), frozenset())
previous_instance = Singleton._instances.pop(singleton_key, None)
manager = PluginManager()
yield manager
Singleton._instances.pop(singleton_key, None)
if previous_instance is not None:
Singleton._instances[singleton_key] = previous_instance
def _build_plugin() -> SimpleNamespace:
"""构造声明一个 Agent 工具的已启用插件。"""
return SimpleNamespace(
plugin_name="Demo Plugin",
get_state=lambda: True,
get_agent_tools=lambda: [DemoPluginTool],
)
def test_mcp_refreshes_tools_after_plugin_lifecycle_change(
plugin_manager: PluginManager,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""MCP 管理器应发现初始化后新增的插件工具,并在插件移除后停止暴露。"""
with patch.object(
MoviePilotToolFactory,
"_get_builtin_tool_classes",
return_value=[],
):
tool_manager = MoviePilotToolsManager(
session_id="mcp-plugin-test",
user_id="api_user",
)
monkeypatch.setattr(mcp, "moviepilot_tool_manager", tool_manager)
assert asyncio.run(mcp.handle_tools_list()) == {"tools": []}
plugin_manager.running_plugins["DemoPlugin"] = _build_plugin()
plugin_manager.clear_plugin_agent_tools_cache()
listed_tools = asyncio.run(mcp.handle_tools_list())["tools"]
assert [tool["name"] for tool in listed_tools] == ["demo_plugin_tool"]
call_result = asyncio.run(
mcp.handle_tools_call(
{
"name": "demo_plugin_tool",
"arguments": {},
}
)
)
assert call_result == {
"content": [{"type": "text", "text": "plugin-ok"}]
}
plugin_manager.running_plugins.pop("DemoPlugin")
plugin_manager.clear_plugin_agent_tools_cache()
assert asyncio.run(mcp.handle_tools_list()) == {"tools": []}
missing_result = asyncio.run(
mcp.handle_tools_call(
{
"name": "demo_plugin_tool",
"arguments": {},
}
)
)
missing_payload = json.loads(missing_result["content"][0]["text"])
assert "未找到" in missing_payload["error"]