Files
codex-app-server/cloud/tests/test_docker_sandbox.py
T
host f39ed3654e ci3: lint/mypy 第二阶段清理 + 覆盖率 76% -> 78%(门槛不动)
基底:#251 头 174b631(堆叠于 #242 feat-ci-expand 之上),
基线 origin/main aebc6238;本轨不改 #251 已定语义、不动
workflow 门槛、不删检查、不加 skip。

lint(ruff E,F@120):109 -> 10
- F401 43 -> 0:删未用导入;try/except 双绑定的名字出现
  “打地鼠”(删 except 分支后 try 分支又被标出),两分支
  均已核对正文无引用后清掉;MCP 三模块的 Error 名不删,
  改走 importlib 属性绑定(见下),两路径绑定都保留。
- E702 40 -> 0:全在 host.py main() 的 argparse 段,纯分行,
  tokenize 按分号 token 拆(含 2377 行多行语句只断首个分号);
  一次 5 空格缩进失误已回滚重做并以 ast + 全量 host 测试验证。
- E501 13 -> 0:host.py 5 项随 E702 分行连带清掉;mcp_server
  3 项把描述拆成相邻字面量(AST 比对 24 条工具描述与原值
  完全一致,注意 ruff 按东亚宽度计长,CJK 算 2);test_host
  5 项断开 git commit 参数表。
- E402 8 -> 10:生产侧 2 项清掉(db.py _re、github_app
  _base64 移到顶部,之前无引用、无重名);剩余 10 项全是
  tests/ 的 sys.path 前置 import(6 项旧 + 4 个新测试文件
  沿用同一既定模式),移上去会破坏单文件直跑,有意保留。
- F841 4 -> 0:只去绑定、不删调用(run_task、DB 入队、
  tarfile 上下文、git 提交),各有注释与原测试/新测试覆盖;
  host dispatch_from_branch 新增真实 git 仓库回归测试。
- F811 1 -> 0:server 本地 _row_get 与 peer_messages.row_get
  逐行相同,删本地定义,全部调用点解析到同一 import;
  新增 test_server_row_get(同一对象 + 三路行为)。

mypy(生产范围):24 -> 0,零新增 type: ignore
- no-redef 9 -> 0:MCP 三模块 try/except 两次 from-import
  改 importlib.import_module + 属性绑定(cloud 优先、flat
  copy 回落、内层 ImportError 同样回落,语义与原来一致;
  flat-copy 子进程测试证实回落后 __module__ 为顶层名);
  server _row_get 随 F811 消除。
- arg-type 10 -> 0:harness2_tasks shared 改 TypedDict
  逐键精确类型;CodexProbeAdapter 补 no-op validate_tree
  (_adapter_validate 本就跳过缺失方法,行为一致)。
- var-annotated 3 -> 0:sub2api _SHARED_ADMIN/_login_slots、
  verify-multiuser RESULTS 加精确注解。
- assignment/misc 2 -> 0:marker_tools ZoneInfo 先声明
  Any 再 try import(草稿先验 mypy 通过)。

bandit:总数 286 不变,High 仍 4 项,全在 host.py,本轨不改
(属第 3 项白名单轨范围),只做 triage:
- 1319 run_verify:无插值,整串验测命令,管道/&& 须 shell
  语义(新增 test_host_run_verify 断言管道与 && 真实生效)。
- 1497 notify:模板来自算子配置,三处插值全部 shlex.quote,
  证据为既有 test_notify_failure_quotes_shell_metacharacters
  (; touch 与 $(touch ) 不执行)。
- 2099/2103 goal verify:无插值,任务配置验测串,同 run_verify。
- 无新增 High;pip-audit 0 漏洞。

覆盖率(同口径三套件 coverage 计量):75.96% -> 77.87%
(22392 行/缺 5385 -> 22437 行/缺 4963,显示 76% -> 78%)
- client.py 0% -> 90%(pipe 假子进程 + main 分支)
- run_parallel.py 0% -> 97%、migrate 包装 0% -> 95%
- playwright_mcp 37% -> 62%、imagegen_mcp 47% -> 79%、
  websearch_mcp 51% -> 86%、deploy.py 56% -> 77%
