feat(system):优化后端
1.新增后端测试 2.增加了后端的加密 3.增加了i18n(国际化)
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
"""``utils.mcp_helper`` 的纯逻辑路径:构建 toolset、合并、列出工具。
|
||||
|
||||
由于 stdio MCP 实例化会立即解析 command/args,但不会主动启动子进程(只在 ``async with``
|
||||
进入后才连),因此 ``build_mcp_toolsets`` 可以在测试里直接验证。
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from kilostar.utils import mcp_helper
|
||||
|
||||
|
||||
# ─── build_mcp_toolsets ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_build_mcp_toolsets_when_mcp_unavailable_returns_empty(monkeypatch):
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", False)
|
||||
assert mcp_helper.build_mcp_toolsets({"a": {"transport": "stdio"}}) == []
|
||||
|
||||
|
||||
def test_build_mcp_toolsets_skips_unsupported_transport():
|
||||
configs = {"weird": {"transport": "ftp", "name": "weird"}}
|
||||
assert mcp_helper.build_mcp_toolsets(configs) == []
|
||||
|
||||
|
||||
def test_build_mcp_toolsets_creates_one_per_supported_config():
|
||||
configs = {
|
||||
"fs": {
|
||||
"name": "fs",
|
||||
"transport": "stdio",
|
||||
"command": "echo",
|
||||
"args": ["hi"],
|
||||
"tool_prefix": "fs",
|
||||
},
|
||||
"remote_sse": {
|
||||
"name": "remote_sse",
|
||||
"transport": "sse",
|
||||
"url": "http://localhost:9000/sse",
|
||||
},
|
||||
"remote_http": {
|
||||
"name": "remote_http",
|
||||
"transport": "http",
|
||||
"url": "http://localhost:9001",
|
||||
},
|
||||
}
|
||||
toolsets = mcp_helper.build_mcp_toolsets(configs)
|
||||
assert len(toolsets) == 3
|
||||
ids = {getattr(t, "id", None) for t in toolsets}
|
||||
assert ids == {"fs", "remote_sse", "remote_http"}
|
||||
|
||||
|
||||
# ─── get_mcp_toolsets_from_gsm ───────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_get_mcp_toolsets_from_gsm_returns_empty_when_unavailable(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", False)
|
||||
assert await mcp_helper.get_mcp_toolsets_from_gsm() == []
|
||||
|
||||
|
||||
async def test_get_mcp_toolsets_from_gsm_swallows_errors_with_no_actor(monkeypatch):
|
||||
"""没有注册 actor 时返回空列表而非抛出。"""
|
||||
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", True)
|
||||
|
||||
async def boom(*_a, **_kw):
|
||||
raise ValueError("no actor")
|
||||
|
||||
from kilostar.core.global_state_machine import gsm_snapshot as snap_mod
|
||||
|
||||
monkeypatch.setattr(snap_mod, "fetch_snapshot", boom)
|
||||
assert await mcp_helper.get_mcp_toolsets_from_gsm() == []
|
||||
|
||||
|
||||
async def test_get_mcp_toolsets_from_gsm_uses_configs_via_snapshot(monkeypatch):
|
||||
"""注入一个返回 mcp 配置的 snapshot,验证最终走通到 build_mcp_toolsets。"""
|
||||
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", True)
|
||||
|
||||
from kilostar.core.global_state_machine import gsm_snapshot as snap_mod
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import GSMSnapshot
|
||||
|
||||
snap = GSMSnapshot(
|
||||
version=1,
|
||||
mcp_servers={
|
||||
"fs": {
|
||||
"name": "fs",
|
||||
"transport": "stdio",
|
||||
"command": "echo",
|
||||
"args": ["hi"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def _fake_fetch(**_kw):
|
||||
return snap
|
||||
|
||||
monkeypatch.setattr(snap_mod, "fetch_snapshot", _fake_fetch)
|
||||
|
||||
toolsets = await mcp_helper.get_mcp_toolsets_from_gsm()
|
||||
assert len(toolsets) == 1
|
||||
assert getattr(toolsets[0], "id", None) == "fs"
|
||||
|
||||
|
||||
# ─── get_all_toolsets_for_scope ──────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_get_all_toolsets_for_scope_merges_local_and_mcp(monkeypatch):
|
||||
"""本地 toolset 列表和 mcp toolset 列表都应该被拼接。"""
|
||||
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", True)
|
||||
|
||||
local_a = MagicMock(name="local-system")
|
||||
local_b = MagicMock(name="local-personal")
|
||||
|
||||
from kilostar.core.global_state_machine import gsm_snapshot as snap_mod
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import GSMSnapshot
|
||||
|
||||
snap = GSMSnapshot(version=1, mcp_servers={})
|
||||
|
||||
async def _fake_fetch(**_kw):
|
||||
return snap
|
||||
|
||||
monkeypatch.setattr(snap_mod, "fetch_snapshot", _fake_fetch)
|
||||
monkeypatch.setattr(
|
||||
snap_mod,
|
||||
"build_toolsets_for_scope",
|
||||
lambda s, scope: [local_a, local_b],
|
||||
)
|
||||
|
||||
result = await mcp_helper.get_all_toolsets_for_scope("control_node")
|
||||
assert result == [local_a, local_b]
|
||||
|
||||
|
||||
async def test_get_all_toolsets_for_scope_local_failure_does_not_block_mcp(
|
||||
monkeypatch,
|
||||
):
|
||||
"""本地 toolset 拉取失败时仍然要返回 mcp toolset。"""
|
||||
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", True)
|
||||
|
||||
from kilostar.core.global_state_machine import gsm_snapshot as snap_mod
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import GSMSnapshot
|
||||
|
||||
# local 路径:fetch 成功但 build_toolsets_for_scope 抛错
|
||||
snap = GSMSnapshot(
|
||||
version=1,
|
||||
mcp_servers={
|
||||
"fs": {
|
||||
"name": "fs",
|
||||
"transport": "stdio",
|
||||
"command": "echo",
|
||||
"args": ["hi"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async def _fake_fetch(**_kw):
|
||||
return snap
|
||||
|
||||
def _broken_build(*_a, **_kw):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(snap_mod, "fetch_snapshot", _fake_fetch)
|
||||
monkeypatch.setattr(snap_mod, "build_toolsets_for_scope", _broken_build)
|
||||
|
||||
result = await mcp_helper.get_all_toolsets_for_scope("control_node")
|
||||
assert len(result) == 1
|
||||
assert getattr(result[0], "id", None) == "fs"
|
||||
|
||||
|
||||
# ─── list_mcp_tools_for_configs ──────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_list_mcp_tools_for_configs_returns_tool_names(monkeypatch):
|
||||
"""模拟 server.get_tools() 返回若干带 name 的工具,结果应被抽取出来。"""
|
||||
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", True)
|
||||
|
||||
class _FakeTool:
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
class _FakeServer:
|
||||
def __init__(self, server_id: str, tools):
|
||||
self.id = server_id
|
||||
self._tools = tools
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def get_tools(self):
|
||||
return self._tools
|
||||
|
||||
fake_servers = [_FakeServer("fs", [_FakeTool("read"), _FakeTool("write")])]
|
||||
monkeypatch.setattr(
|
||||
mcp_helper, "build_mcp_toolsets", lambda configs: list(fake_servers)
|
||||
)
|
||||
|
||||
result = await mcp_helper.list_mcp_tools_for_configs(
|
||||
{"fs": {"name": "fs", "transport": "stdio"}}
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0]["server_id"] == "fs"
|
||||
assert result[0]["tools"] == ["read", "write"]
|
||||
assert "error" not in result[0]
|
||||
|
||||
|
||||
async def test_list_mcp_tools_for_configs_records_error_on_failure(monkeypatch):
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", True)
|
||||
|
||||
class _BrokenServer:
|
||||
def __init__(self) -> None:
|
||||
self.id = "broken"
|
||||
|
||||
async def __aenter__(self):
|
||||
raise RuntimeError("connect fail")
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def get_tools(self): # pragma: no cover - 不会被调用
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(
|
||||
mcp_helper, "build_mcp_toolsets", lambda configs: [_BrokenServer()]
|
||||
)
|
||||
result = await mcp_helper.list_mcp_tools_for_configs(
|
||||
{"broken": {"name": "broken", "transport": "stdio"}}
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0]["server_id"] == "broken"
|
||||
assert result[0]["tools"] == []
|
||||
assert "connect fail" in result[0]["error"]
|
||||
|
||||
|
||||
async def test_list_mcp_tools_for_configs_when_mcp_unavailable(monkeypatch):
|
||||
monkeypatch.setattr(mcp_helper, "_MCP_AVAILABLE", False)
|
||||
assert await mcp_helper.list_mcp_tools_for_configs({"x": {}}) == []
|
||||
Reference in New Issue
Block a user