refactor: extract minimal helpers from _dispatch, _dispatch_project and _create_task

Move the /tasks/* sub-route out of _dispatch into _dispatch_task_route
(query passed explicitly; the files/tree/thread branches use it), move
the push-subscription DELETE branch out of _dispatch_project into
_project_push_subscription_delete, and turn the goal-validation block
of _create_task into the pure module function
_validate_task_goal_options (returns error string instead of sending
400, caller sends it unchanged). Statements are moved verbatim except
indentation and the goal send+return -> return-error transform; no
behaviour change. server.py module split intentionally not done: it
remains a larger, higher-risk refactor.

Add test_longfunc_split covering the pure goal validator's success
paths and all eight error strings.
This commit is contained in:
host committed 2026-11-03 02:48:55 +00:00
1 parent a9e2f70d9e
commit f96960c9c5
2 files changed
+331 -233

No files matched your search

+240 -233
View File
@@ -2001,6 +2001,51 @@ def _validate_hard_gate_body(body):
return None
def _validate_task_goal_options(body, prompt, prompt_goal_target, plan):
# 从 _create_task 平移的 goal 校验:纯函数,不碰 self/DB;
# 返回 (goal_mode, goal, verify_command, goal_max_rounds, error),
# error 非空时调用方按原样发 400。
# goal 模式分新旧两种语义(2026-10-27 起 console 只发新语义):
# 新语义 goal_mode=1:prompt 本身就是目标,不需要专用 goal
# 文本、不需要验收命令、不设轮次上限(goal_max_rounds 存 0,
# worker 走 host.py goal 循环的模型自决分支);传了
# verify_command 是语义冲突,400。旧语义(verify_command
# 非空)保留:goal 文本缺省用 prompt,轮次缺省 3,非法值
# 400 且不建任务;老客户端与已建的旧 goal 任务继续可用。
# /goal 命令优先于 body 里的 goal_mode=0(prompt 里的
# 命令比选项更具体),plan/验收命令冲突仍在下面 400。
goal_mode = 1 if body.get("goal_mode") in (True, 1) \
or prompt_goal_target else 0
if goal_mode:
if plan:
return (0, None, None, 0, "plan 模式与 goal 模式冲突(只读 vs 改文件)")
if body.get("verify_command"):
return (0, None, None, 0, "goal 模式不需要验收命令(达标由模型宣告)")
return (1, prompt, None, 0, None)
else:
goal = body.get("goal")
if goal is not None and not isinstance(goal, str):
return (0, None, None, 0, "goal must be a string")
verify_command = body.get("verify_command")
if verify_command is not None and not isinstance(
verify_command, str):
return (0, None, None, 0, "verify_command must be a string")
# 计划模式与 goal 模式也冲突:goal 循环就是要改文件
# 直到验测通过,只读沙箱下它永远做不成。
if plan and verify_command:
return (0, None, None, 0, "plan 模式与 goal 模式冲突(只读 vs 改文件)")
try:
goal_max_rounds = int(body.get("goal_max_rounds", 3))
except (TypeError, ValueError):
return (0, None, None, 0, "goal_max_rounds must be an integer")
if goal_max_rounds < 1:
return (0, None, None, 0, "goal_max_rounds must be >= 1")
# P1:旧验收形态轮次设上限 100(与 worker/sandbox 两道门
# 一致;新 goal_mode=1 不走本分支,仍由配额兜底)。
if goal_max_rounds > 100:
return (0, None, None, 0, "goal_max_rounds 超过上限 100 轮")
return (0, goal, verify_command, goal_max_rounds, None)
class CloudControlPlaneHandler(BaseHTTPRequestHandler):
"""控制面请求处理:除 /health 与 /webhooks/* 外一律要求 X-API-Key。"""
@@ -2112,6 +2157,160 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
except Exception:
pass
def _dispatch_task_route(self, user_id, method, path, query):
# 从 _dispatch 平移的 /tasks/* 子路由:语句原样平移,
# 仅缩进减一格;未命中任一子路由时仍返回 404。
rest = path[len("/tasks/"):]
if "/" not in rest and method == "GET":
self._get_task(user_id, rest)
elif rest.endswith("/cost") and method == "GET":
self._task_cost(user_id, rest[: -len("/cost")])
elif rest.endswith("/trace/stream") and method == "GET":
self._task_trace_stream(
user_id, rest[: -len("/trace/stream")])
elif rest.endswith("/trace") and method == "GET":
self._task_trace(user_id, rest[: -len("/trace")])
elif rest.endswith("/result-full") and method == "GET":
self._task_result_full(user_id,
rest[: -len("/result-full")])
elif rest.endswith("/agents") and method == "GET":
self._task_agents(user_id, rest[: -len("/agents")])
elif rest.endswith("/cancel") and method == "POST":
self._cancel_task(user_id, rest[: -len("/cancel")])
elif rest.endswith("/pause") and method == "POST":
self._pause_task(user_id, rest[: -len("/pause")])
elif rest.endswith("/merge") and method == "POST":
self._merge_task(user_id, rest[: -len("/merge")])
elif rest.endswith("/pr") and method == "POST":
self._task_pr(user_id, rest[: -len("/pr")])
elif rest.endswith("/retry") and method == "POST":
self._retry_task(user_id, rest[: -len("/retry")])
elif rest.endswith("/goal-resume") and method == "POST":
self._goal_resume_task(user_id, rest[: -len("/goal-resume")])
elif rest.endswith("/auto-memory-flags") and method == "POST":
self._task_auto_memory_flags(
user_id, rest[: -len("/auto-memory-flags")])
elif rest.endswith("/heartbeat") and method == "POST":
self._heartbeat_task(user_id, rest[: -len("/heartbeat")])
elif rest.endswith("/followups") and method == "POST":
self._create_followup(user_id, rest[: -len("/followups")])
elif rest.endswith("/followups") and method == "GET":
self._list_followups(user_id, rest[: -len("/followups")])
elif rest.endswith("/timeline") and method == "GET":
self._task_timeline(user_id, rest[: -len("/timeline")])
elif rest.endswith("/files") and method == "GET":
self._task_files(user_id, rest[: -len("/files")], query)
elif rest.endswith("/tree") and method == "GET":
self._task_tree(user_id, rest[: -len("/tree")], query)
elif rest.endswith("/browser/state") and method == "GET":
self._browser_state(user_id,
rest[: -len("/browser/state")])
elif rest.endswith("/browser/input") and method == "POST":
self._browser_input(user_id,
rest[: -len("/browser/input")],
self._read_body())
elif rest.endswith("/browser/control") and method == "POST":
self._browser_control(user_id,
rest[: -len("/browser/control")],
self._read_body())
elif rest.endswith("/browser/stream") and method == "GET":
self._browser_stream(user_id,
rest[: -len("/browser/stream")])
elif rest.endswith("/chat-history") and method == "GET":
self._task_chat_history(
user_id, rest[: -len("/chat-history")])
elif rest.endswith("/messages") and method == "POST":
self._create_peer_message(user_id, rest[: -len("/messages")])
elif rest.endswith("/messages") and method == "GET":
self._list_peer_messages(user_id, rest[: -len("/messages")])
elif rest.endswith("/reviews") and method == "POST":
self._create_review(user_id, rest[: -len("/reviews")])
elif rest.endswith("/reviews") and method == "GET":
self._list_reviews(user_id, rest[: -len("/reviews")])
elif "/reviews/" in rest and rest.endswith("/findings") \
and method == "POST":
_task_part, _rev_part = rest.split("/reviews/", 1)
self._update_review_findings(
user_id, _task_part, _rev_part.split("/")[0])
elif rest.endswith("/threads") and method == "GET":
self._task_threads(user_id, rest[: -len("/threads")])
elif rest.endswith("/thread") and method == "GET":
self._task_thread(user_id, rest[: -len("/thread")], query)
elif rest.endswith("/thread/rename") and method == "POST":
self._task_thread_rename(
user_id, rest[: -len("/thread/rename")])
elif rest.endswith("/thread/archive") and method == "POST":
self._task_thread_archive(
user_id, rest[: -len("/thread/archive")])
elif rest.endswith("/thread/unarchive") and method == "POST":
self._task_thread_unarchive(
user_id, rest[: -len("/thread/unarchive")])
elif rest.endswith("/thread/compact") and method == "POST":
self._task_thread_compact(
user_id, rest[: -len("/thread/compact")])
elif rest.endswith("/thread/read") and method == "GET":
self._task_thread_read(user_id,
rest[: -len("/thread/read")])
elif rest.endswith("/thread/message") and method == "POST":
self._task_thread_message(
user_id, rest[: -len("/thread/message")])
elif rest.endswith("/handoff") and method == "GET":
self._task_handoff_get(user_id,
rest[: -len("/handoff")])
elif rest.endswith("/handoff") and method == "POST":
self._task_handoff_create(user_id,
rest[: -len("/handoff")])
elif "/handoff/" in rest and rest.endswith("/report") \
and method == "POST":
_task_part, _hid_part = rest.split("/handoff/", 1)
self._task_handoff_report(
user_id, _task_part, _hid_part[: -len("/report")])
elif rest.endswith("/worktree") and method == "GET":
self._task_worktree_status(user_id,
rest[: -len("/worktree")])
elif rest.endswith("/bindings") and method == "GET":
self._task_bindings(user_id,
rest[: -len("/bindings")])
elif rest.endswith("/refs") and method == "GET":
self._task_refs_list(user_id, rest[: -len("/refs")])
elif rest.endswith("/refs") and method == "POST":
self._task_refs_create(user_id, rest[: -len("/refs")])
elif "/refs/" in rest and method == "DELETE":
_task_part, _ref_part = rest.split("/refs/", 1)
self._task_refs_delete(user_id, _task_part, _ref_part)
elif rest.endswith("/worktree/archive") and method == "POST":
self._task_worktree_archive(
user_id, rest[: -len("/worktree/archive")])
elif rest.endswith("/worktree/restore") and method == "POST":
self._task_worktree_restore(
user_id, rest[: -len("/worktree/restore")])
elif rest.endswith("/worktree/snapshots") and method == "GET":
self._task_worktree_snapshots(
user_id, rest[: -len("/worktree/snapshots")])
elif rest.endswith("/pr/checks") and method == "GET":
self._task_pr_checks(user_id,
rest[: -len("/pr/checks")])
elif rest.endswith("/sleep") and method == "POST":
self._task_sleep(user_id, rest[: -len("/sleep")])
elif rest.endswith("/wait") and method == "POST":
self._task_wait(user_id, rest[: -len("/wait")])
elif rest.endswith("/fork") and method == "POST":
self._fork_task(user_id, rest[: -len("/fork")])
elif rest.endswith("/approvals") and method == "GET":
self._list_approvals(user_id, rest[: -len("/approvals")])
elif rest.endswith("/approvals") and method == "POST":
self._create_approval(user_id, rest[: -len("/approvals")])
elif rest.endswith("/approve") and method == "POST":
self._decide_approval(
user_id, rest[: -len("/approve")], "approved")
elif rest.endswith("/deny") and method == "POST":
self._decide_approval(
user_id, rest[: -len("/deny")], "denied")
elif rest.endswith("/continue") and method == "POST":
self._continue_task(user_id, rest[: -len("/continue")])
else:
self._send_json(404, {"error": "not found"})
def _dispatch(self, method):
parsed = urlparse(self.path)
path = parsed.path
@@ -2355,156 +2554,7 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
elif path == "/admin/push/dispatch" and method == "POST":
self._push_dispatch_endpoint(user_id)
elif path.startswith("/tasks/"):
rest = path[len("/tasks/"):]
if "/" not in rest and method == "GET":
self._get_task(user_id, rest)
elif rest.endswith("/cost") and method == "GET":
self._task_cost(user_id, rest[: -len("/cost")])
elif rest.endswith("/trace/stream") and method == "GET":
self._task_trace_stream(
user_id, rest[: -len("/trace/stream")])
elif rest.endswith("/trace") and method == "GET":
self._task_trace(user_id, rest[: -len("/trace")])
elif rest.endswith("/result-full") and method == "GET":
self._task_result_full(user_id,
rest[: -len("/result-full")])
elif rest.endswith("/agents") and method == "GET":
self._task_agents(user_id, rest[: -len("/agents")])
elif rest.endswith("/cancel") and method == "POST":
self._cancel_task(user_id, rest[: -len("/cancel")])
elif rest.endswith("/pause") and method == "POST":
self._pause_task(user_id, rest[: -len("/pause")])
elif rest.endswith("/merge") and method == "POST":
self._merge_task(user_id, rest[: -len("/merge")])
elif rest.endswith("/pr") and method == "POST":
self._task_pr(user_id, rest[: -len("/pr")])
elif rest.endswith("/retry") and method == "POST":
self._retry_task(user_id, rest[: -len("/retry")])
elif rest.endswith("/goal-resume") and method == "POST":
self._goal_resume_task(user_id, rest[: -len("/goal-resume")])
elif rest.endswith("/auto-memory-flags") and method == "POST":
self._task_auto_memory_flags(
user_id, rest[: -len("/auto-memory-flags")])
elif rest.endswith("/heartbeat") and method == "POST":
self._heartbeat_task(user_id, rest[: -len("/heartbeat")])
elif rest.endswith("/followups") and method == "POST":
self._create_followup(user_id, rest[: -len("/followups")])
elif rest.endswith("/followups") and method == "GET":
self._list_followups(user_id, rest[: -len("/followups")])
elif rest.endswith("/timeline") and method == "GET":
self._task_timeline(user_id, rest[: -len("/timeline")])
elif rest.endswith("/files") and method == "GET":
self._task_files(user_id, rest[: -len("/files")], query)
elif rest.endswith("/tree") and method == "GET":
self._task_tree(user_id, rest[: -len("/tree")], query)
elif rest.endswith("/browser/state") and method == "GET":
self._browser_state(user_id,
rest[: -len("/browser/state")])
elif rest.endswith("/browser/input") and method == "POST":
self._browser_input(user_id,
rest[: -len("/browser/input")],
self._read_body())
elif rest.endswith("/browser/control") and method == "POST":
self._browser_control(user_id,
rest[: -len("/browser/control")],
self._read_body())
elif rest.endswith("/browser/stream") and method == "GET":
self._browser_stream(user_id,
rest[: -len("/browser/stream")])
elif rest.endswith("/chat-history") and method == "GET":
self._task_chat_history(
user_id, rest[: -len("/chat-history")])
elif rest.endswith("/messages") and method == "POST":
self._create_peer_message(user_id, rest[: -len("/messages")])
elif rest.endswith("/messages") and method == "GET":
self._list_peer_messages(user_id, rest[: -len("/messages")])
elif rest.endswith("/reviews") and method == "POST":
self._create_review(user_id, rest[: -len("/reviews")])
elif rest.endswith("/reviews") and method == "GET":
self._list_reviews(user_id, rest[: -len("/reviews")])
elif "/reviews/" in rest and rest.endswith("/findings") \
and method == "POST":
_task_part, _rev_part = rest.split("/reviews/", 1)
self._update_review_findings(
user_id, _task_part, _rev_part.split("/")[0])
elif rest.endswith("/threads") and method == "GET":
self._task_threads(user_id, rest[: -len("/threads")])
elif rest.endswith("/thread") and method == "GET":
self._task_thread(user_id, rest[: -len("/thread")], query)
elif rest.endswith("/thread/rename") and method == "POST":
self._task_thread_rename(
user_id, rest[: -len("/thread/rename")])
elif rest.endswith("/thread/archive") and method == "POST":
self._task_thread_archive(
user_id, rest[: -len("/thread/archive")])
elif rest.endswith("/thread/unarchive") and method == "POST":
self._task_thread_unarchive(
user_id, rest[: -len("/thread/unarchive")])
elif rest.endswith("/thread/compact") and method == "POST":
self._task_thread_compact(
user_id, rest[: -len("/thread/compact")])
elif rest.endswith("/thread/read") and method == "GET":
self._task_thread_read(user_id,
rest[: -len("/thread/read")])
elif rest.endswith("/thread/message") and method == "POST":
self._task_thread_message(
user_id, rest[: -len("/thread/message")])
elif rest.endswith("/handoff") and method == "GET":
self._task_handoff_get(user_id,
rest[: -len("/handoff")])
elif rest.endswith("/handoff") and method == "POST":
self._task_handoff_create(user_id,
rest[: -len("/handoff")])
elif "/handoff/" in rest and rest.endswith("/report") \
and method == "POST":
_task_part, _hid_part = rest.split("/handoff/", 1)
self._task_handoff_report(
user_id, _task_part, _hid_part[: -len("/report")])
elif rest.endswith("/worktree") and method == "GET":
self._task_worktree_status(user_id,
rest[: -len("/worktree")])
elif rest.endswith("/bindings") and method == "GET":
self._task_bindings(user_id,
rest[: -len("/bindings")])
elif rest.endswith("/refs") and method == "GET":
self._task_refs_list(user_id, rest[: -len("/refs")])
elif rest.endswith("/refs") and method == "POST":
self._task_refs_create(user_id, rest[: -len("/refs")])
elif "/refs/" in rest and method == "DELETE":
_task_part, _ref_part = rest.split("/refs/", 1)
self._task_refs_delete(user_id, _task_part, _ref_part)
elif rest.endswith("/worktree/archive") and method == "POST":
self._task_worktree_archive(
user_id, rest[: -len("/worktree/archive")])
elif rest.endswith("/worktree/restore") and method == "POST":
self._task_worktree_restore(
user_id, rest[: -len("/worktree/restore")])
elif rest.endswith("/worktree/snapshots") and method == "GET":
self._task_worktree_snapshots(
user_id, rest[: -len("/worktree/snapshots")])
elif rest.endswith("/pr/checks") and method == "GET":
self._task_pr_checks(user_id,
rest[: -len("/pr/checks")])
elif rest.endswith("/sleep") and method == "POST":
self._task_sleep(user_id, rest[: -len("/sleep")])
elif rest.endswith("/wait") and method == "POST":
self._task_wait(user_id, rest[: -len("/wait")])
elif rest.endswith("/fork") and method == "POST":
self._fork_task(user_id, rest[: -len("/fork")])
elif rest.endswith("/approvals") and method == "GET":
self._list_approvals(user_id, rest[: -len("/approvals")])
elif rest.endswith("/approvals") and method == "POST":
self._create_approval(user_id, rest[: -len("/approvals")])
elif rest.endswith("/approve") and method == "POST":
self._decide_approval(
user_id, rest[: -len("/approve")], "approved")
elif rest.endswith("/deny") and method == "POST":
self._decide_approval(
user_id, rest[: -len("/deny")], "denied")
elif rest.endswith("/continue") and method == "POST":
self._continue_task(user_id, rest[: -len("/continue")])
else:
self._send_json(404, {"error": "not found"})
self._dispatch_task_route(user_id, method, path, query)
else:
self._send_json(404, {"error": "not found"})
@@ -3150,6 +3200,38 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
return 403, "forbidden"
return None
def _project_push_subscription_delete(self, user_id, method, project_id, parts):
# 从 _dispatch_project 平移的 push 订阅删除分支:
# 语句原样平移,仅缩进减一格;每条路径都已发响应。
if method != "DELETE":
self._send_json(404, {"error": "not found"})
return
err = self._check_project(user_id, project_id, "create_task")
if err:
self._send_json(err[0], {"error": err[1]})
return
subs = _push_list(self.server.db_path, project_id)
mine = [s for s in subs if s["id"] == parts[3]]
if not mine:
self._send_json(404, {"error": "subscription not found"})
return
# 只能删自己的订阅,项目 owner 可删任意(owner 角色检查)。
with _auth_store(self.server.db_path) as auth:
is_owner = auth.check_permission(
user_id, project_id, "manage_project")
if mine[0]["user_id"] != user_id and not is_owner:
self._send_json(403, {"error": "forbidden"})
return
deleted = _push_delete(self.server.db_path,
subscription_id=parts[3])
if deleted:
with _auth_store(self.server.db_path) as store:
store.audit(
user_id, project_id, "push_subscription_deleted",
"subscription_id=%s" % parts[3])
self._send_json(200, {"deleted": bool(deleted)})
return
def _dispatch_project(self, user_id, method, path, query):
rest = path[len("/projects/"):]
parts = rest.split("/")
@@ -3302,33 +3384,8 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
return
# /projects/{id}/push/subscriptions/{sid} DELETE
if len(parts) == 4 and parts[1] == "push" and parts[2] == "subscriptions":
if method != "DELETE":
self._send_json(404, {"error": "not found"})
return
err = self._check_project(user_id, project_id, "create_task")
if err:
self._send_json(err[0], {"error": err[1]})
return
subs = _push_list(self.server.db_path, project_id)
mine = [s for s in subs if s["id"] == parts[3]]
if not mine:
self._send_json(404, {"error": "subscription not found"})
return
# 只能删自己的订阅,项目 owner 可删任意(owner 角色检查)。
with _auth_store(self.server.db_path) as auth:
is_owner = auth.check_permission(
user_id, project_id, "manage_project")
if mine[0]["user_id"] != user_id and not is_owner:
self._send_json(403, {"error": "forbidden"})
return
deleted = _push_delete(self.server.db_path,
subscription_id=parts[3])
if deleted:
with _auth_store(self.server.db_path) as store:
store.audit(
user_id, project_id, "push_subscription_deleted",
"subscription_id=%s" % parts[3])
self._send_json(200, {"deleted": bool(deleted)})
self._project_push_subscription_delete(
user_id, method, project_id, parts)
return
# /projects/{id}/automations
if len(parts) == 2 and parts[1] == "automations":
@@ -4411,62 +4468,12 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self._send_json(400, {
"error": "plan 模式与 approval=full 冲突(只读 vs 完全访问)"})
return
# goal 模式分新旧两种语义(2026-10-27 起 console 只发新语义):
# 新语义 goal_mode=1:prompt 本身就是目标,不需要专用 goal
# 文本、不需要验收命令、不设轮次上限(goal_max_rounds 存 0,
# worker 走 host.py goal 循环的模型自决分支);传了
# verify_command 是语义冲突,400。旧语义(verify_command
# 非空)保留:goal 文本缺省用 prompt,轮次缺省 3,非法值
# 400 且不建任务;老客户端与已建的旧 goal 任务继续可用。
# /goal 命令优先于 body 里的 goal_mode=0(prompt 里的
# 命令比选项更具体),plan/验收命令冲突仍在下面 400。
goal_mode = 1 if body.get("goal_mode") in (True, 1) \
or prompt_goal_target else 0
if goal_mode:
if plan:
self._send_json(400, {
"error": "plan 模式与 goal 模式冲突(只读 vs 改文件)"})
return
if body.get("verify_command"):
self._send_json(400, {
"error": "goal 模式不需要验收命令(达标由模型宣告)"})
return
goal = prompt
verify_command = None
goal_max_rounds = 0
else:
goal = body.get("goal")
if goal is not None and not isinstance(goal, str):
self._send_json(400, {"error": "goal must be a string"})
return
verify_command = body.get("verify_command")
if verify_command is not None and not isinstance(
verify_command, str):
self._send_json(400, {
"error": "verify_command must be a string"})
return
# 计划模式与 goal 模式也冲突:goal 循环就是要改文件
# 直到验测通过,只读沙箱下它永远做不成。
if plan and verify_command:
self._send_json(400, {
"error": "plan 模式与 goal 模式冲突(只读 vs 改文件)"})
return
try:
goal_max_rounds = int(body.get("goal_max_rounds", 3))
except (TypeError, ValueError):
self._send_json(400, {
"error": "goal_max_rounds must be an integer"})
return
if goal_max_rounds < 1:
self._send_json(400, {
"error": "goal_max_rounds must be >= 1"})
return
# P1:旧验收形态轮次设上限 100(与 worker/sandbox 两道门
# 一致;新 goal_mode=1 不走本分支,仍由配额兜底)。
if goal_max_rounds > 100:
self._send_json(400, {
"error": "goal_max_rounds 超过上限 100 轮"})
return
(goal_mode, goal, verify_command, goal_max_rounds,
_goal_err) = _validate_task_goal_options(
body, prompt, prompt_goal_target, plan)
if _goal_err:
self._send_json(400, {"error": _goal_err})
return
# 协调员:coordinator_parts 非空时任务本身不跑模型回合,
# worker 认领后按分工建并行子任务、等齐、合流;与 goal/
# plan/验收命令语义冲突(那些都是“本任务跑模型”的意思),
+91
View File
@@ -0,0 +1,91 @@
"""长函数小步拆分单测:只测从 _create_task 抽出的纯函数。
不跑真实模型回合、不联网、不碰 DB:_validate_task_goal_options
只读 body/prompt/plan,错误串须与抽取前 _create_task 内联分支
发出的 400 文案逐字一致(改动文案即本文件失败)。
本文件不含任何真实密钥、token、证书或私钥。
"""
import sys
import unittest
from pathlib import Path
try:
from cloud.server import (
CloudControlPlaneHandler,
_validate_task_goal_options,
)
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.server import (
CloudControlPlaneHandler,
_validate_task_goal_options,
)
class ValidateTaskGoalOptionsTestCase(unittest.TestCase):
def test_new_goal_mode(self):
self.assertEqual(
_validate_task_goal_options(
{"goal_mode": 1}, "做 X", None, 0),
(1, "做 X", None, 0, None))
def test_goal_command_target_is_new_goal_mode(self):
self.assertEqual(
_validate_task_goal_options(
{}, "做 X", "做 X", 0),
(1, "做 X", None, 0, None))
def test_new_goal_mode_conflicts(self):
self.assertEqual(
_validate_task_goal_options(
{"goal_mode": 1}, "做 X", None, 1)[4],
"plan 模式与 goal 模式冲突(只读 vs 改文件)")
self.assertEqual(
_validate_task_goal_options(
{"goal_mode": 1, "verify_command": "make test"},
"做 X", None, 0)[4],
"goal 模式不需要验收命令(达标由模型宣告)")
def test_old_goal_defaults(self):
self.assertEqual(
_validate_task_goal_options({}, "p", None, 0),
(0, None, None, 3, None))
def test_old_goal_explicit(self):
self.assertEqual(
_validate_task_goal_options(
{"goal": "G", "verify_command": "make test",
"goal_max_rounds": 5}, "p", None, 0),
(0, "G", "make test", 5, None))
def test_old_goal_errors(self):
# (body, plan, want_error):plan+verify 冲突用例传 plan=1,
# 其余 plan=0。
cases = [
({"goal": 123}, 0, "goal must be a string"),
({"verify_command": 123}, 0, "verify_command must be a string"),
({"verify_command": "make test"}, 1,
"plan 模式与 goal 模式冲突(只读 vs 改文件)"),
({"goal_max_rounds": "x"}, 0,
"goal_max_rounds must be an integer"),
({"goal_max_rounds": 0}, 0, "goal_max_rounds must be >= 1"),
({"goal_max_rounds": 101}, 0, "goal_max_rounds 超过上限 100 轮"),
]
for body, plan, want in cases:
self.assertEqual(
_validate_task_goal_options(body, "p", None, plan)[4],
want, body)
def test_split_helpers_exist(self):
# 结构锚点:两个路由助手须挂在 handler 上(行为由既有
# dispatch/任务/订阅测试覆盖,本文件不重复搭 HTTP 骨架)。
self.assertTrue(callable(
getattr(CloudControlPlaneHandler, "_dispatch_task_route")))
self.assertTrue(callable(
getattr(CloudControlPlaneHandler,
"_project_push_subscription_delete")))
if __name__ == "__main__":
unittest.main()