Files
xiaoli-cli/tests/test_plugins.py
T
Xiaoli Dev ff53a89754 feat: 重写沙箱 + 完整测试套件 (192 tests)
沙箱 v2 - 多层隔离:
- AST 静态分析: 30+ 危险模式检测
- 子进程隔离: 独立进程执行 + 资源限制
- 网络阻断: socket 模块替换
- 模块白名单: 30 个安全模块
- 文件系统限制: 临时目录隔离
- SandboxResult 数据类 + 执行历史

测试套件 (8 个模块, 192 个用例):
- test_sandbox.py:     AST检查(47) + 沙箱执行(15) + 安全拒绝(20) + 超时(2) + 模式对比(3)
- test_conversation.py: 响应解析(13) + 思考标记(5) + 工具提取(10)
- test_config.py:       配置加载/保存/缓存/损坏处理(8)
- test_history.py:      历史CRUD/持久化/边界(12)
- test_engines.py:      引擎管理/加载/切换(14)
- test_plugins.py:      code_editor(10) + code_search(7) + git_tools(4) + tool_manager(7)
- test_integration.py:  沙箱+对话(2) + 历史+对话(2) + 安全链(3) + 提示词(1)
2026-04-25 11:43:47 +08:00

241 lines
9.3 KiB
Python

