任务列表 task-list 标记 + 首轮项目上下文注入
This commit is contained in:
1 parent
73288f08db
commit
2fd73c1121
5 files changed
+510
-13
No files matched your search
@@ -0,0 +1,256 @@
|
||||
"""任务列表标记与项目注入单测:task-list 解析、sandbox 提取、
|
||||
worker 投递与首轮项目注入。
|
||||
|
||||
不跑真实模型、不联网:解析是纯函数;worker 投递与注入用
|
||||
init_db 建的临时 SQLite 库,直接调 _deliver_marker_tools 与
|
||||
_apply_project_injection,不走 run_one 全量流程。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
try:
|
||||
from cloud.marker_tools import extract_task_lists, format_task
|
||||
from cloud.sandbox import LocalExecutor, _parse_host_result
|
||||
from cloud.server import 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.marker_tools import extract_task_lists, format_task
|
||||
from cloud.sandbox import LocalExecutor, _parse_host_result
|
||||
from cloud.server import init_db
|
||||
from cloud.worker import Worker
|
||||
|
||||
FAKE_ENV = {"XCM_API_KEY": "test-placeholder"}
|
||||
|
||||
|
||||
class ExtractTestCase(unittest.TestCase):
|
||||
def test_list_default_and_alias(self):
|
||||
out = extract_task_lists("```task-list\n{}\n```")
|
||||
self.assertEqual(out, [{"status": None, "q": None, "limit": 20}])
|
||||
out = extract_task_lists(
|
||||
"```task_list\n{\"status\": \"failed\", \"limit\": 5}\n```")
|
||||
self.assertEqual(out, [{"status": "failed", "q": None,
|
||||
"limit": 5}])
|
||||
|
||||
def test_list_limit_clamped_and_invalid_skipped(self):
|
||||
out = extract_task_lists(
|
||||
"```task-list\n{\"limit\": 999}\n```")
|
||||
self.assertEqual(out[0]["limit"], 50)
|
||||
out = extract_task_lists(
|
||||
"```task-list\n{\"limit\": 0}\n```")
|
||||
self.assertEqual(out[0]["limit"], 1)
|
||||
for body in ("{\"status\": \"doing\"}",
|
||||
"{\"limit\": \"many\"}",
|
||||
"{\"limit\": true}",
|
||||
"{\"q\": 123}"):
|
||||
with self.subTest(body=body):
|
||||
self.assertEqual(extract_task_lists(
|
||||
"```task-list\n%s\n```" % body), [])
|
||||
|
||||
def test_list_cap_five(self):
|
||||
text = "\n".join("```task-list\n{}\n```" for _ in range(6))
|
||||
self.assertEqual(len(extract_task_lists(text)), 5)
|
||||
|
||||
def test_format_task_truncates(self):
|
||||
line = format_task({"id": "t1", "status": "failed",
|
||||
"prompt": "x" * 100, "created_by": "u1",
|
||||
"created_at": 1700000000})
|
||||
self.assertIn("…", line)
|
||||
self.assertIn("2023-11-14T22:13:20Z", line)
|
||||
|
||||
def test_sandbox_extracts_both_executors(self):
|
||||
text = "```task-list\n{\"status\": \"failed\"}\n```"
|
||||
fake = SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout=json.dumps({"status": "completed", "text": text}),
|
||||
stderr="")
|
||||
local = LocalExecutor("/fake/host.py")._result_from_runner("t1", fake)
|
||||
self.assertEqual(len(local["task_lists"]), 1)
|
||||
docker = _parse_host_result("t1", fake)
|
||||
self.assertEqual(len(docker["task_lists"]), 1)
|
||||
|
||||
|
||||
class WorkerTaskListTestCase(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._insert("PM", status="running", created_by="tester")
|
||||
self._member("tester", "maintainer")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _insert(self, task_id, status="queued", created_by="tester",
|
||||
project_id="p1", prompt="do work", **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(?, ?, ?, ?, NULL, '[]', 0, ?, ?, ?, NULL)",
|
||||
(task_id, project_id, prompt, status, created_by,
|
||||
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 _member(self, user_id, role):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY,"
|
||||
" username TEXT UNIQUE, api_key_hash TEXT,"
|
||||
" created_at REAL)")
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS project_members("
|
||||
"project_id TEXT, user_id TEXT, role TEXT,"
|
||||
" PRIMARY KEY(project_id, user_id))")
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO project_members"
|
||||
"(project_id, user_id, role) VALUES('p1', ?, ?)",
|
||||
(user_id, role))
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO users(id, username, api_key_hash,"
|
||||
" created_at) VALUES(?, ?, '', 0)", (user_id, user_id))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _project(self, project_id="p1", name="演示项目",
|
||||
repo_url="git@github.com:openqj/demo.git"):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO projects(id, name, owner_id,"
|
||||
" created_at, repo_url, auto_pr)"
|
||||
" VALUES(?, ?, 'tester', ?, ?, 0)",
|
||||
(project_id, name, time.time(), repo_url))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _worker(self):
|
||||
return Worker(self.db_path, "/fake/host.py", env=FAKE_ENV)
|
||||
|
||||
def _followup_bodies(self, task_id="PM"):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT body FROM task_followups"
|
||||
" WHERE task_id = ?", (task_id,)).fetchall()
|
||||
return [row[0] for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_deliver_lists_all_with_counts(self):
|
||||
self._insert("E1", status="failed", prompt="修 bug")
|
||||
self._insert("E2", status="running", prompt="做功能")
|
||||
self._insert("X1", status="failed", project_id="p2",
|
||||
prompt="别项目")
|
||||
worker = self._worker()
|
||||
worker._deliver_marker_tools(worker.get_task("PM"), {
|
||||
"status": "completed",
|
||||
"task_lists": [{"status": None, "q": None, "limit": 20}]})
|
||||
bodies = self._followup_bodies()
|
||||
self.assertEqual(len(bodies), 1)
|
||||
# 全部状态 + 计数在,跨项目任务不在,PM 自己在(本项目)。
|
||||
self.assertIn("failed=1", bodies[0])
|
||||
self.assertIn("修 bug", bodies[0])
|
||||
self.assertIn("做功能", bodies[0])
|
||||
self.assertNotIn("别项目", bodies[0])
|
||||
|
||||
def test_deliver_status_filter_and_limit(self):
|
||||
self._insert("E1", status="failed", prompt="修 bug")
|
||||
self._insert("E2", status="failed", prompt="修另一 bug")
|
||||
self._insert("E3", status="completed", prompt="已做完")
|
||||
worker = self._worker()
|
||||
worker._deliver_marker_tools(worker.get_task("PM"), {
|
||||
"status": "completed",
|
||||
"task_lists": [{"status": "failed", "q": None, "limit": 1}]})
|
||||
bodies = self._followup_bodies()
|
||||
self.assertIn("共 2 条", bodies[0])
|
||||
self.assertIn("还有 1 条未列出", bodies[0])
|
||||
self.assertNotIn("已做完", bodies[0])
|
||||
|
||||
def test_deliver_denied_without_read_permission(self):
|
||||
self._insert("PM2", status="running", created_by="outsider")
|
||||
worker = self._worker()
|
||||
worker._deliver_marker_tools(worker.get_task("PM2"), {
|
||||
"status": "completed",
|
||||
"task_lists": [{"status": None, "q": None, "limit": 20}]})
|
||||
bodies = self._followup_bodies("PM2")
|
||||
self.assertIn("无 read 权限", bodies[0])
|
||||
|
||||
def test_deliver_goal_sender_ignored(self):
|
||||
self._insert("G1", status="running", goal_mode=1)
|
||||
worker = self._worker()
|
||||
worker._deliver_marker_tools(worker.get_task("G1"), {
|
||||
"status": "completed",
|
||||
"task_lists": [{"status": None, "q": None, "limit": 20}]})
|
||||
self.assertEqual(self._followup_bodies("G1"), [])
|
||||
|
||||
def test_injection_project_and_env(self):
|
||||
self._project()
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO environments(id, project_id, name, branch,"
|
||||
" executor, status, error_text, repo_url, created_by,"
|
||||
" created_at, updated_at)"
|
||||
" VALUES('env1', 'p1', '主环境', 'main', 'docker',"
|
||||
" 'ready', NULL, 'git@github.com:openqj/demo.git',"
|
||||
" 'tester', ?, ?)", (time.time(), time.time()))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
worker = self._worker()
|
||||
task = {"id": "PM", "project_id": "p1", "prompt": "做事",
|
||||
"created_by": "tester", "environment_id": "env1"}
|
||||
out = worker._apply_project_injection(task)
|
||||
self.assertIn("演示项目", out["prompt"])
|
||||
self.assertIn("git@github.com:openqj/demo.git", out["prompt"])
|
||||
self.assertIn("主环境", out["prompt"])
|
||||
self.assertIn("task-list", out["prompt"])
|
||||
self.assertEqual(task["prompt"], "做事")
|
||||
|
||||
def test_injection_unbound_repo_and_goal(self):
|
||||
self._project(repo_url=None)
|
||||
worker = self._worker()
|
||||
goal = {"id": "G1", "project_id": "p1", "prompt": "做事",
|
||||
"goal": "做目标", "created_by": "tester",
|
||||
"goal_mode": 1}
|
||||
out = worker._apply_project_injection(goal)
|
||||
self.assertIn("未绑定", out["prompt"])
|
||||
# goal 任务同时改 goal(执行器取 goal 优先于 prompt)。
|
||||
self.assertIn("未绑定", out["goal"])
|
||||
self.assertIn("演示项目", out["goal"])
|
||||
|
||||
def test_injection_skips_coordinator_resume_missing(self):
|
||||
worker = self._worker()
|
||||
coord = {"id": "C1", "project_id": "p1", "prompt": "x",
|
||||
"coordinator_parts": "a,b"}
|
||||
self.assertIs(worker._apply_project_injection(coord), coord)
|
||||
resume = {"id": "PM", "project_id": "p1", "prompt": "x",
|
||||
"_resume": True}
|
||||
self.assertIs(worker._apply_project_injection(resume), resume)
|
||||
missing = {"id": "PM", "project_id": "nope", "prompt": "x"}
|
||||
self.assertIs(worker._apply_project_injection(missing), missing)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user