- DockerExecutor: extra mounts (host.py work/state, repo, codex-home
at same paths), worker --docker-extra-mounts, container user
configurable; Dockerfile copies codex-resources beside the codex
binary and puts bwrap on PATH (previous /usr/local/lib layout
made codex report bubblewrap unavailable inside the container).
- Real Docker task on external host: triple added in ~18s, merged
inside a container, main repo now add+mul2+triple, --rm cleanup.
Honest boundary: this host/kernel forbids unprivileged user
namespaces, so bwrap fails on host and in containers; production
mode here is Docker isolation + inner danger-full-access, not
double sandboxing.
- Cloud merge status: POST /tasks/{id}/merge syncs status only
(merge itself stays with host.py merge); completed -> merged,
maintainer+ (approve_merge), other states and repeat calls 409,
audit merge_task. Tests: cloud 223, host 27, all passing.
224 lines
9.0 KiB
Python
224 lines
9.0 KiB
Python
"""派发队列与沙箱执行器的单测。
|
||
|
||
运行(在 ~/workspace/codex-app-server 下):
|
||
python3 -m unittest discover -s cloud/tests -p "test_queue_sandbox.py" -v
|
||
|
||
说明:cloud/ 没有 __init__.py(协调员统一创建),因此这里把 cloud/
|
||
目录插到 sys.path 前面再导入 queue / sandbox,与 discover 的
|
||
top-level 目录无关。
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import unittest
|
||
from types import SimpleNamespace
|
||
from unittest import mock
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||
os.path.abspath(__file__))))
|
||
|
||
from queue import TaskQueue, budget_exceeded, order_tasks, ready_tasks
|
||
from sandbox import CloudExecutor, DockerExecutor, Executor, LocalExecutor
|
||
|
||
|
||
def _task(task_id, depends_on=None, priority=0, status="queued"):
|
||
task = {"id": task_id, "priority": priority, "status": status}
|
||
if depends_on is not None:
|
||
task["depends_on"] = depends_on
|
||
return task
|
||
|
||
|
||
class TestOrderTasks(unittest.TestCase):
|
||
def test_dependency_order(self):
|
||
# A -> B 时 B 在 A 后
|
||
ordered = order_tasks([_task("B", depends_on=["A"]),
|
||
_task("A")])
|
||
self.assertEqual([t["id"] for t in ordered], ["A", "B"])
|
||
|
||
def test_dependency_chain(self):
|
||
ordered = order_tasks([_task("C", depends_on=["B"]),
|
||
_task("B", depends_on=["A"]),
|
||
_task("A")])
|
||
self.assertEqual([t["id"] for t in ordered], ["A", "B", "C"])
|
||
|
||
def test_priority_within_wave(self):
|
||
# 同一波(均无依赖)按 priority 大的先
|
||
ordered = order_tasks([_task("low", priority=1),
|
||
_task("high", priority=10),
|
||
_task("mid", priority=5)])
|
||
self.assertEqual([t["id"] for t in ordered],
|
||
["high", "mid", "low"])
|
||
|
||
def test_priority_does_not_jump_dependency(self):
|
||
# 高优先级也不能越过依赖:B 依赖 A,即使 B 优先级更高也排 A 后
|
||
ordered = order_tasks([_task("B", depends_on=["A"], priority=99),
|
||
_task("A", priority=0)])
|
||
self.assertEqual([t["id"] for t in ordered], ["A", "B"])
|
||
|
||
def test_missing_dependency_raises(self):
|
||
with self.assertRaises(ValueError):
|
||
order_tasks([_task("B", depends_on=["NOPE"])])
|
||
|
||
def test_cycle_raises(self):
|
||
with self.assertRaises(ValueError):
|
||
order_tasks([_task("A", depends_on=["B"]),
|
||
_task("B", depends_on=["A"])])
|
||
|
||
def test_self_dependency_raises(self):
|
||
with self.assertRaises(ValueError):
|
||
order_tasks([_task("A", depends_on=["A"])])
|
||
|
||
def test_defaults(self):
|
||
# depends_on / priority 缺省时按 0 处理、不报错
|
||
ordered = order_tasks([{"id": "A"}, {"id": "B"}])
|
||
self.assertEqual([t["id"] for t in ordered], ["A", "B"])
|
||
|
||
|
||
class TestReadyTasks(unittest.TestCase):
|
||
def test_only_dependency_satisfied_queued(self):
|
||
tasks = [_task("A"),
|
||
_task("B", depends_on=["A"]),
|
||
_task("C", depends_on=["A"], status="running"),
|
||
_task("D", depends_on=["MISSING"])]
|
||
ready = ready_tasks(tasks, completed_ids=[])
|
||
self.assertEqual([t["id"] for t in ready], ["A"])
|
||
|
||
# A 已完成(状态随之改为 completed)后,B 依赖满足且 queued;
|
||
# C 是 running 不返回;D 依赖未满足不返回
|
||
done_tasks = [_task("A", status="completed"),
|
||
_task("B", depends_on=["A"]),
|
||
_task("C", depends_on=["A"], status="running"),
|
||
_task("D", depends_on=["MISSING"])]
|
||
ready = ready_tasks(done_tasks, completed_ids=["A"])
|
||
self.assertEqual([t["id"] for t in ready], ["B"])
|
||
|
||
def test_ready_ordered_by_priority(self):
|
||
tasks = [_task("low", priority=1), _task("high", priority=9)]
|
||
ready = ready_tasks(tasks, completed_ids=[])
|
||
self.assertEqual([t["id"] for t in ready], ["high", "low"])
|
||
|
||
|
||
class TestBudgetExceeded(unittest.TestCase):
|
||
def test_below_limits(self):
|
||
self.assertFalse(budget_exceeded(5, 10, 0.5, 1.0))
|
||
|
||
def test_at_limit_counts_as_exceeded(self):
|
||
self.assertTrue(budget_exceeded(10, 10, 0.5, 1.0))
|
||
self.assertTrue(budget_exceeded(5, 10, 1.0, 1.0))
|
||
|
||
def test_none_limit_means_unlimited(self):
|
||
self.assertFalse(budget_exceeded(10 ** 9, None, 10 ** 9, None))
|
||
self.assertTrue(budget_exceeded(10 ** 9, None, 10 ** 9, 1.0))
|
||
|
||
|
||
class TestTaskQueue(unittest.TestCase):
|
||
def test_next_ready_flow(self):
|
||
queue = TaskQueue()
|
||
queue.add(_task("A"))
|
||
queue.add(_task("B", depends_on=["A"]))
|
||
self.assertEqual(queue.next_ready([])["id"], "A")
|
||
queue.mark_running("A")
|
||
# A 在 running,不再是可派任务;B 依赖未完成,也不可派
|
||
self.assertIsNone(queue.next_ready([]))
|
||
queue.mark_done("A", "completed")
|
||
self.assertEqual(queue.next_ready(["A"])["id"], "B")
|
||
|
||
def test_mark_done_validates_status(self):
|
||
queue = TaskQueue([_task("A")])
|
||
with self.assertRaises(ValueError):
|
||
queue.mark_done("A", "bogus-status")
|
||
with self.assertRaises(ValueError):
|
||
queue.add(_task("A")) # 重复 id
|
||
|
||
def test_all_host_statuses_accepted(self):
|
||
queue = TaskQueue()
|
||
for status in ("running", "completed", "failed",
|
||
"interrupted", "merged", "merge-failed"):
|
||
queue.add(_task("t-" + status, status=status))
|
||
# 非 queued 任务一律不可派
|
||
self.assertIsNone(queue.next_ready([]))
|
||
|
||
|
||
class TestExecutors(unittest.TestCase):
|
||
def test_base_executor_must_implement(self):
|
||
with self.assertRaises(NotImplementedError):
|
||
Executor().execute({"id": "x"})
|
||
|
||
def test_local_executor_success(self):
|
||
seen = []
|
||
|
||
def fake_runner(cmd):
|
||
seen.append(cmd)
|
||
return SimpleNamespace(returncode=0, stdout="ok-status")
|
||
|
||
executor = LocalExecutor("/fake/host.py", config_path="/fake/cfg.json",
|
||
runner=fake_runner)
|
||
result = executor.execute({"id": "t1", "project_id": "p",
|
||
"repo": "/r", "prompt": "hi"})
|
||
self.assertEqual(result["executor"], "local")
|
||
self.assertEqual(result["task_id"], "t1")
|
||
self.assertEqual(result["status"], "completed")
|
||
self.assertEqual(result["stdout"], "ok-status")
|
||
# 只发只读 status 命令,不发 dispatch/run(不直接跑模型)
|
||
self.assertIn("status", seen[0])
|
||
self.assertNotIn("dispatch", seen[0])
|
||
self.assertNotIn("run", seen[0])
|
||
self.assertIn("--config", seen[0])
|
||
|
||
def test_local_executor_failure(self):
|
||
def fake_runner(cmd):
|
||
return SimpleNamespace(returncode=1, stdout="boom")
|
||
|
||
result = LocalExecutor("/fake/host.py",
|
||
runner=fake_runner).execute({"id": "t2"})
|
||
self.assertEqual(result["status"], "failed")
|
||
self.assertEqual(result["stdout"], "boom")
|
||
|
||
def test_local_executor_truncates_stdout(self):
|
||
def fake_runner(cmd):
|
||
return SimpleNamespace(returncode=0, stdout="x" * 5000)
|
||
|
||
result = LocalExecutor("/fake/host.py",
|
||
runner=fake_runner).execute({"id": "t3"})
|
||
self.assertEqual(len(result["stdout"]), 2000)
|
||
|
||
def test_docker_executor_no_docker_raises_not_local(self):
|
||
with mock.patch("sandbox.shutil.which", return_value=None):
|
||
with self.assertRaises(RuntimeError) as ctx:
|
||
DockerExecutor().execute({"id": "t1"})
|
||
self.assertIn("docker not available", str(ctx.exception))
|
||
# 报错而不是返回 local 结果:绝不静默降级为本地执行
|
||
|
||
def test_docker_executor_with_docker_runs_container(self):
|
||
seen = []
|
||
|
||
def fake_runner(cmd, timeout=None):
|
||
seen.append(cmd)
|
||
return SimpleNamespace(returncode=0, stdout="ok-status")
|
||
|
||
with mock.patch("sandbox.shutil.which",
|
||
return_value="/usr/bin/docker"):
|
||
executor = DockerExecutor(
|
||
"/fake/host.py", runner=fake_runner,
|
||
env={"XCM_API_KEY": "placeholder-not-a-real-key"})
|
||
result = executor.execute({"id": "t1", "worktree": "/tmp/wt1"})
|
||
self.assertEqual(result["executor"], "docker")
|
||
self.assertEqual(result["status"], "completed")
|
||
self.assertEqual(seen[0][0:2], ["docker", "run"])
|
||
self.assertIn("--rm", seen[0])
|
||
self.assertIn("/tmp/wt1:/work", seen[0])
|
||
self.assertIn("/fake:/app:ro", seen[0])
|
||
# -e 只传名,值绝不出现在命令列表里。
|
||
self.assertIn("XCM_API_KEY", seen[0])
|
||
self.assertNotIn("placeholder-not-a-real-key", seen[0])
|
||
self.assertNotIn("placeholder-not-a-real-key", " ".join(seen[0]))
|
||
|
||
def test_cloud_executor_raises_with_cloud_account(self):
|
||
with self.assertRaises(RuntimeError) as ctx:
|
||
CloudExecutor().execute({"id": "t1"})
|
||
self.assertIn("cloud account", str(ctx.exception))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|