coverage85-step2b: server/worker 剩余缺口真实行为测试(+24 项)

- server:_dispatch_project(项目 PATCH repo_url、成员邀请/改角
  色/删除与 404/403/409 族)、_create_task 校验 400 族与合法变体
  (goal/协调员/plan/环境)、trace/agents/trace-stream(终态 SSE
  snapshot+done)、GitHub webhook 去重与坏体。
- worker:_gateway_context(xcmapi/缺 origin/缺属主/假 sub2api
  key 与协调员父属主回查)、_ensure_gateway 缓存、_docker_task_
  context(代理/租户网络失败/搜索透传)、_deliver_marker_tools
 (守卫空跑、创建派生、goal 转换、轮询快照)、协调员孤儿扫尾、
  run_one/run_all/run_loop/main 空库路径。
生产代码零改动。
This commit is contained in:
host committed 2026-11-03 02:48:54 +00:00
1 parent 576fd50ee3
commit 3d2df7dfe6
2 files changed
+807

No files matched your search

+420
View File
@@ -0,0 +1,420 @@
"""覆盖率 78%->85% 第 2 步:cloud/server.py 剩余缺口的真实 HTTP 测试。
沿用 test_server.py 的 create_server + http.client 模式:每个测试
用独立临时 SQLite 与临时端口,不跑模型、不联网。重点:
_dispatch_project(项目 PATCH / 成员增删改查)、_create_task 的
校验分支与合法变体、任务 trace / trace-stream、GitHub webhook
去重与坏体分支。
"""
import hashlib
import hmac
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import unittest
from pathlib import Path
from unittest import mock
try:
from cloud.auth import AuthStore
from cloud.server import create_server
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.server import create_server
class Server2TestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self._old_admins = os.environ.get("CODEX_GLOBAL_ADMINS")
os.environ["CODEX_GLOBAL_ADMINS"] = self.owner_id
self.viewer_id, self.viewer_key = self.auth.create_user("viewer")
self.outsider_id, self.outsider_key = self.auth.create_user(
"outsider")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
self.auth.set_member(
self.project["id"], self.owner_id, self.viewer_id, "viewer")
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
if self._old_admins is None:
os.environ.pop("CODEX_GLOBAL_ADMINS", None)
else:
os.environ["CODEX_GLOBAL_ADMINS"] = self._old_admins
def _req(self, method, path, body=None, key=None, headers=None,
raw_body=None):
conn = http.client.HTTPConnection(
"127.0.0.1", self.port, timeout=10)
hdrs = dict(headers or {})
if raw_body is not None:
payload = raw_body
elif body is not None:
payload = json.dumps(body).encode("utf-8")
hdrs.setdefault("Content-Type", "application/json")
else:
payload = None
if key is not None:
hdrs["X-API-Key"] = key
try:
conn.request(method, path, body=payload, headers=hdrs)
resp = conn.getresponse()
data = resp.read()
finally:
conn.close()
try:
parsed = json.loads(data.decode("utf-8")) if data else None
except ValueError:
parsed = None
return resp.status, parsed
def _create_task(self, key, **extra):
body = {"project_id": self.project["id"], "prompt": "做一个加法函数"}
body.update(extra)
status, task = self._req("POST", "/tasks", body, key)
self.assertEqual(status, 201, task)
return task
def _task_count(self):
conn = sqlite3.connect(self.db_path)
try:
return conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0]
finally:
conn.close()
def _members_path(self):
return "/projects/%s/members" % self.project["id"]
# -- _dispatch_project:项目 PATCH --------------------------------
def test_project_patch_repo_url(self):
pid = self.project["id"]
status, body = self._req(
"PATCH", "/projects/" + pid,
{"repo_url": "https://github.com/org/repo.git"}, self.owner_key)
self.assertEqual(status, 200, body)
self.assertEqual(body["project"]["repo_url"],
"https://github.com/org/repo.git")
status, body = self._req(
"PATCH", "/projects/" + pid, {"repo_url": ""}, self.owner_key)
self.assertEqual(status, 200)
self.assertIsNone(body["project"]["repo_url"])
status, body = self._req(
"PATCH", "/projects/" + pid, {}, self.owner_key)
self.assertEqual(status, 400)
self.assertEqual(body["error"], "repo_url required")
status, body = self._req(
"PATCH", "/projects/" + pid, {"repo_url": 123}, self.owner_key)
self.assertEqual(status, 400)
status, body = self._req(
"PATCH", "/projects/" + pid,
{"repo_url": "https://user:pass@github.com/org/repo.git"},
self.owner_key)
self.assertEqual(status, 400)
status, body = self._req(
"PATCH", "/projects/" + pid,
{"repo_url": "https://github.com/org/repo.git"},
self.viewer_key)
self.assertEqual(status, 403)
status, body = self._req(
"PATCH", "/projects/nope",
{"repo_url": "https://github.com/org/repo.git"},
self.owner_key)
self.assertEqual(status, 404)
# -- _dispatch_project:成员 --------------------------------------
def test_project_members_flow(self):
path = self._members_path()
status, body = self._req("GET", path, key=self.owner_key)
self.assertEqual(status, 200)
roles = {m["username"]: m["role"] for m in body["members"]}
self.assertEqual(roles, {"owner": "owner", "viewer": "viewer"})
status, body = self._req("GET", path, key=self.outsider_key)
self.assertEqual(status, 403)
# 邀请:按 username、默认角色 contributor。
status, body = self._req(
"POST", path, {"username": "outsider"}, self.owner_key)
self.assertEqual(status, 201, body)
self.assertEqual(body["role"], "contributor")
self.assertEqual(body["user_id"], self.outsider_id)
# 外人已是成员:再邀 409;邀不存在的用户 404;坏角色 400;
# 无目标 400;viewer 邀人 403。
status, _ = self._req(
"POST", path, {"username": "outsider"}, self.owner_key)
self.assertEqual(status, 409)
status, body = self._req(
"POST", path, {"username": "ghost"}, self.owner_key)
self.assertEqual(status, 404)
status, body = self._req(
"POST", path, {"username": "viewer", "role": "super"},
self.owner_key)
self.assertEqual(status, 400)
status, body = self._req("POST", path, {}, self.owner_key)
self.assertEqual(status, 400)
status, _ = self._req(
"POST", path, {"username": "viewer"}, self.viewer_key)
self.assertEqual(status, 403)
# 新成员立刻能建任务(角色真实生效)。
self._create_task(self.outsider_key)
# 改角色:viewer -> contributor,之后 viewer 也能建任务。
status, body = self._req(
"PATCH", path + "/" + self.viewer_id,
{"role": "contributor"}, self.owner_key)
self.assertEqual(status, 200)
self.assertEqual(body["role"], "contributor")
self._create_task(self.viewer_key)
# 唯一 owner 降级 409;删非成员 404;viewer 改角色 403。
status, body = self._req(
"PATCH", path + "/" + self.owner_id, {"role": "viewer"},
self.owner_key)
self.assertEqual(status, 409)
status, _ = self._req(
"PATCH", path + "/nobody", {"role": "viewer"}, self.owner_key)
self.assertEqual(status, 404)
status, _ = self._req(
"PATCH", path + "/" + self.viewer_id, {"role": "viewer"},
self.viewer_key)
self.assertEqual(status, 403)
# 删成员:删后读该项目任务 403、建任务 403;删唯一 owner 409。
status, body = self._req(
"DELETE", path + "/" + self.outsider_id, key=self.owner_key)
self.assertEqual(status, 200)
task = self._create_task(self.owner_key)
status, _ = self._req(
"GET", "/tasks/" + task["id"], key=self.outsider_key)
self.assertEqual(status, 403)
status, _ = self._req(
"POST", "/tasks",
{"project_id": self.project["id"], "prompt": "x"},
self.outsider_key)
self.assertEqual(status, 403)
status, _ = self._req(
"DELETE", path + "/" + self.owner_id, key=self.owner_key)
self.assertEqual(status, 409)
status, _ = self._req(
"DELETE", path + "/nobody", key=self.owner_key)
self.assertEqual(status, 404)
status, _ = self._req("PUT", path, {}, self.owner_key)
self.assertEqual(status, 404)
status, _ = self._req(
"GET", path + "/" + self.viewer_id, key=self.owner_key)
self.assertEqual(status, 404)
status, _ = self._req("GET", "/projects/nope/members",
key=self.owner_key)
self.assertEqual(status, 404)
# -- _create_task:校验分支 ---------------------------------------
def test_create_task_validation_errors(self):
pid = self.project["id"]
cases = [
({"prompt": "x"}, "project_id required"),
({"project_id": pid}, "prompt required"),
({"project_id": pid, "prompt": "/goal"},
"goal 目标不能为空"),
({"project_id": pid, "prompt": "x", "depends_on": {"a": 1}},
"depends_on must be a list"),
({"project_id": pid, "prompt": "x", "priority": "abc"},
"priority must be an integer"),
({"project_id": pid, "prompt": "x", "executor": "bogus"},
"executor must be local, docker or cloud"),
({"project_id": pid, "prompt": "x", "model": " "},
"model must be a non-empty string"),
({"project_id": pid, "prompt": "x", "effort": "ultra"},
"effort must be low, medium, high, xhigh or max"),
({"project_id": pid, "prompt": "x", "approval": "sometimes"},
"approval must be ask, auto or full"),
({"project_id": pid, "prompt": "x", "child_model": " "},
"child_model must be a non-empty string"),
({"project_id": pid, "prompt": "x", "child_effort": "ultra"},
"child_effort must be low, medium, high, xhigh or max"),
({"project_id": pid, "prompt": "x", "plan": True,
"approval": "full"}, "plan 模式与 approval=full 冲突"),
({"project_id": pid, "prompt": "x", "goal_mode": True,
"verify_command": "make test"},
"goal 模式不需要验收命令"),
({"project_id": pid, "prompt": "x", "goal_mode": True,
"plan": True}, "plan 模式与 goal 模式冲突"),
({"project_id": pid, "prompt": "x", "goal_max_rounds": 0},
"goal_max_rounds must be >= 1"),
({"project_id": pid, "prompt": "x", "goal_max_rounds": 101},
"goal_max_rounds 超过上限 100 轮"),
({"project_id": pid, "prompt": "x", "goal_max_rounds": "abc"},
"goal_max_rounds must be an integer"),
({"project_id": pid, "prompt": "x", "goal": 123},
"goal must be a string"),
({"project_id": pid, "prompt": "x", "verify_command": 123},
"verify_command must be a string"),
({"project_id": pid, "prompt": "x", "plan": True,
"verify_command": "make test"},
"plan 模式与 goal 模式冲突"),
({"project_id": pid, "prompt": "x",
"coordinator_parts": "not-a-list"},
"coordinator_parts must be a list"),
({"project_id": pid, "prompt": "x",
"coordinator_parts": []},
"coordinator_parts must not be empty"),
({"project_id": pid, "prompt": "x",
"coordinator_parts": [{"name": "a", "prompt": "do a"}],
"goal_mode": True},
"协调员任务不跑模型回合"),
({"project_id": pid, "prompt": "x", "idempotency_key": 123},
"idempotency_key must be a string"),
({"project_id": pid, "prompt": "x", "environment_id": " "},
"environment_id must be a non-empty string"),
]
for body, want_err in cases:
with self.subTest(body=body):
status, resp = self._req("POST", "/tasks", body,
self.owner_key)
self.assertEqual(status, 400, resp)
self.assertIn(want_err, resp["error"])
# environment 不存在走 404(与上面 400 族不同)。
status, resp = self._req(
"POST", "/tasks",
{"project_id": pid, "prompt": "x", "environment_id": "nope"},
self.owner_key)
self.assertEqual(status, 404)
self.assertEqual(resp["error"], "environment not found")
self.assertEqual(self._task_count(), 0)
# 坏 JSON 体也 400。
status, resp = self._req("POST", "/tasks", key=self.owner_key,
raw_body=b"{not json")
self.assertEqual(status, 400)
def test_create_task_valid_variants(self):
pid = self.project["id"]
task = self._create_task(self.owner_key, depends_on="dep-1")
self.assertEqual(task["depends_on"], ["dep-1"])
task = self._create_task(self.owner_key, prompt="/goal 收邮件")
self.assertEqual(task["goal_mode"], 1)
self.assertEqual(task["goal"], "收邮件")
task = self._create_task(
self.owner_key,
coordinator_parts=[{"name": "a", "prompt": "do a"},
{"name": "b", "prompt": "do b"}])
self.assertTrue(task["coordinator_parts"])
task = self._create_task(self.owner_key, plan=True,
approval="ask")
self.assertEqual(task["plan"], 1)
task = self._create_task(self.owner_key, executor="local",
effort="high", model=" m1 ",
child_model="cm", child_effort="low")
self.assertEqual(task["model"], "m1")
self.assertEqual(task["executor"], "local")
# -- trace / trace-stream -----------------------------------------
def test_task_trace_and_stream(self):
task = self._create_task(self.owner_key)
tid = task["id"]
status, body = self._req("GET", "/tasks/%s/trace" % tid,
key=self.owner_key)
self.assertEqual(status, 200, body)
self.assertEqual(body["task_id"], tid)
self.assertIn("entries", body)
status, body = self._req("GET", "/tasks/%s/trace" % tid,
key=self.outsider_key)
self.assertEqual(status, 403)
status, body = self._req("GET", "/tasks/nope/trace",
key=self.owner_key)
self.assertEqual(status, 404)
status, body = self._req("GET", "/tasks/%s/agents" % tid,
key=self.owner_key)
self.assertEqual(status, 200)
self.assertIn("agents", body)
# 终态任务的 stream 立刻以 done 收尾(不进 3600 秒轮询)。
conn = sqlite3.connect(self.db_path)
try:
conn.execute("UPDATE tasks SET status = 'completed' "
"WHERE id = ?", (tid,))
conn.commit()
finally:
conn.close()
conn = http.client.HTTPConnection(
"127.0.0.1", self.port, timeout=10)
try:
conn.request("GET", "/tasks/%s/trace/stream" % tid,
headers={"X-API-Key": self.owner_key})
resp = conn.getresponse()
self.assertEqual(resp.status, 200)
self.assertIn("text/event-stream",
resp.getheader("Content-Type"))
raw = resp.read().decode("utf-8")
finally:
conn.close()
events = [json.loads(ln[6:]) for ln in raw.splitlines()
if ln.startswith("data: ")]
self.assertEqual(events[0]["type"], "snapshot")
self.assertEqual(events[-1]["type"], "done")
self.assertEqual(events[-1]["status"], "completed")
status, _ = self._req("GET", "/tasks/nope/trace/stream",
key=self.owner_key)
self.assertEqual(status, 404)
status, _ = self._req("GET", "/tasks/%s/trace/stream" % tid,
key=self.outsider_key)
self.assertEqual(status, 403)
# -- GitHub webhook:去重与坏体 ------------------------------------
def _github_post(self, payload, raw=None, delivery="d-1",
event="issue_comment"):
if raw is None:
raw = json.dumps(payload).encode("utf-8")
sig = "sha256=" + hmac.new(
b"test-secret", raw, hashlib.sha256).hexdigest()
return self._req(
"POST", "/webhooks/github", raw_body=raw,
headers={"Content-Type": "application/json",
"X-Hub-Signature-256": sig,
"X-GitHub-Event": event,
"X-GitHub-Delivery": delivery})
def test_github_webhook_duplicate_and_bad_bodies(self):
payload = {
"action": "created",
"comment": {"body": "请 /fix-review 处理",
"user": {"login": "alice"}},
"repository": {"full_name": "org/repo"},
"issue": {"number": 7},
}
with mock.patch.dict(
os.environ, {"GITHUB_WEBHOOK_SECRET": "test-secret"}):
status, body = self._github_post(payload)
self.assertEqual(status, 200)
self.assertEqual(body["type"], "fix_review")
# 无匹配项目:不建任务,task_id 为 None。
self.assertIsNone(body.get("task_id"))
status, body = self._github_post(payload)
self.assertEqual(status, 200)
self.assertTrue(body["duplicate"])
self.assertEqual(body["delivery_id"], "d-1")
status, body = self._github_post(
payload, raw=b"{broken", delivery="d-2")
self.assertEqual(status, 400)
status, body = self._github_post(
payload, raw=b"[1, 2]", delivery="d-3")
self.assertEqual(status, 400)
if __name__ == "__main__":
unittest.main()
+387
View File
@@ -0,0 +1,387 @@
"""覆盖率 78%->85% 第 2 步:cloud/worker.py 剩余缺口的真实行为测试。
只用临时 SQLite(server.init_db 建表)与假执行器/假网关:
_gateway_context 的 sub2api 分支用模块级 _ensure_sub2api_key 替身
(假网关,绝不碰真实 sub2api/生产网关);_docker_task_context
用假执行器;_deliver_marker_tools 用真实父子任务行;main/run_loop
用空库。绝不跑模型、不建生产任务。
"""
import io
import json
import os
import sqlite3
import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
try:
from cloud.server import init_db
from cloud.worker import Worker, main as worker_main
import cloud.worker as worker_mod
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.server import init_db
from cloud.worker import Worker, main as worker_main
import cloud.worker as worker_mod
class _Env:
def __init__(self, **kw):
self.kw = kw
self.old = {}
def __enter__(self):
for k, v in self.kw.items():
self.old[k] = os.environ.get(k)
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
return self
def __exit__(self, *exc):
for k, v in self.old.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
return False
def _insert_task(db_path, task_id, status="queued", **kw):
conn = sqlite3.connect(db_path)
try:
cols = {"id": task_id, "project_id": kw.pop("project_id", "proj"),
"prompt": kw.pop("prompt", "do it"), "status": status,
"created_by": kw.pop("created_by", "owner1"),
"created_at": kw.pop("created_at", 1000.0),
"updated_at": kw.pop("updated_at", 1000.0)}
cols.update(kw)
names = ", ".join(cols)
placeholders = ", ".join("?" * len(cols))
conn.execute("INSERT INTO tasks(%s) VALUES(%s)"
% (names, placeholders), tuple(cols.values()))
conn.commit()
finally:
conn.close()
def _rows(db_path, sql, params=()):
conn = sqlite3.connect(db_path)
try:
return conn.execute(sql, params).fetchall()
finally:
conn.close()
class Worker2Base(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
self.hb = os.path.join(self.tmp.name, "hb.json")
self.worker = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"},
heartbeat_file=self.hb,
worker_id="cov85-worker2")
class GatewayContextTest(Worker2Base):
def test_xcmapi_returns_none_none(self):
with _Env(CLOUD_GATEWAY="xcmapi", SUB2API_BASE_URL=None):
self.assertEqual(
self.worker._gateway_context({"created_by": "owner1"}),
(None, None))
with _Env(CLOUD_GATEWAY=None, SUB2API_BASE_URL=None):
self.assertEqual(
self.worker._gateway_context({"created_by": "owner1"}),
(None, None))
def test_sub2api_missing_origin_raises(self):
with _Env(CLOUD_GATEWAY="sub2api", SUB2API_BASE_URL=None):
with self.assertRaises(RuntimeError) as ctx:
self.worker._gateway_context({"created_by": "owner1"})
self.assertIn("SUB2API_BASE_URL", str(ctx.exception))
def test_sub2api_missing_owner_raises(self):
with _Env(CLOUD_GATEWAY="sub2api",
SUB2API_BASE_URL="https://gw.example.test"):
with self.assertRaises(RuntimeError) as ctx:
self.worker._gateway_context({})
self.assertIn("created_by", str(ctx.exception))
def test_sub2api_fake_gateway_and_parent_owner(self):
_insert_task(self.db_path, "parent1", status="completed",
created_by="owner9")
calls = []
with _Env(CLOUD_GATEWAY="sub2api",
SUB2API_BASE_URL="https://gw.example.test:8443"):
with mock.patch.object(
worker_mod, "_ensure_sub2api_key",
side_effect=lambda conn, owner: (
calls.append(owner), "fake-key-123")[1]):
provider, env = self.worker._gateway_context(
{"created_by": "owner1"})
self.assertEqual(provider, "sub2api")
self.assertEqual(env["SUB2API_API_KEY"], "fake-key-123")
self.assertEqual(env["SUB2API_BASE_URL"],
"https://gw.example.test:8443")
self.assertIn("gw.example.test", env["NO_PROXY"])
self.assertEqual(env["NO_PROXY"], env["no_proxy"])
self.assertEqual(calls, ["owner1"])
# 协调员子任务无 created_by 时跟父任务属主走。
provider, env = self.worker._gateway_context(
{"parent_task_id": "parent1"})
self.assertEqual(provider, "sub2api")
self.assertEqual(calls, ["owner1", "owner9"])
def test_sub2api_default_model_env_and_missing_key(self):
with _Env(CLOUD_SUB2API_DEFAULT_MODEL="model-x"):
self.assertEqual(
self.worker._sub2api_default_model({}), "model-x")
with _Env(CLOUD_SUB2API_DEFAULT_MODEL=None,
SUB2API_BASE_URL=None):
with self.assertRaises(RuntimeError):
self.worker._sub2api_default_model({})
def test_ensure_gateway_caches(self):
task = {"created_by": "owner1"}
with _Env(CLOUD_GATEWAY="xcmapi"):
first = self.worker._ensure_gateway(task)
self.assertEqual(first, (None, None))
self.assertIn("_gateway_provider", task)
second = self.worker._ensure_gateway(task)
self.assertEqual(second, first)
class FakeExecutor:
def __init__(self, gateway=None, network_ok=True):
self.gateway = gateway
self.network_ok = network_ok
self.networks = []
def ensure_network(self, name):
self.networks.append(name)
return self.network_ok
def network_gateway(self, network):
return self.gateway
class DockerTaskContextTest(Worker2Base):
def test_plain_no_tenant_no_proxy(self):
ok, mounts, network, proxy, err = self.worker._docker_task_context(
{"project_id": "proj"})
self.assertEqual((ok, mounts, network, proxy, err),
(True, None, None, None, None))
def test_egress_proxy_gateway(self):
w = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"},
heartbeat_file=self.hb, worker_id="cov85-w2",
egress_proxy_port=8888,
executor=FakeExecutor(gateway="10.0.0.1"))
ok, _, _, proxy, err = w._docker_task_context({"project_id": "p"})
self.assertTrue(ok)
self.assertIsNone(err)
self.assertEqual(proxy["HTTP_PROXY"], "http://10.0.0.1:8888")
self.assertEqual(proxy["NO_PROXY"], "localhost,127.0.0.1")
w2 = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"},
heartbeat_file=self.hb, worker_id="cov85-w2",
egress_proxy_port=8888,
executor=FakeExecutor(gateway=None))
ok, _, _, proxy, err = w2._docker_task_context({"project_id": "p"})
self.assertFalse(ok)
self.assertIsNone(proxy)
self.assertIn("egress 代理网关解析失败", err)
def test_tenant_network_failure_and_search_passthrough(self):
tenants = os.path.join(self.tmp.name, "tenants")
template_home = os.path.join(self.tmp.name, "template-home")
os.makedirs(template_home)
with open(os.path.join(template_home, "config.toml"), "w") as f:
f.write("[mcp_servers]\n")
template_repo = os.path.join(self.tmp.name, "template-repo")
os.makedirs(template_repo)
with open(os.path.join(template_repo, "sample.py"), "w") as f:
f.write("x = 1\n")
w = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"},
heartbeat_file=self.hb, worker_id="cov85-w2",
tenant_isolation=True, tenants_root=tenants,
tenant_template_home=template_home,
tenant_template_repo=template_repo,
executor=FakeExecutor(network_ok=False))
ok, _, _, _, err = w._docker_task_context({"project_id": "proj"})
self.assertFalse(ok)
self.assertIn("租户 Docker 网络创建失败", err)
w2 = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"},
heartbeat_file=self.hb, worker_id="cov85-w2",
executor=FakeExecutor())
with _Env(SEARXNG_URL="http://search.example.test:8080"):
ok, _, _, proxy, err = w2._docker_task_context(
{"project_id": "proj"})
self.assertTrue(ok)
self.assertEqual(proxy["SEARXNG_URL"],
"http://search.example.test:8080")
def test_tenant_safe_name_errors(self):
with self.assertRaises(ValueError):
self.worker._tenant_safe_name(None)
with self.assertRaises(ValueError):
self.worker._tenant_safe_name("...")
self.assertEqual(self.worker._tenant_network("proj"),
"codex-proj")
class MarkerToolsTest(Worker2Base):
def _followups(self, task_id):
return _rows(self.db_path,
"SELECT body FROM task_followups WHERE task_id = ?",
(task_id,))
def test_guards_are_noops(self):
_insert_task(self.db_path, "parent1", status="queued")
parent = self.worker.get_task("parent1")
for result, patch in [
(None, {}),
({"status": "failed"}, {}),
({"status": "completed", "task_creates": [
{"prompt": "x", "name": "c", "model": None,
"effort": None, "executor": None}]},
{"verify_command": "make test"}),
({"status": "completed", "task_creates": [
{"prompt": "x", "name": "c", "model": None,
"effort": None, "executor": None}]},
{"goal_mode": 1}),
({"status": "completed", "task_creates": [
{"prompt": "x", "name": "c", "model": None,
"effort": None, "executor": None}]},
{"coordinator_parts": [{"name": "a"}]}),
({"status": "completed"}, {}),
]:
with self.subTest(patch=patch):
task = dict(parent)
task.update(patch)
self.worker._deliver_marker_tools(task, result)
self.assertEqual(self._followups("parent1"), [])
self.assertEqual(_rows(self.db_path,
"SELECT COUNT(*) FROM tasks"), [(1,)])
def test_creates_child_and_report(self):
_insert_task(self.db_path, "parent1", status="completed")
parent = self.worker.get_task("parent1")
self.worker._deliver_marker_tools(parent, {
"status": "completed",
"task_creates": [{"prompt": "child do", "name": "c1",
"model": None, "effort": None,
"executor": None}]})
children = _rows(
self.db_path,
"SELECT prompt, status, parent_task_id FROM tasks "
"WHERE parent_task_id = ?", ("parent1",))
self.assertEqual(children, [("child do", "queued", "parent1")])
followups = self._followups("parent1")
self.assertEqual(len(followups), 1)
self.assertIn("任务工具报告", followups[0][0])
def test_goal_convert_queues_goal_followup(self):
_insert_task(self.db_path, "parent1", status="completed")
parent = self.worker.get_task("parent1")
self.worker._deliver_marker_tools(parent, {
"status": "completed",
"goal_converts": [{"goal": "收齐周报"}]})
followups = self._followups("parent1")
self.assertEqual(len(followups), 1)
self.assertIn("/goal", followups[0][0])
self.assertIn("收齐周报", followups[0][0])
def test_poll_snapshot_report(self):
_insert_task(self.db_path, "parent1", status="completed")
_insert_task(self.db_path, "child1", status="completed")
parent = self.worker.get_task("parent1")
self.worker._deliver_marker_tools(parent, {
"status": "completed",
"task_polls": [{"task_id": "child1", "interval_seconds": 0.05,
"timeout_seconds": 5, "chars": 200}]})
followups = self._followups("parent1")
self.assertEqual(len(followups), 1)
self.assertIn("child1", followups[0][0])
class OrphanSweepTest(Worker2Base):
def test_sweep_marks_orphans_only(self):
_insert_task(self.db_path, "p-done", status="completed")
_insert_task(self.db_path, "p-run", status="running")
_insert_task(self.db_path, "c1", status="queued",
parent_task_id="p-done")
_insert_task(self.db_path, "c2", status="queued",
parent_task_id="p-run")
_insert_task(self.db_path, "c3", status="queued",
parent_task_id="p-missing")
_insert_task(self.db_path, "c4", status="failed",
parent_task_id="p-done")
marked = self.worker.sweep_orphaned_coordinator_children()
self.assertEqual(sorted(marked),
[("p-done", "c1"), ("p-missing", "c3")])
statuses = dict(_rows(
self.db_path, "SELECT id, status FROM tasks"))
self.assertEqual(statuses["c1"], "interrupted")
self.assertEqual(statuses["c3"], "interrupted")
self.assertEqual(statuses["c2"], "queued")
self.assertEqual(statuses["c4"], "failed")
reasons = _rows(
self.db_path,
"SELECT interrupted_reason FROM tasks WHERE id = 'c1'")
self.assertEqual(reasons, [("coordinator_orphan",)])
class RunLoopMainTest(Worker2Base):
def test_run_one_no_ready_and_budget(self):
result = self.worker.run_one()
self.assertEqual(result, {"dispatched": False,
"reason": "no_ready_task"})
self.assertEqual(self.worker.run_all(), [])
tight = Worker(self.db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"},
heartbeat_file=self.hb, worker_id="cov85-w2",
max_cost_usd=0)
result = tight.run_one()
self.assertEqual(result["dispatched"], False)
self.assertEqual(result["reason"], "budget_exceeded")
def test_run_loop_max_seconds_zero(self):
results = self.worker.run_loop(max_seconds=0)
self.assertEqual(results, [])
self.assertTrue(os.path.exists(self.hb))
def test_main_single_and_loop_and_bad_executor(self):
out = io.StringIO()
with redirect_stdout(out):
worker_main(["--db", self.db_path,
"--host-py", "/fake/host.py",
"--heartbeat-file", self.hb,
"--worker-id", "cov85-main"])
self.assertEqual(json.loads(out.getvalue()), [])
out = io.StringIO()
with redirect_stdout(out):
worker_main(["--db", self.db_path,
"--host-py", "/fake/host.py",
"--heartbeat-file", self.hb,
"--worker-id", "cov85-main",
"--loop", "--max-seconds", "0"])
self.assertEqual(json.loads(out.getvalue()), [])
with self.assertRaises(SystemExit) as ctx:
worker_main(["--db", self.db_path, "--executor", "bogus"])
self.assertEqual(ctx.exception.code, 2)
if __name__ == "__main__":
unittest.main()