mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-28 15:57:07 +08:00
194 lines
6.6 KiB
Python
194 lines
6.6 KiB
Python
from types import SimpleNamespace
|
|
from typing import Iterator, Optional, Type
|
|
|
|
import pytest
|
|
from pydantic import BaseModel
|
|
|
|
from app.agent.middleware.activity_log import QueryActivityLogInput
|
|
from app.agent.middleware.skills import SkillToolInput
|
|
from app.agent.tools.base import MoviePilotTool
|
|
from app.agent.tools.factory import MoviePilotToolFactory
|
|
from app.agent.tools.impl.ask_user_choice import AskUserChoiceInput, AskUserChoiceTool
|
|
from app.agent.tools.impl.send_local_file import SendLocalFileTool
|
|
from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool
|
|
from app.core.plugin import PluginManager
|
|
from app.utils.singleton import Singleton
|
|
|
|
|
|
class DemoAgentTool(MoviePilotTool):
|
|
"""测试用插件工具。"""
|
|
|
|
name: str = "demo_agent_tool"
|
|
description: str = "Demo agent tool for tests."
|
|
|
|
async def run(self, **kwargs) -> str:
|
|
"""返回测试结果。"""
|
|
return "ok"
|
|
|
|
|
|
class DemoMessageAgentTool(DemoAgentTool):
|
|
"""测试用消息发送插件工具。"""
|
|
|
|
name: str = "demo_message_agent_tool"
|
|
sends_message: bool = True
|
|
|
|
|
|
@pytest.fixture
|
|
def plugin_manager() -> Iterator[PluginManager]:
|
|
"""构造隔离的插件管理器实例,避免单例缓存污染其它用例。"""
|
|
Singleton._instances.pop((PluginManager, (), frozenset()), None)
|
|
manager = PluginManager()
|
|
yield manager
|
|
Singleton._instances.pop((PluginManager, (), frozenset()), None)
|
|
|
|
|
|
def _build_plugin(
|
|
tools: list[type[MoviePilotTool]],
|
|
state: bool = True,
|
|
calls: Optional[list[int]] = None,
|
|
) -> SimpleNamespace:
|
|
"""构造仅包含 Agent 工具接口的插件实例。"""
|
|
|
|
def get_agent_tools() -> list[type[MoviePilotTool]]:
|
|
"""返回测试预设的工具类列表。"""
|
|
if calls is not None:
|
|
calls.append(1)
|
|
return tools
|
|
|
|
return SimpleNamespace(
|
|
plugin_name="Demo Plugin",
|
|
get_state=lambda: state,
|
|
get_agent_tools=get_agent_tools,
|
|
)
|
|
|
|
|
|
def _schema_properties(args_schema: Type[BaseModel]) -> dict:
|
|
"""返回工具输入模型的 JSON Schema 属性。"""
|
|
return args_schema.model_json_schema().get("properties", {})
|
|
|
|
|
|
def test_agent_tool_schemas_do_not_expose_explanation_parameter() -> None:
|
|
"""仓库内置 Agent 工具和中间件输入模型不应暴露 explanation 参数。"""
|
|
tool_classes = [
|
|
*MoviePilotToolFactory.BUILTIN_TOOL_CLASSES,
|
|
AskUserChoiceTool,
|
|
SendLocalFileTool,
|
|
SendVoiceMessageTool,
|
|
]
|
|
middleware_schemas = [
|
|
SkillToolInput,
|
|
QueryActivityLogInput,
|
|
]
|
|
|
|
for tool_class in tool_classes:
|
|
args_schema = getattr(tool_class, "args_schema", None)
|
|
if args_schema is None:
|
|
continue
|
|
assert "explanation" not in _schema_properties(args_schema), tool_class.name
|
|
|
|
for args_schema in middleware_schemas:
|
|
assert "explanation" not in _schema_properties(args_schema), args_schema.__name__
|
|
|
|
|
|
def test_ask_user_choice_option_schema_does_not_expose_description() -> None:
|
|
"""询问用户意图工具的选项参数不应暴露 description 字段。"""
|
|
schema = AskUserChoiceInput.model_json_schema()
|
|
option_schema = schema["$defs"]["UserChoiceOptionInput"]
|
|
|
|
assert "description" not in option_schema["properties"]
|
|
assert option_schema["required"] == ["label", "value"]
|
|
|
|
|
|
def test_plugin_agent_tools_are_cached(plugin_manager: PluginManager) -> None:
|
|
"""插件智能体工具注册表应缓存,避免同一轮启动反复询问插件实例。"""
|
|
calls: list[int] = []
|
|
plugin_manager.running_plugins["DemoPlugin"] = _build_plugin(
|
|
[DemoAgentTool], calls=calls
|
|
)
|
|
|
|
first_result = plugin_manager.get_plugin_agent_tools()
|
|
second_result = plugin_manager.get_plugin_agent_tools()
|
|
|
|
assert len(calls) == 1
|
|
assert first_result == second_result
|
|
assert first_result[0]["tools"] == [DemoAgentTool]
|
|
|
|
|
|
def test_plugin_agent_tools_cache_returns_copy(plugin_manager: PluginManager) -> None:
|
|
"""缓存命中时应返回副本,调用方修改结果不应污染注册表缓存。"""
|
|
plugin_manager.running_plugins["DemoPlugin"] = _build_plugin([DemoAgentTool])
|
|
|
|
first_result = plugin_manager.get_plugin_agent_tools()
|
|
first_result[0]["tools"].append(DemoMessageAgentTool)
|
|
|
|
second_result = plugin_manager.get_plugin_agent_tools()
|
|
|
|
assert second_result[0]["tools"] == [DemoAgentTool]
|
|
|
|
|
|
def test_plugin_agent_tools_cache_can_be_cleared(
|
|
plugin_manager: PluginManager,
|
|
) -> None:
|
|
"""清理缓存后应重新读取插件当前声明的智能体工具。"""
|
|
tools = [DemoAgentTool]
|
|
calls: list[int] = []
|
|
plugin_manager.running_plugins["DemoPlugin"] = _build_plugin(tools, calls=calls)
|
|
|
|
assert plugin_manager.get_plugin_agent_tools()[0]["tools"] == [DemoAgentTool]
|
|
tools.append(DemoMessageAgentTool)
|
|
assert plugin_manager.get_plugin_agent_tools()[0]["tools"] == [DemoAgentTool]
|
|
|
|
plugin_manager.clear_plugin_agent_tools_cache()
|
|
|
|
assert plugin_manager.get_plugin_agent_tools()[0]["tools"] == [
|
|
DemoAgentTool,
|
|
DemoMessageAgentTool,
|
|
]
|
|
assert len(calls) == 2
|
|
|
|
|
|
def test_factory_reuses_plugin_registry_but_creates_new_tool_instances(
|
|
plugin_manager: PluginManager,
|
|
) -> None:
|
|
"""工具工厂可复用插件注册表缓存,但每次请求仍需创建新的工具实例。"""
|
|
calls: list[int] = []
|
|
plugin_manager.running_plugins["DemoPlugin"] = _build_plugin(
|
|
[DemoAgentTool], calls=calls
|
|
)
|
|
|
|
first_tools = MoviePilotToolFactory.create_tools(
|
|
session_id="session-1",
|
|
user_id="10001",
|
|
)
|
|
second_tools = MoviePilotToolFactory.create_tools(
|
|
session_id="session-2",
|
|
user_id="10002",
|
|
)
|
|
|
|
first_demo_tool = next(tool for tool in first_tools if tool.name == "demo_agent_tool")
|
|
second_demo_tool = next(tool for tool in second_tools if tool.name == "demo_agent_tool")
|
|
|
|
assert len(calls) == 1
|
|
assert first_demo_tool is not second_demo_tool
|
|
assert first_demo_tool._session_id == "session-1"
|
|
assert second_demo_tool._session_id == "session-2"
|
|
|
|
|
|
def test_factory_suppresses_plugin_message_tools_for_subagents(
|
|
plugin_manager: PluginManager,
|
|
) -> None:
|
|
"""子代理静默工具列表不应包含会直接向用户发消息的插件工具。"""
|
|
plugin_manager.running_plugins["DemoPlugin"] = _build_plugin(
|
|
[DemoAgentTool, DemoMessageAgentTool]
|
|
)
|
|
|
|
tools = MoviePilotToolFactory.create_tools(
|
|
session_id="session-1",
|
|
user_id="10001",
|
|
allow_message_tools=False,
|
|
)
|
|
tool_names = {tool.name for tool in tools}
|
|
|
|
assert "demo_agent_tool" in tool_names
|
|
assert "demo_message_agent_tool" not in tool_names
|