旧单页 console.html 换成 Vite + React + TypeScript 单文件构建产物 (309525 字节,sha 90345ec8…,重建字节一致),源码在 cloud/console-react/,改页面只改 src 再 npm run build:console, 产物不手改。布局:左侧列表(模块导航 + 任务列表)、中间标准对话 (实时链路时间线 + 继续会话聊天框,共用 Chat 组件)、右侧详情 (元数据、操作、评审、审批、协调子任务、子代理、线程)。 API 与后端语义未改,key 只存浏览器 localStorage。 6 个 console 标记单测随实现改名更新(意图不变:壳确实调了 这些接口、turn 信息仍在详情里、旧 goal 输入仍不存在); P1 标记单测未动且通过。
202 lines
7.5 KiB
Python
202 lines
7.5 KiB
Python
"""新 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 直接回文本。
|
||
# React 版(2026-10-29 起):旧单页的 goalHint id 与
|
||
# body.goal_mode = 1 字面量已不存在,goal 提示改为
|
||
# DispatchSection 的条件渲染文案,提交时仍设 goal_mode=1;
|
||
# 断言意图不变,只换成构建产物里实际存在的标记。
|
||
_status, text = self._req("GET", "/console")
|
||
self.assertIn("goal 模式:输入框内容即目标", text)
|
||
self.assertIn("goal_mode", 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()
|