云端 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
@@ -18,7 +18,8 @@ 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
|
||||
from sandbox import (CloudExecutor, DockerExecutor, Executor,
|
||||
LocalExecutor, _parse_goal_output)
|
||||
|
||||
|
||||
def _task(task_id, depends_on=None, priority=0, status="queued"):
|
||||
@@ -219,5 +220,170 @@ class TestExecutors(unittest.TestCase):
|
||||
self.assertIn("cloud account", str(ctx.exception))
|
||||
|
||||
|
||||
class TestGoalExecutors(unittest.TestCase):
|
||||
def test_parse_goal_output(self):
|
||||
met, err = _parse_goal_output(
|
||||
"round 1: turn=completed verify_rc=0 verify_out=ok\n"
|
||||
"goal met after rounds\n")
|
||||
self.assertTrue(met)
|
||||
self.assertIsNone(err)
|
||||
met, err = _parse_goal_output(
|
||||
"round 1: turn=completed verify_rc=1 verify_out=file missing\n"
|
||||
"round 2: turn=completed verify_rc=1 verify_out=file missing\n"
|
||||
"goal unmet after rounds\n")
|
||||
self.assertFalse(met)
|
||||
self.assertIn("file missing", err)
|
||||
self.assertIn("2 rounds", err)
|
||||
met, err = _parse_goal_output("some other output\n")
|
||||
self.assertIsNone(met)
|
||||
met, err = _parse_goal_output("")
|
||||
self.assertIsNone(met)
|
||||
|
||||
def test_local_goal_task_met(self):
|
||||
seen = []
|
||||
|
||||
def fake(cmd):
|
||||
seen.append(cmd)
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="round 1: turn=completed verify_rc=0 verify_out=ok\n"
|
||||
"goal met after rounds\n",
|
||||
stderr="")
|
||||
|
||||
executor = LocalExecutor("/fake/host.py", runner=fake)
|
||||
result = executor.goal_task(
|
||||
{"id": "t1", "prompt": "do it", "verify_command": "test -f x",
|
||||
"goal_max_rounds": 2, "repo": "/r"})
|
||||
self.assertEqual(result["status"], "completed")
|
||||
self.assertTrue(result["goal_met"])
|
||||
self.assertIsNone(result["goal_error"])
|
||||
self.assertEqual(result["host_status"], "goal-met")
|
||||
self.assertIn("goal", seen[0])
|
||||
self.assertIn("--verify", seen[0])
|
||||
self.assertIn("test -f x", seen[0])
|
||||
self.assertIn("/r", seen[0])
|
||||
self.assertEqual(seen[0][seen[0].index("--max-rounds") + 1], "2")
|
||||
|
||||
def test_local_goal_task_unmet(self):
|
||||
def fake(cmd):
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="round 1: turn=completed verify_rc=1 "
|
||||
"verify_out=file missing\n"
|
||||
"goal unmet after rounds\n",
|
||||
stderr="")
|
||||
|
||||
result = LocalExecutor("/fake/host.py",
|
||||
runner=fake).goal_task(
|
||||
{"id": "t1", "prompt": "do it",
|
||||
"verify_command": "test -f x"})
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertFalse(result["goal_met"])
|
||||
self.assertIn("file missing", result["goal_error"])
|
||||
self.assertEqual(result["host_status"], "goal-unmet")
|
||||
|
||||
def test_local_goal_task_defaults_prompt_and_rounds(self):
|
||||
seen = []
|
||||
|
||||
def fake(cmd):
|
||||
seen.append(list(cmd))
|
||||
return SimpleNamespace(
|
||||
returncode=0, stdout="goal met after rounds\n", stderr="")
|
||||
|
||||
LocalExecutor("/fake/host.py", runner=fake).goal_task(
|
||||
{"id": "t1", "prompt": "do it", "verify_command": "true"})
|
||||
self.assertIn("do it", seen[0])
|
||||
self.assertEqual(seen[0][seen[0].index("--max-rounds") + 1], "3")
|
||||
|
||||
def test_local_goal_task_no_conclusion_is_failed_not_completed(self):
|
||||
def fake(cmd):
|
||||
return SimpleNamespace(returncode=0, stdout="nothing\n",
|
||||
stderr="")
|
||||
|
||||
result = LocalExecutor("/fake/host.py",
|
||||
runner=fake).goal_task(
|
||||
{"id": "t1", "prompt": "do it",
|
||||
"verify_command": "true"})
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIsNone(result["goal_met"])
|
||||
self.assertIn("无结论", result["goal_error"])
|
||||
|
||||
def test_local_goal_task_nonzero_is_failed(self):
|
||||
def fake(cmd):
|
||||
return SimpleNamespace(returncode=1, stdout="", stderr="boom")
|
||||
|
||||
result = LocalExecutor("/fake/host.py",
|
||||
runner=fake).goal_task(
|
||||
{"id": "t1", "prompt": "do it",
|
||||
"verify_command": "true"})
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIn("boom", result["goal_error"])
|
||||
|
||||
def test_local_goal_task_already_exists_names_retry(self):
|
||||
def fake(cmd):
|
||||
return SimpleNamespace(
|
||||
returncode=1, stdout="",
|
||||
stderr="Traceback\nRuntimeError: 任务已存在")
|
||||
|
||||
result = LocalExecutor("/fake/host.py",
|
||||
runner=fake).goal_task(
|
||||
{"id": "t1", "prompt": "do it",
|
||||
"verify_command": "true"})
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIn("请新建任务", result["goal_error"])
|
||||
|
||||
def test_local_goal_task_requires_verify(self):
|
||||
with self.assertRaises(ValueError):
|
||||
LocalExecutor("/fake/host.py",
|
||||
runner=lambda cmd: None).goal_task(
|
||||
{"id": "t1", "prompt": "do it"})
|
||||
|
||||
def test_base_goal_task_not_implemented(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
Executor().goal_task({"id": "t1"})
|
||||
|
||||
def test_popen_goal_command(self):
|
||||
seen = []
|
||||
|
||||
class _FakePopen:
|
||||
def __init__(self, cmd, **kwargs):
|
||||
seen.append(list(cmd))
|
||||
|
||||
with mock.patch("sandbox.subprocess.Popen", _FakePopen):
|
||||
LocalExecutor("/fake/host.py").popen_task(
|
||||
{"id": "t1", "prompt": "do it",
|
||||
"verify_command": "test -f x", "goal_max_rounds": 2},
|
||||
kind="goal")
|
||||
self.assertIn("goal", seen[0])
|
||||
self.assertIn("test -f x", seen[0])
|
||||
self.assertEqual(seen[0][seen[0].index("--max-rounds") + 1], "2")
|
||||
|
||||
def test_docker_goal_task_unmet(self):
|
||||
seen = []
|
||||
|
||||
def fake_runner(cmd, timeout=None):
|
||||
seen.append(cmd)
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="round 1: turn=completed verify_rc=1 "
|
||||
"verify_out=file missing\n"
|
||||
"goal unmet after rounds\n",
|
||||
stderr="")
|
||||
|
||||
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.goal_task(
|
||||
{"id": "t1", "prompt": "do it",
|
||||
"verify_command": "test -f x"})
|
||||
self.assertEqual(result["executor"], "docker")
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertFalse(result["goal_met"])
|
||||
self.assertIn("goal", seen[0])
|
||||
self.assertIn("test -f x", seen[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user