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)
This commit is contained in:
Xiaoli Dev committed 2026-04-25 11:43:47 +08:00
1 parent b1b2097311
commit ff53a89754
10 files changed
+2205 -72

No files matched your search

+154
View File
@@ -0,0 +1,154 @@
"""集成测试 - 模块间协作"""
import os
import sys
import json
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from xiaoli.sandbox import Sandbox
from xiaoli.conversation import parse_response, extract_tool_calls
from xiaoli.conversation.history import History
class TestSandboxConversationIntegration:
"""沙箱 + 对话解析 集成测试"""
def test_sandbox_result_parsed_as_tool_output(self):
"""沙箱执行结果可以正确传递给对话系统"""
sandbox = Sandbox(timeout=10, mode='threaded')
result = sandbox.run('result = [x**2 for x in range(5)]')
assert result['success']
# 模拟工具结果注入对话
tool_msg = f"工具执行结果:\n{result['result']}"
text, data = parse_response(tool_msg)
assert data is None # 纯文本,无工具调用
assert '0' in text and '16' in text
sandbox.cleanup()
def test_sandbox_failure_triggers_error_handling(self):
"""沙箱失败时应正确处理"""
sandbox = Sandbox(timeout=10, mode='threaded')
result = sandbox.run('import os') # should fail
assert not result['success']
# 错误信息应可解析
assert result['result']
assert len(result['result']) > 0
sandbox.cleanup()
class TestHistoryConversationIntegration:
"""历史 + 对话解析 集成测试"""
def test_full_conversation_roundtrip(self, history_dir):
"""完整的对话存取流程"""
h = History(history_dir)
# 模拟对话
h.add("user", "帮我写一个排序函数")
h.add("assistant", "好的,这是一个快速排序:\n```python\ndef quicksort(arr):\n ...\n```")
h.add("user", "能改成归并排序吗?")
# 保存
assert h.save("sorting_chat")
# 加载到新实例
h2 = History(history_dir)
assert h2.load("sorting_chat")
assert len(h2.messages) == 3
assert "quicksort" in h2.messages[1]['content']
def test_conversation_with_tool_calls(self, history_dir):
"""带工具调用的对话历史"""
h = History(history_dir)
# 用户请求
h.add("user", "查看 main.py 的第 10-20 行")
# AI 的工具调用
tool_call = '{"action":"use_tool","tool":"code_editor","args":"read_range main.py 10 20"}'
text, data = parse_response(tool_call)
assert data is not None
calls = extract_tool_calls(data)
assert len(calls) == 1
assert calls[0]['tool'] == 'code_editor'
# 记录工具结果
h.add("assistant", f"工具调用: {json.dumps(data)}")
h.add("assistant", "工具执行结果:\n line 10: def main():")
assert len(h.messages) == 3
class TestSandboxSecurityIntegration:
"""沙箱安全 + 工具链 集成测试"""
def test_safe_code_executes_in_chain(self):
"""安全代码在工具链中正常执行"""
sandbox = Sandbox(timeout=10, mode='threaded')
# 步骤 1: 数学计算
r1 = sandbox.run('import math\nresult = math.factorial(10)')
assert r1['success']
assert '3628800' in r1['result']
# 步骤 2: 使用上一步结果
r2 = sandbox.run(
'result = f"factorial(10) = {val}"',
variables={'val': r1['result']}
)
assert r2['success']
assert '3628800' in r2['result']
sandbox.cleanup()
def test_blocked_code_stops_chain(self):
"""被阻止的代码应中断工具链"""
sandbox = Sandbox(timeout=10, mode='threaded')
r = sandbox.run('import os\nresult = os.listdir(".")')
assert not r['success']
# 后续步骤不应执行
assert r['result']
sandbox.cleanup()
def test_subprocess_isolation(self):
"""子进程模式下文件系统隔离"""
sandbox = Sandbox(timeout=10, mode='subprocess')
result = sandbox.run(
'import tempfile\nresult = tempfile.gettempdir()'
)
# 子进程应使用沙箱临时目录
if result['success']:
assert 'xiaoli_sandbox' in result['result'] or result['success']
sandbox.cleanup()
class TestPromptIntegration:
"""系统提示词集成测试"""
def test_prompt_builder(self):
from xiaoli import prompt as prompt_builder
# 无工具
p1 = prompt_builder.build()
assert '小狸' in p1
assert '编程' in p1 or 'code' in p1.lower()
# 带工具
p2 = prompt_builder.build(tool_prompts="- code_editor: edit code")
assert 'code_editor' in p2
# 带 tool_search
p3 = prompt_builder.build(has_tool_search=True)
assert 'tool_search' in p3
# Clawli 模式
p4 = prompt_builder.build(is_clawli=True)
assert 'Clawli' in p4 or 'clawli' in p4 or '远程' in p4