任务列表 task-list 标记 + 首轮项目上下文注入

This commit is contained in:
host committed 2026-10-30 05:11:06 +08:00
1 parent 73288f08db
commit 2fd73c1121
5 files changed
+510 -13

No files matched your search

+256
View File
@@ -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()