fix: avoid blocking event loop during plugin install

This commit is contained in:
jxxghp
2026-05-21 09:16:42 +08:00
parent 2109d323ae
commit ee9eced2f1
4 changed files with 102 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from app.agent.tools.impl.install_plugin import InstallPluginTool
from app.agent.tools.impl._plugin_tool_utils import install_plugin_runtime
from app.agent.tools.impl.query_installed_plugins import QueryInstalledPluginsTool
from app.agent.tools.impl.query_market_plugins import QueryMarketPluginsTool
from app.agent.tools.impl.query_plugin_config import QueryPluginConfigTool
@@ -170,6 +171,54 @@ class TestAgentPluginTools(unittest.TestCase):
"DemoPlugin", "https://example.com/market", force=False
)
def test_install_plugin_runtime_reloads_in_threadpool(self):
plugin_manager = MagicMock()
plugin_manager.get_plugin_ids.return_value = ["DemoPlugin"]
plugin_helper = MagicMock()
plugin_helper.async_install_reg = AsyncMock(return_value=True)
config_oper = MagicMock()
config_oper.get.return_value = ["DemoPlugin"]
calls = []
async def fake_to_thread(func, *args, **kwargs):
calls.append((func, args, kwargs))
return None
with patch(
"app.agent.tools.impl._plugin_tool_utils.SystemConfigOper",
return_value=config_oper,
), patch(
"app.agent.tools.impl._plugin_tool_utils.PluginManager",
return_value=plugin_manager,
), patch(
"app.agent.tools.impl._plugin_tool_utils.PluginHelper",
return_value=plugin_helper,
), patch(
"app.agent.tools.impl._plugin_tool_utils.reload_plugin_runtime",
) as reload_runtime, patch(
"app.agent.tools.impl._plugin_tool_utils.asyncio.to_thread",
side_effect=fake_to_thread,
):
success, message, refreshed_only = asyncio.run(
install_plugin_runtime(
"DemoPlugin",
"https://example.com/market",
force=False,
)
)
self.assertTrue(success)
self.assertEqual("插件已存在,已刷新加载", message)
self.assertTrue(refreshed_only)
plugin_helper.async_install_reg.assert_awaited_once_with(
pid="DemoPlugin",
repo_url="https://example.com/market",
)
self.assertEqual(1, len(calls))
self.assertEqual(reload_runtime, calls[0][0])
self.assertEqual(("DemoPlugin",), calls[0][1])
self.assertEqual({}, calls[0][2])
def test_uninstall_plugin_uninstalls_installed_candidate(self):
tool = UninstallPluginTool(session_id="session-1", user_id="10001")
installed_plugin = self._market_plugin(

View File

@@ -1,3 +1,4 @@
import asyncio
import sys
import tempfile
import threading
@@ -360,3 +361,37 @@ class PluginHelperTest(TestCase):
self.assertIn("已自动恢复主程序依赖", message)
self.assertEqual(1, len(repair_commands))
self.assertIn("runtime-constraints-", repair_commands[0][-1])
def test_async_pip_install_runs_in_threadpool(self):
"""
验证异步安装路径会把同步 pip 安装派发到线程池,避免阻塞事件循环。
"""
try:
from app.helper.plugin import PluginHelper
except ModuleNotFoundError as exc:
self.skipTest(f"missing dependency: {exc}")
helper = PluginHelper()
requirements_file = Path("/tmp/demo-requirements.txt")
find_links_dirs = [Path("/tmp/demo-wheels")]
calls = []
async def run_install():
return await helper._PluginHelper__async_pip_install_with_fallback(
requirements_file,
find_links_dirs
)
async def fake_to_thread(func, *args, **kwargs):
calls.append((func, args, kwargs))
return True, "ok"
with patch("app.helper.plugin.asyncio.to_thread", side_effect=fake_to_thread):
success, message = asyncio.run(run_install())
self.assertTrue(success)
self.assertEqual("ok", message)
self.assertEqual(1, len(calls))
self.assertEqual(helper.pip_install_with_fallback, calls[0][0])
self.assertEqual((requirements_file, find_links_dirs), calls[0][1])
self.assertEqual({}, calls[0][2])