"""插件系统测试"""
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# ═══════════════════════════════════════════════
# 1. code_editor 插件测试
# ═══════════════════════════════════════════════
class TestCodeEditor:
"""代码编辑器插件测试"""
@pytest.fixture(autouse=True)
def setup(self, tmp_dir):
sys.path.insert(0, os.path.join(PROJECT_ROOT, 'plugins'))
from code_editor import Plugin
self.plugin = Plugin()
self.tmp_dir = tmp_dir
def test_tool_info(self):
info = self.plugin.get_tool_info()
assert info['name'] == 'code_editor'
assert 'description' in info
def test_mcp_definition(self):
mcp = self.plugin.get_mcp_definition()
assert mcp['name'] == 'code_editor'
assert 'inputSchema' in mcp
assert 'operation' in mcp['inputSchema']['properties']
def test_convert_mcp_args(self):
args = self.plugin.convert_mcp_args({"operation": "edit", "path": "test.py"})
assert 'edit' in args
def test_create_file(self):
filepath = os.path.join(self.tmp_dir, 'test.py')
result = self.plugin.handle(f'create {filepath}\nprint("hello")')
assert os.path.exists(filepath)
with open(filepath) as f:
assert 'print("hello")' in f.read()
def test_read_range(self):
filepath = os.path.join(self.tmp_dir, 'read_test.py')
with open(filepath, 'w') as f:
f.write('line1\nline2\nline3\nline4\nline5\n')
result = self.plugin.handle(f'read_range {filepath} 2 4')
assert 'line2' in result
assert 'line3' in result
def test_edit_file(self):
filepath = os.path.join(self.tmp_dir, 'edit_test.py')
with open(filepath, 'w') as f:
f.write('def old():\n pass\n')
result = self.plugin.handle(f'edit {filepath} def old():\n pass <<<>>> def new():\n return True')
with open(filepath) as f:
content = f.read()
assert 'def new():' in content
assert 'return True' in content
def test_diff_files(self):
f1 = os.path.join(self.tmp_dir, 'a.py')
f2 = os.path.join(self.tmp_dir, 'b.py')
with open(f1, 'w') as f:
f.write('hello\nworld\n')
with open(f2, 'w') as f:
f.write('hello\nuniverse\n')
result = self.plugin.handle(f'diff {f1} {f2}')
assert result is not None # should produce diff output
def test_search_code(self):
filepath = os.path.join(self.tmp_dir, 'search.py')
with open(filepath, 'w') as f:
f.write('def foo():\n return 42\ndef bar():\n return foo()\n')
result = self.plugin.handle(f'search {self.tmp_dir} main *.py')
assert 'main' in result
def test_write_file(self):
filepath = os.path.join(self.tmp_dir, 'write.py')
result = self.plugin.handle(f'write {filepath}\nresult = 42\n')
assert os.path.exists(filepath)
with open(filepath) as f:
assert 'result = 42' in f.read()
def test_append_file(self):
filepath = os.path.join(self.tmp_dir, 'append.py')
with open(filepath, 'w') as f:
f.write('line1\n')
self.plugin.handle(f'append {filepath}\nline2\n')
with open(filepath) as f:
content = f.read()
assert 'line1' in content
assert 'line2' in content
# ═══════════════════════════════════════════════
# 2. code_search 插件测试
# ═══════════════════════════════════════════════
class TestCodeSearch:
"""代码搜索插件测试"""
@pytest.fixture(autouse=True)
def setup(self, tmp_dir):
sys.path.insert(0, os.path.join(PROJECT_ROOT, 'plugins'))
from code_search import Plugin
self.plugin = Plugin()
self.tmp_dir = tmp_dir
# 创建测试文件
with open(os.path.join(tmp_dir, 'main.py'), 'w') as f:
f.write('import os\ndef main():\n print("hello")\n\nif __name__ == "__main__":\n main()\n')
with open(os.path.join(tmp_dir, 'utils.py'), 'w') as f:
f.write('def helper():\n return 42\ndef process(data):\n return [x*2 for x in data]\n')
with open(os.path.join(tmp_dir, 'test_main.py'), 'w') as f:
f.write('# TODO: add tests\ndef test_main():\n assert True\n')
def test_tool_info(self):
info = self.plugin.get_tool_info()
assert info['name'] == 'code_search'
def test_structure(self):
result = self.plugin.handle(f'structure {self.tmp_dir}')
assert 'main.py' in result
assert 'utils.py' in result
def test_find(self):
result = self.plugin.handle(f'find {self.tmp_dir} def *.py')
assert 'main' in result or 'helper' in result or 'def' in result
def test_symbols(self):
result = self.plugin.handle(f'symbols {self.tmp_dir} main.py')
assert 'main' in result
def test_todo(self):
result = self.plugin.handle(f'todo {self.tmp_dir}')
assert 'TODO' in result
def test_stats(self):
result = self.plugin.handle(f'stats {self.tmp_dir}')
assert result is not None
def test_context(self):
filepath = os.path.join(self.tmp_dir, 'main.py')
result = self.plugin.handle(f'context {filepath} 2 2')
assert 'main' in result or 'def' in result
# ═══════════════════════════════════════════════
# 3. git_tools 插件测试
# ═══════════════════════════════════════════════
class TestGitTools:
"""Git 工具插件测试"""
@pytest.fixture(autouse=True)
def setup(self, tmp_dir):
sys.path.insert(0, os.path.join(PROJECT_ROOT, 'plugins'))
from git_tools import Plugin
self.plugin = Plugin()
self.tmp_dir = tmp_dir
def test_tool_info(self):
info = self.plugin.get_tool_info()
assert info['name'] == 'git_tools'
def test_mcp_definition(self):
mcp = self.plugin.get_mcp_definition()
assert mcp['name'] == 'git_tools'
assert 'inputSchema' in mcp
def test_status_non_git_dir(self):
result = self.plugin.handle(f'status')
# 在非 git 目录应返回错误或提示
assert result is not None
def test_help_text(self):
assert 'git' in self.plugin.usage.lower() or 'Git' in self.plugin.usage
# ═══════════════════════════════════════════════
# 4. unified_tool_manager 测试
# ═══════════════════════════════════════════════
class TestUnifiedToolManager:
"""统一工具管理器测试"""
@pytest.fixture(autouse=True)
def setup(self, plugins_dir, skills_dir):
from unified_tool_manager import UnifiedToolManager
self.manager = UnifiedToolManager()
self.plugins_dir = plugins_dir
self.skills_dir = skills_dir
def test_initialize(self):
self.manager.initialize(self.plugins_dir, self.skills_dir)
assert len(self.manager._tools) > 0
def test_tools_registered(self):
self.manager.initialize(self.plugins_dir, self.skills_dir)
assert 'test_tool' in self.manager._tools
def test_protocol_map(self):
self.manager.initialize(self.plugins_dir, self.skills_dir)
assert 'test_tool' in self.manager._protocol_map
def test_execute_plugin(self):
self.manager.initialize(self.plugins_dir, self.skills_dir)
result = self.manager.execute('test_tool', 'hello')
assert result is not None
assert 'hello' in str(result) or 'test' in str(result).lower()
def test_execute_nonexistent(self):
self.manager.initialize(self.plugins_dir, self.skills_dir)
result = self.manager.execute('nonexistent_tool', 'args')
assert result is None or 'not found' in str(result).lower() or '未找到' in str(result)
def test_mcp_cache(self):
self.manager.initialize(self.plugins_dir, self.skills_dir)
cache = self.manager._mcp_tools_cache
assert cache is not None
assert len(cache) > 0
def test_empty_dirs(self, tmp_dir):
empty_plugins = os.path.join(tmp_dir, 'empty_p')
empty_skills = os.path.join(tmp_dir, 'empty_s')
os.makedirs(empty_plugins)
os.makedirs(empty_skills)
with open(os.path.join(empty_plugins, '__init__.py'), 'w') as f:
pass
with open(os.path.join(empty_skills, '__init__.py'), 'w') as f:
pass
mgr = __import__('unified_tool_manager').UnifiedToolManager()
mgr.initialize(empty_plugins, empty_skills)
assert len(mgr._tools) == 0