Files
codex-app-server/cloud/tests/test_goal_mode.py
T
openqjandhost c86b450851 feat: goal 模式新语义——prompt 即目标,无验收命令,不限轮次,模型宣告完成 (#67)
console 打勾 goal 模式时不再有专用 goal 文本、验收命令、轮次
三个输入(已移除,换成说明行),只发 goal_mode=1;server 侧
goal_mode=1 时 goal 存 prompt、验收命令空、轮次存 0,带验收
命令或与计划模式同用时 400,旧的带验收命令任务照原样可用。

host.py goal 循环:verify 为空时不跑验测,轮次提示里写明
标记约定;模型在完整跑完的回合末尾单独一行输出“目标已
达成”即达标(超时回合的部分文本含标记也不算);--max-rounds
0 为不限轮次,goal-resume 续跑按 0 处理(不用 or 兜底成 3)。
刹车只有取消与预算耗尽(后者记 interrupted、可续跑)。

迁移与 worker:tasks 加 goal_mode 列(server 迁移、worker 列
补齐、PG schema 三处同步);worker 按 goal_mode 或
verify_command 判定 goal 路径,重试与后续消息排空对新语义
同样只跑一次/不排空;Local/Docker goal_task 的 --verify 可选、
--max-rounds 0 原样传。

Co-authored-by: host <host@test.local>
2026-10-27 03:54:48 +08:00

198 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""新 goal 语义单测:prompt 即目标,无验收命令,不限轮次,
模型在回合末尾单独一行输出“目标已达成”即达标。
不跑真实模型回合、不联网:server 侧走本地临时端口 + 临时
SQLite;worker 侧注入假 runner;旧语义(verify_command)任务
仍按原样建任务、走 goal 循环。
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
try:
from cloud.auth import AuthStore
from cloud.sandbox import LocalExecutor
from cloud.server import create_server, init_db
from cloud.worker import Worker
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.sandbox import LocalExecutor
from cloud.server import create_server, init_db
from cloud.worker import Worker
def _goal_met_runner(cmd):
return SimpleNamespace(
returncode=0,
stdout="round 1: turn=completed verify_rc=- verify_out=\n"
"goal met after rounds\n",
stderr="")
class ServerGoalModeTestCase(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.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)
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()
def _req(self, method, path, body=None, key=None):
conn = http.client.HTTPConnection(
"127.0.0.1", self.port, timeout=10)
hdrs = {}
payload = None
if key:
hdrs["X-API-Key"] = key
if body is not None:
payload = json.dumps(body)
hdrs["Content-Type"] = "application/json"
conn.request(method, path, body=payload, headers=hdrs)
resp = conn.getresponse()
raw = resp.read()
conn.close()
try:
return resp.status, json.loads(raw.decode("utf-8"))
except ValueError:
return resp.status, raw.decode("utf-8", errors="replace")
def _create(self, **extra):
body = {"project_id": self.project["id"], "prompt": "build it"}
body.update(extra)
return self._req("POST", "/tasks", body, self.owner_key)
def test_create_goal_mode_task(self):
status, task = self._create(goal_mode=1)
self.assertEqual(status, 201)
self.assertEqual(task["goal_mode"], 1)
# 新语义:prompt 本身就是目标,不存验收命令,轮次存 0。
self.assertEqual(task["goal"], "build it")
self.assertIsNone(task["verify_command"])
self.assertEqual(task["goal_max_rounds"], 0)
def test_goal_mode_with_verify_command_400(self):
status, body = self._create(goal_mode=1, verify_command="true")
self.assertEqual(status, 400)
def test_goal_mode_with_plan_400(self):
status, body = self._create(goal_mode=1, plan=1)
self.assertEqual(status, 400)
def test_old_style_verify_task_still_works(self):
status, task = self._create(verify_command="true",
goal_max_rounds=2)
self.assertEqual(status, 201)
self.assertEqual(task["goal_mode"], 0)
self.assertEqual(task["verify_command"], "true")
self.assertEqual(task["goal_max_rounds"], 2)
def test_console_goal_hint_and_no_old_inputs(self):
# console 壳是静态页:无 key 也能取,_req 对非 JSON 直接回文本。
_status, text = self._req("GET", "/console")
self.assertIn('id="goalHint"', text)
self.assertIn("body.goal_mode = 1", text)
# 旧的专用 prompt / 验收命令 / 轮次输入已移除。
self.assertNotIn('id="verifyCommand"', text)
self.assertNotIn('id="goalRounds"', text)
self.assertNotIn('id="goalFields"', text)
class ExecutorGoalModeTestCase(unittest.TestCase):
def test_local_goal_task_no_verify_zero_rounds(self):
seen = []
def fake(cmd):
seen.append(list(cmd))
return _goal_met_runner(cmd)
result = LocalExecutor("/fake/host.py", runner=fake).goal_task(
{"id": "t1", "prompt": "build it", "goal": "build it",
"goal_mode": 1, "goal_max_rounds": 0})
self.assertEqual(result["status"], "completed")
self.assertTrue(result["goal_met"])
# 新形态:不传 --verify,--max-rounds 原样传 0(不能被
# or 兜底成 3,否则不限轮次悄悄变回 3 轮上限)。
self.assertNotIn("--verify", seen[0])
self.assertEqual(seen[0][seen[0].index("--max-rounds") + 1], "0")
class WorkerGoalModeTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
self.env = {"XCM_API_KEY": "test-placeholder"}
def tearDown(self):
self.tmp.cleanup()
def _insert(self, task_id, **extra):
now = time.time()
cols = ["id", "project_id", "prompt", "status",
"idempotency_key", "depends_on", "priority",
"created_by", "created_at", "updated_at",
"last_heartbeat"]
vals = [task_id, "p1", "do work", "queued", None, "[]", 0,
"tester", now, now, None]
for k, v in extra.items():
cols.append(k)
vals.append(v)
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"INSERT INTO tasks(%s) VALUES(%s)"
% (", ".join(cols), ", ".join("?" * len(cols))), vals)
conn.commit()
finally:
conn.close()
def test_goal_mode_task_routes_to_goal_not_dispatch(self):
self._insert("T", goal_mode=1, goal="build it",
goal_max_rounds=0)
seen = []
def fake(cmd):
seen.append(list(cmd))
if "goal" in cmd:
return _goal_met_runner(cmd)
raise AssertionError("goal 任务不应走 dispatch: %s" % cmd)
worker = Worker(self.db_path, "/fake/host.py", runner=fake,
env=self.env)
result = worker.run_one()
self.assertEqual(result["status"], "completed")
self.assertTrue(any("goal" in cmd for cmd in seen))
self.assertNotIn("--verify", seen[0])
self.assertEqual(seen[0][seen[0].index("--max-rounds") + 1], "0")
if __name__ == "__main__":
unittest.main()