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:
1 parent
a9e2f70d9e
commit
f96960c9c5
2 files changed
+331
-233
No files matched your search
+240
-233
@@ -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/验收命令语义冲突(那些都是“本任务跑模型”的意思),
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user