Files
AgentLab/backend/tests/test_sandbox_errors.py
ThearchyHelios 9d8b41430e 完善口径管理、主动调查与后台任务,修复通用问题
- 完善口径编辑、只读试运行、统一质量验证与执行语义指纹
- 增加可执行指标约束、有预算的主动调查和过程评测
- 提取发布检查、证据读取与起草服务,支持任务停止、重试和检查点恢复
- 增加版本传递影响分析,修复并发覆盖、敏感值遮罩、异常评分及页面状态问题
- 修复 SQLite 复合查询追溯和 microVM 可选依赖缺失处理
- 纳入当前设计文档及部署配置调整

验证:相关主回归 3822 项通过,4 项跳过、8 项预期失败;补充回归通过。
前端构建、文案检查及 git diff --check 通过。
全量检查仍有环境权限、PostgreSQL 可选依赖和既有部署配置、SQLite 文件基线差异导致的未通过项。
2026-11-06 17:14:45 +08:00

117 lines
4.7 KiB
Python

"""沙箱起不来时的报错:说原因,不带 Python 异常类名(SPEC §3.1)。
四个后端起进程、连 VM 失败时以前都写 f"{type(e).__name__}: {e}",代码节点再把它
拼进首行,界面上的红字就成了「代码执行失败(exit=0):FileNotFoundError: [Errno 2]
No such file or directory: …」。原始异常留在日志里,排查时照样找得到。
"""
from __future__ import annotations
import asyncio
import re
import sys
import types
import pytest
from app.engine.context import NodeContext, NodeError, RunContext
from app.engine.nodes.tools import run_code
from app.engine.schema import GraphNode, GraphSpec
from app.sandbox import interpreter
from app.sandbox.bubblewrap_sandbox import BubblewrapSandbox
from app.sandbox.local_sandbox import LocalSandbox
from app.sandbox.microvm_sandbox import MicroVMSandbox
from app.sandbox.seatbelt_sandbox import SeatbeltSandbox
_CLASS_NAME = re.compile(r"\b[A-Z]\w*(?:Error|Exception)\b")
def _broken_interpreter(tmp_path) -> str:
"""一个存在、可执行,但 shebang 指向不存在的解释器的文件:which 找得到,exec 起不来。"""
fake = tmp_path / "python-broken"
fake.write_text("#!/nonexistent/interpreter\n")
fake.chmod(0o755)
return str(fake)
def test_a_code_node_whose_interpreter_cannot_start_says_why(tmp_path, monkeypatch) -> None:
import app.engine.nodes.tools as tools
monkeypatch.setattr(interpreter, "python_argv", lambda: [_broken_interpreter(tmp_path)])
local = LocalSandbox()
class _Manager:
async def run(self, code, *, backend=None, **kwargs): # noqa: ANN001, ANN003, ANN201
return await local.run(code, **kwargs)
monkeypatch.setattr(tools, "sandbox_manager", _Manager())
node = GraphNode(id="calc", type="code",
data={"label": "计算", "config": {"language": "python", "code": "print(1)",
"isolation": "fast"}})
ctx = NodeContext(node=node, run=RunContext(run_id="r1", thread_id="t1",
spec=GraphSpec(nodes=[node], edges=[])))
with pytest.raises(NodeError) as caught:
asyncio.run(run_code({"vars": {}}, ctx))
text = str(caught.value)
assert text.startswith("代码执行失败"), text
assert not _CLASS_NAME.search(text), text
async def _boom(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
raise PermissionError(13, "Permission denied", "/usr/bin/sandbox-exec")
@pytest.mark.parametrize("backend", [LocalSandbox, SeatbeltSandbox, BubblewrapSandbox])
def test_a_process_that_cannot_spawn_has_no_class_name(backend, monkeypatch) -> None:
if backend is not LocalSandbox:
monkeypatch.setattr(backend, "available", staticmethod(lambda: True))
monkeypatch.setattr(asyncio, "create_subprocess_exec", _boom)
result = asyncio.run(backend().run("print(1)"))
assert not result.ok
assert result.error and not _CLASS_NAME.search(result.error), result.error
def _microvm(monkeypatch) -> MicroVMSandbox:
sdk = types.ModuleType("microsandbox")
sdk.ExecTimeoutError = TimeoutError
monkeypatch.setitem(sys.modules, "microsandbox", sdk)
async def nothing(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
return None
monkeypatch.setattr(MicroVMSandbox, "available", staticmethod(lambda: True))
monkeypatch.setattr(MicroVMSandbox, "_reap_idle", nothing)
monkeypatch.setattr(MicroVMSandbox, "_destroy", nothing)
return MicroVMSandbox()
def test_a_microvm_that_cannot_boot_has_no_class_name(monkeypatch) -> None:
async def no_vm(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
raise RuntimeError("虚拟化框架拒绝了这次启动")
vm = _microvm(monkeypatch)
monkeypatch.setattr(MicroVMSandbox, "_lease", no_vm)
result = asyncio.run(vm.run("print(1)"))
assert result.error and result.error.startswith("microVM 启动失败"), result.error
assert not _CLASS_NAME.search(result.error), result.error
def test_a_microvm_that_drops_mid_exec_has_no_class_name(monkeypatch) -> None:
async def write(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
return None
async def gone(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
raise ConnectionResetError(54, "Connection reset by peer")
lease = types.SimpleNamespace(vm=types.SimpleNamespace(fs=types.SimpleNamespace(write=write, mkdir=write),
exec=gone))
async def leased(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
return lease
vm = _microvm(monkeypatch)
monkeypatch.setattr(MicroVMSandbox, "_lease", leased)
result = asyncio.run(vm.run("print(1)"))
assert result.error and not _CLASS_NAME.search(result.error), result.error