主任务可显式声明派生默认,或在 prompt 里写“子任务用 gpt-6-luna max” 由服务端解析填入;marker task-create 与协调员分工未显式写模型/强度时 按 块/分工 > 派生默认 > 父任务模型/强度 继承,fork 复制派生默认, continue 可改派生默认;console 新建/继续表单与任务详情同步展示。
293 lines
11 KiB
Python
293 lines
11 KiB
Python
"""子任务默认模型/强度(child_model / child_effort)单测。
|
||
|
||
不跑真实模型、不联网:解析是纯函数;服务端用本地临时端口
|
||
+ 临时 SQLite(与 test_server_endpoints 同款骨架);worker
|
||
派生用临时 SQLite + _deliver_marker_tools(与
|
||
test_marker_tools 同款骨架,不走 run_one 全量流程)。
|
||
"""
|
||
|
||
import http.client
|
||
import json
|
||
import os
|
||
import sqlite3
|
||
import sys
|
||
import tempfile
|
||
import threading
|
||
import time
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
try:
|
||
from cloud.auth import AuthStore
|
||
from cloud.marker_tools import parse_spawn_defaults
|
||
from cloud.server import create_server, init_db
|
||
from cloud.worker import Worker
|
||
except ImportError:
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||
from cloud.auth import AuthStore
|
||
from cloud.marker_tools import parse_spawn_defaults
|
||
from cloud.server import create_server, init_db
|
||
from cloud.worker import Worker
|
||
|
||
FAKE_ENV = {"XCM_API_KEY": "test-placeholder"}
|
||
|
||
|
||
class ParseSpawnDefaultsTestCase(unittest.TestCase):
|
||
def test_user_example(self):
|
||
self.assertEqual(
|
||
parse_spawn_defaults("子代理用 gpt-6-luna max"),
|
||
{"model": "gpt-6-luna", "effort": "max"})
|
||
|
||
def test_labelled_forms(self):
|
||
self.assertEqual(
|
||
parse_spawn_defaults("子任务模型:gpt-6-luna,强度:max"),
|
||
{"model": "gpt-6-luna", "effort": "max"})
|
||
self.assertEqual(
|
||
parse_spawn_defaults("子任务用 gpt-6.1-sol,effort high"),
|
||
{"model": "gpt-6.1-sol", "effort": "high"})
|
||
|
||
def test_english_and_reversed(self):
|
||
self.assertEqual(
|
||
parse_spawn_defaults("subtasks use gpt-6-luna with effort max"),
|
||
{"model": "gpt-6-luna", "effort": "max"})
|
||
self.assertEqual(
|
||
parse_spawn_defaults("用 gpt-6-luna max 跑子任务"),
|
||
{"model": "gpt-6-luna", "effort": "max"})
|
||
|
||
def test_no_false_positives(self):
|
||
self.assertEqual(
|
||
parse_spawn_defaults("子任务要写报告,不要用默认模型"), {})
|
||
self.assertEqual(
|
||
parse_spawn_defaults("模型选择很重要,子任务用默认配置"), {})
|
||
# 裸说主线用 X 是本任务自己的模型,不算派生默认。
|
||
self.assertEqual(parse_spawn_defaults("主线用 gpt-6-luna max"), {})
|
||
# 跨子句的强度词不算。
|
||
self.assertEqual(
|
||
parse_spawn_defaults("子任务用 gpt-6-luna。下一句强度是 high。"),
|
||
{"model": "gpt-6-luna"})
|
||
|
||
|
||
class ServerChildModelTestCase(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 body is not None:
|
||
payload = json.dumps(body).encode("utf-8")
|
||
hdrs["Content-Type"] = "application/json"
|
||
if key is not None:
|
||
hdrs["X-API-Key"] = key
|
||
try:
|
||
conn.request(method, path, body=payload, headers=hdrs)
|
||
resp = conn.getresponse()
|
||
data = resp.read()
|
||
finally:
|
||
conn.close()
|
||
try:
|
||
parsed = json.loads(data.decode("utf-8")) if data else None
|
||
except ValueError:
|
||
parsed = None
|
||
return resp.status, parsed
|
||
|
||
def _create(self, prompt, **extra):
|
||
body = {"project_id": self.project["id"], "prompt": prompt}
|
||
body.update(extra)
|
||
status, task = self._req("POST", "/tasks", body, self.owner_key)
|
||
self.assertEqual(status, 201, task)
|
||
return task
|
||
|
||
def _set_terminal(self, task_id):
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
conn.execute(
|
||
"UPDATE tasks SET status = 'completed' WHERE id = ?",
|
||
(task_id,))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
def test_create_explicit_child_fields(self):
|
||
task = self._create(
|
||
"做事", model="gpt-5.6-terra", effort="low",
|
||
child_model="gpt-6-luna", child_effort="max")
|
||
self.assertEqual(task["model"], "gpt-5.6-terra")
|
||
self.assertEqual(task["child_model"], "gpt-6-luna")
|
||
self.assertEqual(task["child_effort"], "max")
|
||
|
||
def test_create_prompt_directive_fills_child_fields(self):
|
||
task = self._create("子代理用 gpt-6-luna max,做三件事")
|
||
self.assertEqual(task["child_model"], "gpt-6-luna")
|
||
self.assertEqual(task["child_effort"], "max")
|
||
# prompt 指令只填派生默认,不改本任务自己的模型。
|
||
self.assertIsNone(task["model"])
|
||
|
||
def test_create_explicit_wins_over_prompt(self):
|
||
task = self._create(
|
||
"子代理用 gpt-6-luna max", child_model="gpt-6-sol",
|
||
child_effort="high")
|
||
self.assertEqual(task["child_model"], "gpt-6-sol")
|
||
self.assertEqual(task["child_effort"], "high")
|
||
|
||
def test_create_invalid_child_effort_400(self):
|
||
status, body = self._req("POST", "/tasks", {
|
||
"project_id": self.project["id"], "prompt": "做事",
|
||
"child_effort": "huge"}, self.owner_key)
|
||
self.assertEqual(status, 400)
|
||
|
||
def test_fork_copies_child_fields(self):
|
||
parent = self._create(
|
||
"做事", model="gpt-5.6-terra", child_model="gpt-6-luna",
|
||
child_effort="max")
|
||
self._set_terminal(parent["id"])
|
||
status, forked = self._req(
|
||
"POST", "/tasks/%s/fork" % parent["id"],
|
||
{"message": "继续"}, self.owner_key)
|
||
self.assertEqual(status, 201, forked)
|
||
self.assertEqual(forked["model"], "gpt-5.6-terra")
|
||
self.assertEqual(forked["child_model"], "gpt-6-luna")
|
||
self.assertEqual(forked["child_effort"], "max")
|
||
|
||
def test_continue_updates_child_fields(self):
|
||
task = self._create("做事")
|
||
self._set_terminal(task["id"])
|
||
status, continued = self._req(
|
||
"POST", "/tasks/%s/continue" % task["id"],
|
||
{"message": "再跑一轮", "child_model": "gpt-6-luna",
|
||
"child_effort": "max"}, self.owner_key)
|
||
self.assertEqual(status, 200, continued)
|
||
self.assertEqual(continued["child_model"], "gpt-6-luna")
|
||
self.assertEqual(continued["child_effort"], "max")
|
||
|
||
|
||
class WorkerChildModelTestCase(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.auth = AuthStore(self.db_path)
|
||
self.user_id, _key = self.auth.create_user("tester")
|
||
self._insert("PM", status="running", model="gpt-5.6-terra",
|
||
effort="low", child_model="gpt-6-luna",
|
||
child_effort="max")
|
||
|
||
def tearDown(self):
|
||
try:
|
||
self.auth.conn.close()
|
||
except Exception:
|
||
pass
|
||
self.tmp.cleanup()
|
||
|
||
def _insert(self, task_id, status="queued", **cols):
|
||
now = time.time()
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
conn.execute(
|
||
"INSERT INTO tasks(id, project_id, prompt, status,"
|
||
" idempotency_key, depends_on, priority, created_by,"
|
||
" created_at, updated_at, last_heartbeat)"
|
||
" VALUES(?, 'p1', 'do work', ?, NULL, '[]', 0, 'tester',"
|
||
" ?, ?, NULL)",
|
||
(task_id, status, now, now))
|
||
for key, value in cols.items():
|
||
conn.execute(
|
||
"UPDATE tasks SET %s = ? WHERE id = ?" % key,
|
||
(value, task_id))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
def _child_row(self):
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
return conn.execute(
|
||
"SELECT model, effort, child_model, child_effort"
|
||
" FROM tasks WHERE parent_task_id = 'PM'").fetchone()
|
||
finally:
|
||
conn.close()
|
||
|
||
def _deliver(self, creates):
|
||
worker = Worker(self.db_path, "/fake/host.py", env=FAKE_ENV)
|
||
worker._deliver_marker_tools(worker.get_task("PM"), {
|
||
"status": "completed", "task_creates": creates,
|
||
"task_polls": [], "wait_threads": [], "task_cancels": [],
|
||
"task_retries": []})
|
||
|
||
def test_marker_inherits_child_defaults_when_block_blank(self):
|
||
self._deliver([{"name": "E1", "prompt": "do e1",
|
||
"model": None, "effort": None, "executor": None}])
|
||
row = self._child_row()
|
||
self.assertIsNotNone(row)
|
||
self.assertEqual(row[0], "gpt-6-luna")
|
||
self.assertEqual(row[1], "max")
|
||
# 派生默认本身也传给子任务,隔代派生不丢。
|
||
self.assertEqual(row[2], "gpt-6-luna")
|
||
self.assertEqual(row[3], "max")
|
||
|
||
def test_marker_explicit_block_wins(self):
|
||
self._deliver([{"name": "E1", "prompt": "do e1",
|
||
"model": "gpt-6-sol", "effort": "high",
|
||
"executor": None}])
|
||
row = self._child_row()
|
||
self.assertEqual(row[0], "gpt-6-sol")
|
||
self.assertEqual(row[1], "high")
|
||
|
||
def test_marker_falls_back_to_parent_model_without_child_defaults(self):
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
conn.execute(
|
||
"UPDATE tasks SET child_model = NULL,"
|
||
" child_effort = NULL WHERE id = 'PM'")
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
self._deliver([{"name": "E1", "prompt": "do e1",
|
||
"model": None, "effort": None, "executor": None}])
|
||
row = self._child_row()
|
||
self.assertEqual(row[0], "gpt-5.6-terra")
|
||
self.assertEqual(row[1], "low")
|
||
|
||
def test_coordinator_inherits_child_defaults(self):
|
||
worker = Worker(self.db_path, "/fake/host.py", env=FAKE_ENV)
|
||
parent = worker.get_task("PM")
|
||
children = worker._coord_sync_children(parent, [
|
||
{"name": "后端", "prompt": "做后端", "model": None,
|
||
"effort": None, "executor": None}])
|
||
self.assertEqual(len(children), 1)
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
row = conn.execute(
|
||
"SELECT model, effort FROM tasks WHERE id = ?",
|
||
(children[0]["id"],)).fetchone()
|
||
finally:
|
||
conn.close()
|
||
self.assertEqual(row[0], "gpt-6-luna")
|
||
self.assertEqual(row[1], "max")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|