云端 goal/merge/review 三事件:goal 循环 + PR 事件通知
此前 goal-unmet/merge-failed/review-requested 只有允许列表,
云端没有生产者(概念只在本地 host.py)。本轮给三者接真生产者:
- goal:POST /tasks 接受 goal/verify_command/goal_max_rounds
(迁移补列,SQLite 与 Postgres 两支);带 verify_command 的
任务走 host.py goal 循环(Local/Docker 执行器 goal_task,
turn→验测→喂回重试,缺省 3 轮);达标 completed,未达标
failed 且只发 goal-unmet、不重复发 failed;goal 模式下 worker
级重试/fallback 关闭,同任务 /retry 会撞 host 任务名已存在
(已在文档写明,须新建任务);CloudExecutor 无 goal 概念,
如实失败不降级
- review-requested:POST /tasks/{id}/pr 建 PR 成功时发
(payload 含 pr_url/pr_number)
- merge-failed:PR 落盘(push/建 PR)失败 502 时发(payload
含 step/error),任务保持 merged、可重调 /pr;跳过时不发
测试:host 29、cloud 469 全过。
This commit is contained in:
1 parent
26e73124fb
commit
5ee9d291dd
8 files changed
+965
-27
No files matched your search
@@ -223,6 +223,157 @@ class WorkerTestCase(unittest.TestCase):
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
# -- goal 模式 -------------------------------------------------------
|
||||
def _set_goal(self, task_id, verify="test -f x", goal=None,
|
||||
rounds=None):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE tasks SET verify_command = ?, goal = ?,"
|
||||
" goal_max_rounds = ? WHERE id = ?",
|
||||
(verify, goal, rounds, task_id))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_goal_met_notifies_completed(self):
|
||||
"""goal 达标:任务 completed,发 completed(不发 goal-unmet)。"""
|
||||
import http.server
|
||||
import threading
|
||||
|
||||
requests = []
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
body = self.rfile.read(length) if length else b""
|
||||
requests.append(json.loads(body.decode("utf-8")))
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", "2")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"{}")
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
server = http.server.ThreadingHTTPServer(
|
||||
("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(
|
||||
target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
from cloud.push import create_subscription
|
||||
url = "http://127.0.0.1:%d/hook" % server.server_address[1]
|
||||
create_subscription(
|
||||
self.db_path, "p1", "user-1", "webhook", url, "instant")
|
||||
_insert_task(self.db_path, "A")
|
||||
self._set_goal("A")
|
||||
calls = []
|
||||
|
||||
def goal_runner(cmd):
|
||||
calls.append(list(cmd))
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="round 1: turn=completed verify_rc=0 "
|
||||
"verify_out=ok\ngoal met after rounds\n",
|
||||
stderr="")
|
||||
|
||||
worker = self._worker(runner=goal_runner)
|
||||
results = worker.run_all()
|
||||
self.assertEqual(
|
||||
[r["status"] for r in results], ["completed"])
|
||||
self.assertIn("goal", calls[0])
|
||||
self.assertEqual(len(requests), 1)
|
||||
self.assertEqual(requests[0]["event_type"], "completed")
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
def test_goal_unmet_notifies_only_goal_unmet(self):
|
||||
"""goal 未达:任务 failed,只发 goal-unmet,不重复发 failed。"""
|
||||
import http.server
|
||||
import threading
|
||||
|
||||
requests = []
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
body = self.rfile.read(length) if length else b""
|
||||
requests.append(json.loads(body.decode("utf-8")))
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", "2")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"{}")
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
server = http.server.ThreadingHTTPServer(
|
||||
("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(
|
||||
target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
from cloud.push import create_subscription
|
||||
url = "http://127.0.0.1:%d/hook" % server.server_address[1]
|
||||
create_subscription(
|
||||
self.db_path, "p1", "user-1", "webhook", url, "instant")
|
||||
_insert_task(self.db_path, "A")
|
||||
self._set_goal("A")
|
||||
|
||||
def unmet_runner(cmd):
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="round 1: turn=completed verify_rc=1 "
|
||||
"verify_out=file missing\n"
|
||||
"goal unmet after rounds\n",
|
||||
stderr="")
|
||||
|
||||
worker = self._worker(runner=unmet_runner)
|
||||
results = worker.run_all()
|
||||
self.assertEqual([r["status"] for r in results], ["failed"])
|
||||
self.assertEqual(len(requests), 1)
|
||||
self.assertEqual(requests[0]["event_type"], "goal-unmet")
|
||||
self.assertIn("file missing",
|
||||
requests[0]["payload"]["error"])
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
def test_goal_mode_runs_once_despite_retries(self):
|
||||
"""goal 模式只跑一次:循环内部已多轮重试,worker 级重试
|
||||
不再叠加(同任务重跑会撞 host 任务名已存在)。"""
|
||||
_insert_task(self.db_path, "A")
|
||||
self._set_goal("A")
|
||||
calls = []
|
||||
|
||||
def counting(cmd):
|
||||
calls.append(cmd)
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="round 1: turn=completed verify_rc=1 "
|
||||
"verify_out=nope\ngoal unmet after rounds\n",
|
||||
stderr="")
|
||||
|
||||
worker = self._worker(runner=counting, max_retries=2)
|
||||
results = worker.run_all()
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(results[0]["status"], "failed")
|
||||
|
||||
def test_cloud_executor_goal_unsupported(self):
|
||||
"""cloud 执行器没有 goal 概念:goal 任务如实失败,
|
||||
不静默降级成普通 dispatch。"""
|
||||
from cloud.sandbox import CloudExecutor
|
||||
_insert_task(self.db_path, "A")
|
||||
self._set_goal("A")
|
||||
worker = self._worker(executor=CloudExecutor())
|
||||
results = worker.run_all()
|
||||
self.assertEqual(results[0]["status"], "failed")
|
||||
self.assertIn("不支持 goal", results[0]["error"])
|
||||
|
||||
# -- 取消 ------------------------------------------------------------
|
||||
def test_cancel_queued_task_marks_interrupted(self):
|
||||
_insert_task(self.db_path, "A")
|
||||
|
||||
Reference in new issue
Block a user