"""派发队列与沙箱执行器的单测。 运行(在 ~/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()