- verify-multiuser 0% -> 27%(仅 check/req helper;main()
  是主机侧真实多用户实弹验证,按门禁不建生产任务、
  不以 mock 冒充,仍为缺口)
- host.py 55% -> 57%(文件 2020 -> 2060 行系 E702 分行
  的计量口径变化,非代码膨胀)
- 未到 85%:门槛保持 75% 不动,不删低覆盖模块出统计口径;
  缺口主力仍是 server.py / worker.py / host.py 大文件,
  下一阶段须针对性深测,非本轨快赢可及。

验证:三套件 1576 -> 1629 项全过(tests 127、cloud/tests
1449 skipped=4、deploy/tests 53);ruff E9 通过;mypy 0;
bandit High 4(同上);pip-audit 0 漏洞;试跑产物已删,
提交后工作树干净。
2026-11-03 02:48:54 +00:00

422 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Docker 沙箱执行器单测:命令构造、安全挂载、超时与取消。
运行(在 ~/workspace/codex-app-server 下):
python3 -m unittest discover -s cloud/tests -p "test_docker_sandbox.py" -v
说明:全部用注入的假 docker runner,不调用真实 docker、不联网、
不跑真实模型回合;env 映射里的值只是占位假名,绝不是真实密钥。
"""
import json
import os
import sqlite3
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
try:
from cloud.sandbox import DockerExecutor
from cloud.server import init_db
from cloud.worker import Worker
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.sandbox import DockerExecutor
from cloud.server import init_db
from cloud.worker import Worker
def _ok_runner_factory(seen):
def fake(cmd, timeout=None):
seen.append(list(cmd))
return SimpleNamespace(
returncode=0,
stdout=json.dumps({"status": "completed", "text": "done"}),
stderr="")
return fake
def _insert_task(db_path, task_id, prompt="do work"):
now = time.time()
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat)"
" VALUES(?, 'p1', ?, 'queued', NULL, '[]', 0, 'tester', ?, ?, NULL)",
(task_id, prompt, now, now),
)
conn.commit()
finally:
conn.close()
def _task_status(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
row = conn.execute(
"SELECT status, error_text FROM tasks WHERE id = ?",
(task_id,)).fetchone()
finally:
conn.close()
return row
class DockerCommandTestCase(unittest.TestCase):
def _executor(self, seen, **kwargs):
kwargs.setdefault("runner", _ok_runner_factory(seen))
kwargs.setdefault("env", {"XCM_API_KEY": "placeholder-not-a-real-key"})
return DockerExecutor("/fake/host.py", **kwargs)
def test_command_mounts_only_task_worktree_and_ro_code(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen)
cmd = ex.build_run_command(
{"id": "t1", "worktree": "/tmp/wt-t1"},
["python3", "/app/host.py", "status", "--task", "t1"])
mounts = [cmd[i + 1] for i, a in enumerate(cmd) if a == "-v"]
self.assertEqual(mounts, ["/tmp/wt-t1:/work", "/fake:/app:ro"])
# 绝不挂载宿主根目录。
self.assertNotIn("/:/work", mounts)
self.assertNotIn("/:/app:ro", mounts)
self.assertIn("--rm", cmd)
self.assertIn("/tmp/wt-t1:/work", cmd)
def test_command_resource_limits_and_non_root_user(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen, cpus="1", memory="512m",
user="1000:1000")
cmd = ex.build_run_command(
{"id": "t1", "worktree": "/tmp/wt"},
["echo", "hi"])
self.assertEqual(cmd[cmd.index("--cpus") + 1], "1")
self.assertEqual(cmd[cmd.index("--memory") + 1], "512m")
self.assertEqual(cmd[cmd.index("--user") + 1], "1000:1000")
self.assertNotEqual(cmd[cmd.index("--user") + 1], "0")
self.assertNotEqual(cmd[cmd.index("--user") + 1], "root")
def test_command_defaults_are_2cpu_2g(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen)
cmd = ex.build_run_command(
{"id": "t1", "worktree": "/tmp/wt"}, ["echo", "hi"])
self.assertEqual(cmd[cmd.index("--cpus") + 1], "2")
self.assertEqual(cmd[cmd.index("--memory") + 1], "2g")
def test_container_name_sanitized(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen)
self.assertEqual(ex.container_name({"id": "a b/c"}),
"codex-a-b-c")
cmd = ex.build_run_command(
{"id": "a b/c", "worktree": "/tmp/wt"}, ["echo", "hi"])
self.assertIn("codex-a-b-c", cmd)
self.assertEqual(cmd[cmd.index("--name") + 1], "codex-a-b-c")
def test_env_name_only_no_value_in_command(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen)
cmd = ex.build_run_command(
{"id": "t1", "worktree": "/tmp/wt"}, ["echo", "hi"])
joined = " ".join(cmd)
self.assertIn("-e", cmd)
self.assertIn("XCM_API_KEY", cmd)
# 值绝不出现在命令列表里(-e 只传名,docker 从宿主环境取值)。
self.assertNotIn("placeholder-not-a-real-key", joined)
self.assertNotIn("placeholder-not-a-real-key=", joined)
def test_missing_key_does_not_start_container(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py",
runner=_ok_runner_factory(seen), env={})
with self.assertRaises(RuntimeError) as ctx:
ex.dispatch_task({"id": "t1", "prompt": "hi",
"worktree": "/tmp/wt"})
self.assertIn("Missing environment variable", str(ctx.exception))
self.assertEqual(seen, [])
def test_missing_docker_does_not_call_runner(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which", return_value=None):
ex = self._executor(seen)
with self.assertRaises(RuntimeError) as ctx:
ex.dispatch_task({"id": "t1", "prompt": "hi",
"worktree": "/tmp/wt"})
self.assertIn("docker not available", str(ctx.exception))
self.assertEqual(seen, [])
def test_dispatch_and_run_shapes(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen)
dispatched = ex.dispatch_task(
{"id": "t1", "prompt": "do it", "worktree": "/tmp/wt"})
# run_task 调用本身有副作用(seen 记命令):只去绑定,不删调用。
ex.run_task(
{"id": "t1", "prompt": "again", "worktree": "/tmp/wt"})
self.assertEqual(dispatched["status"], "completed")
self.assertEqual(dispatched["executor"], "docker")
dispatch_cmd = seen[0]
self.assertIn("dispatch", dispatch_cmd)
self.assertIn("t1", dispatch_cmd)
self.assertIn("do it", dispatch_cmd)
run_cmd = seen[1]
self.assertIn("run", run_cmd)
self.assertIn("again", run_cmd)
def test_execute_is_readonly_status_not_dispatch(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen)
result = ex.execute({"id": "t1", "worktree": "/tmp/wt"})
self.assertEqual(result["status"], "completed")
self.assertIn("status", seen[0])
self.assertNotIn("dispatch", seen[0])
# run 在命令里会出现 "--name" 等,但内层命令不应是 run:
# 内层 host.py 命令的第一个动词是 status。
inner = seen[0][seen[0].index("codex-app-server:latest") + 1:]
self.assertEqual(inner[2], "status")
def test_timeout_kills_and_removes_container(self):
seen = []
def fake(cmd, timeout=None):
seen.append(list(cmd))
if len(cmd) > 1 and cmd[1] == "run":
raise subprocess.TimeoutExpired(cmd, timeout)
return SimpleNamespace(returncode=0, stdout="", stderr="")
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py", runner=fake,
env={"XCM_API_KEY": "placeholder"},
timeout=5)
result = ex.dispatch_task(
{"id": "t9", "prompt": "slow", "worktree": "/tmp/wt"})
self.assertEqual(result["status"], "failed")
self.assertIn("timeout", result["stdout"])
kinds = [(c[1], c[2]) for c in seen if len(c) > 2]
self.assertIn(("kill", "codex-t9"), kinds)
self.assertIn(("rm", "-f"), [(c[1], c[2]) for c in seen
if len(c) > 2])
def test_timeout_keeps_partial_stdout_and_container_logs(self):
# 旧超时路径只剩一句 'docker run timeout':TimeoutExpired
# 自带的 partial stdout 与 kill 后抓的 docker logs 尾都
# 被丢了。新路径:先 kill 不 rm,抓 logs,再 rm。
seen = []
def fake(cmd, timeout=None):
seen.append(list(cmd))
if len(cmd) > 1 and cmd[1] == "run":
exc = subprocess.TimeoutExpired(cmd, timeout)
exc.stdout = "partial-host-output"
exc.stderr = "partial-err"
raise exc
if len(cmd) > 1 and cmd[1] == "logs":
return SimpleNamespace(returncode=0,
stdout="container-tail-log",
stderr="")
return SimpleNamespace(returncode=0, stdout="", stderr="")
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py", runner=fake,
env={"XCM_API_KEY": "placeholder"},
timeout=5)
result = ex.dispatch_task(
{"id": "t10", "prompt": "slow", "worktree": "/tmp/wt"})
self.assertEqual(result["status"], "failed")
self.assertIn("partial-host-output", result["stdout"])
# partial 已有内容时 stdout 优先用 partial,logs 另存字段。
self.assertEqual(result["container_log_tail"],
"container-tail-log")
# 顺序:kill → logs → rm,logs 必须在 rm 之前抓到。
kinds = [c[1] for c in seen if len(c) > 1]
self.assertLess(kinds.index("logs"), kinds.index("rm"))
def test_kill_container_calls_kill_and_rm(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor(seen)
ok = ex.kill_container({"id": "t2"})
self.assertTrue(ok)
self.assertEqual([c[1] for c in seen], ["kill", "rm"])
self.assertEqual(seen[0][2], "codex-t2")
self.assertEqual(seen[1][2], "-f")
self.assertEqual(seen[1][3], "codex-t2")
class WorkerDockerTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
self.env = {"XCM_API_KEY": "placeholder-not-a-real-key"}
def tearDown(self):
self.tmp.cleanup()
def test_worker_docker_missing_fails_task_with_docker_error(self):
seen = []
def fake(cmd, timeout=None):
seen.append(list(cmd))
return SimpleNamespace(returncode=0, stdout="{}", stderr="")
_insert_task(self.db_path, "d1", prompt="do it")
with mock.patch("cloud.sandbox.shutil.which", return_value=None):
ex = DockerExecutor("/fake/host.py", runner=fake, env=self.env)
worker = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "placeholder-not-a-real-key"}, executor=ex)
result = worker.run_one()
self.assertTrue(result["dispatched"])
self.assertEqual(result["status"], "failed")
self.assertIn("docker", result["error"])
self.assertEqual(seen, [])
status, error_text = _task_status(self.db_path, "d1")
self.assertEqual(status, "failed")
self.assertIn("docker", error_text)
def test_worker_docker_success(self):
seen = []
_insert_task(self.db_path, "d2", prompt="do it")
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py",
runner=_ok_runner_factory(seen),
env=self.env)
worker = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "placeholder-not-a-real-key"}, executor=ex)
result = worker.run_one()
self.assertTrue(result["dispatched"])
self.assertEqual(result["status"], "completed")
self.assertEqual(result["executor"], "docker")
self.assertTrue(any(c[1] == "run" for c in seen))
def test_worker_cancel_kills_docker_container(self):
seen = []
_insert_task(self.db_path, "d3", prompt="do it")
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py",
runner=_ok_runner_factory(seen),
env=self.env)
worker = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "placeholder-not-a-real-key"}, executor=ex)
# 模拟 dispatch 阻塞期间:worker 已把容器名记下来。
worker._running_set("d3", container="codex-d3")
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET status = 'running' WHERE id = 'd3'")
conn.commit()
finally:
conn.close()
self.assertTrue(worker.cancel("d3"))
self.assertEqual([c[1] for c in seen], ["kill", "rm"])
status, _ = _task_status(self.db_path, "d3")
self.assertEqual(status, "interrupted")
if __name__ == "__main__":
unittest.main()
class TestDockerExtraMounts(DockerCommandTestCase):
def test_extra_mounts_appended_after_base_mounts(self):
with tempfile.TemporaryDirectory() as td:
work = os.path.join(td, "work")
os.makedirs(work)
state = os.path.join(td, "host-state.json")
Path(state).write_text("{}", encoding="utf-8")
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py", runner=_ok_runner_factory([]),
env={"XCM_API_KEY": "placeholder-not-a-real-key"},
extra_mounts=[f"{work}:/app/work", f"{state}:/app/host-state.json"])
cmd = ex.build_run_command({"id": "t1"}, ["python3", "/app/host.py", "status"])
# 基础挂载在前,额外挂载在后,且 /app/work 覆盖只读子路径
self.assertIn(f"{work}:/app/work:rw", cmd)
self.assertIn(f"{state}:/app/host-state.json:rw", cmd)
mounts = [cmd[i + 1] for i, a in enumerate(cmd) if a == "-v"]
self.assertEqual(mounts[1], "/fake:/app:ro")
self.assertIn(f"{work}:/app/work:rw", mounts)
self.assertLess(mounts.index("/fake:/app:ro"), mounts.index(f"{work}:/app/work:rw"))
def test_tmpfs_tmp_and_readonly_root_flag(self):
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = self._executor([])
cmd = ex.build_run_command(
{"id": "t1", "worktree": "/tmp/wt"}, ["echo", "hi"])
self.assertIn("--tmpfs", cmd)
self.assertIn("/tmp", cmd)
self.assertIn("/var/tmp", cmd)
self.assertNotIn("--read-only", cmd)
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py", runner=_ok_runner_factory([]),
env={"XCM_API_KEY": "placeholder-not-a-real-key"},
readonly_root=True)
cmd = ex.build_run_command(
{"id": "t1", "worktree": "/tmp/wt"}, ["echo", "hi"])
self.assertIn("--read-only", cmd)
with self.assertRaises(ValueError):
DockerExecutor("/fake/host.py",
extra_mounts=["/tmp/host-tmp:/tmp"])
def test_extra_mounts_reject_dangerous_paths(self):
with self.assertRaises(ValueError):
DockerExecutor("/fake/host.py", extra_mounts=["/: /x"])
with self.assertRaises(ValueError):
DockerExecutor("/fake/host.py", extra_mounts=["/etc/codex-app-server/env:/x"])
with self.assertRaises(ValueError):
DockerExecutor("/fake/host.py", extra_mounts=["/tmp:/app"])
with self.assertRaises(ValueError):
DockerExecutor("/fake/host.py", extra_mounts=["/nonexistent-xyz:/x"])
class TestDockerDefaultRunnerEnv(unittest.TestCase):
def test_env_merged_not_replaced(self):
from cloud.sandbox import _docker_default_runner
out = _docker_default_runner(
["python3", "-c",
"import os,sys;sys.stdout.write("
"os.environ.get('PROBE_X','')+':'+"
"('1' if os.environ.get('PATH') else '0'))"],
env={"PROBE_X": "42"})
self.assertEqual(out.returncode, 0)
self.assertEqual(out.stdout, "42:1")
def test_call_runner_passes_env_to_default_runner(self):
seen = []
def recording(cmd, timeout=None, env=None):
seen.append(env)
return SimpleNamespace(returncode=0, stdout="", stderr="")
ex = DockerExecutor("/fake/host.py", runner=recording,
env={"XCM_API_KEY": "placeholder-not-a-real-key"})
ex._call_runner(["docker", "version"], env={"SUB2API_API_KEY": "x"})
self.assertEqual(seen, [{"SUB2API_API_KEY": "x"}])