同项目任务互发消息、对方做完可回消息:API POST /tasks/<目标>/messages (from_task_id,目标项目 create_task + 发送项目 read 鉴权)与 agent 回合输出标记块(worker 回合后解析投递,不用联网、不用凭证,本地 与 Docker 都通)。投递不中断任务:对方运行中/排队中排成后续消息 (轮次边界执行,direct 优先);对方终态则续跑复活(resume_message + queued),A→B→A 往返不受回合时序影响。拒绝项:自己、跨项目、goal 目标、已合并、复活位被占;每轮最多 5 块,仅 completed 轮次投递; 目标执行时配额门照常生效。控制台后续消息列显示发送方,新增给 其他任务发消息框(落已代理的 /tasks 下,不用改 nginx)。 测试:新增 test_peer_messages(15)、test_peer_messages_server(8)、 test_worker_peer(4);全量 cloud 654、host 64、deploy 19 全过(cloud 4 跳过)。
220 lines
8.2 KiB
Python
220 lines
8.2 KiB
Python
"""任务间互发消息单测:标记块提取 + 投递分支。
|
|
|
|
不跑真实模型、不联网:extract 是纯函数;deliver 用内存
|
|
SQLite + 最小表结构(tasks 只含投递读到的列,followups
|
|
表由 peer_messages 自己建)。
|
|
"""
|
|
|
|
import sqlite3
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from cloud.peer_messages import (
|
|
deliver_peer_message,
|
|
extract_peer_messages,
|
|
peer_message_prefix,
|
|
)
|
|
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
from cloud.peer_messages import (
|
|
deliver_peer_message,
|
|
extract_peer_messages,
|
|
peer_message_prefix,
|
|
)
|
|
|
|
|
|
def _conn():
|
|
conn = sqlite3.connect(":memory:")
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute(
|
|
"CREATE TABLE tasks(id TEXT PRIMARY KEY, project_id TEXT,"
|
|
" created_by TEXT, status TEXT, verify_command TEXT,"
|
|
" goal_mode INTEGER, resume_message TEXT, updated_at REAL)")
|
|
return conn
|
|
|
|
|
|
def _task(conn, task_id, project_id="p1", status="queued",
|
|
created_by="tester", verify_command=None, goal_mode=None,
|
|
resume_message=None):
|
|
conn.execute(
|
|
"INSERT INTO tasks(id, project_id, created_by, status,"
|
|
" verify_command, goal_mode, resume_message, updated_at)"
|
|
" VALUES(?, ?, ?, ?, ?, ?, ?, 0)",
|
|
(task_id, project_id, created_by, status, verify_command,
|
|
goal_mode, resume_message))
|
|
conn.commit()
|
|
|
|
|
|
def _followups(conn, task_id=None, sender=None):
|
|
sql = "SELECT * FROM task_followups"
|
|
conds = []
|
|
args = []
|
|
if task_id is not None:
|
|
conds.append("task_id = ?")
|
|
args.append(task_id)
|
|
if sender is not None:
|
|
conds.append("sender_task_id = ?")
|
|
args.append(sender)
|
|
if conds:
|
|
sql += " WHERE " + " AND ".join(conds)
|
|
try:
|
|
return conn.execute(sql, args).fetchall()
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _task_row(conn, task_id):
|
|
return conn.execute(
|
|
"SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
|
|
|
|
|
|
class ExtractTestCase(unittest.TestCase):
|
|
def test_single_block_default_queued(self):
|
|
text = ("做完了。\n```task-message\n"
|
|
"{\"to\": \"T2\", \"body\": \"请评审\"}\n```\n")
|
|
msgs = extract_peer_messages(text)
|
|
self.assertEqual(len(msgs), 1)
|
|
self.assertEqual(msgs[0]["to_task_id"], "T2")
|
|
self.assertEqual(msgs[0]["body"], "请评审")
|
|
self.assertEqual(msgs[0]["mode"], "queued")
|
|
|
|
def test_direct_mode_and_alias_key(self):
|
|
text = ("```task-message\n{\"to_task_id\": \"T2\","
|
|
" \"body\": \"急\", \"mode\": \"direct\"}\n```")
|
|
msgs = extract_peer_messages(text)
|
|
self.assertEqual(msgs[0]["mode"], "direct")
|
|
|
|
def test_bad_mode_falls_back_to_queued(self):
|
|
text = ("```task-message\n{\"to\": \"T2\", \"body\": \"x\","
|
|
" \"mode\": \"smoke-signal\"}\n```")
|
|
self.assertEqual(extract_peer_messages(text)[0]["mode"], "queued")
|
|
|
|
def test_invalid_blocks_skipped(self):
|
|
text = (
|
|
"```task-message\nnot json\n```\n"
|
|
"```task-message\n{\"body\": \"no to\"}\n```\n"
|
|
"```task-message\n{\"to\": \"T2\"}\n```\n"
|
|
"```task-message\n{\"to\": \"T2\", \"body\": \" \"}\n```\n"
|
|
"```task-message\n{\"to\": \"T2\", \"body\": \"ok\"}\n```\n")
|
|
msgs = extract_peer_messages(text)
|
|
self.assertEqual(len(msgs), 1)
|
|
self.assertEqual(msgs[0]["body"], "ok")
|
|
|
|
def test_body_too_long_skipped(self):
|
|
text = ("```task-message\n{\"to\": \"T2\", \"body\": \""
|
|
+ "x" * 20001 + "\"}\n```")
|
|
self.assertEqual(extract_peer_messages(text), [])
|
|
|
|
def test_max_five_per_turn(self):
|
|
blocks = "".join(
|
|
"```task-message\n{\"to\": \"T%d\", \"body\": \"m%d\"}\n```\n"
|
|
% (i, i) for i in range(7))
|
|
self.assertEqual(len(extract_peer_messages(blocks)), 5)
|
|
|
|
def test_no_fence_or_empty(self):
|
|
self.assertEqual(extract_peer_messages("plain text"), [])
|
|
self.assertEqual(extract_peer_messages(""), [])
|
|
self.assertEqual(extract_peer_messages(None), [])
|
|
|
|
|
|
class DeliverTestCase(unittest.TestCase):
|
|
def test_running_target_becomes_followup(self):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status="running")
|
|
status, detail = deliver_peer_message(conn, "T1", "T2", "请评审")
|
|
self.assertEqual(status, "delivered")
|
|
rows = _followups(conn, task_id="T2", sender="T1")
|
|
self.assertEqual(len(rows), 1)
|
|
self.assertEqual(rows[0]["body"], "请评审")
|
|
self.assertEqual(rows[0]["mode"], "queued")
|
|
self.assertEqual(rows[0]["status"], "pending")
|
|
self.assertEqual(rows[0]["created_by"], "tester")
|
|
self.assertEqual(rows[0]["id"], detail)
|
|
|
|
def test_queued_target_direct_mode(self):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status="queued")
|
|
status, _ = deliver_peer_message(conn, "T1", "T2", "急", "direct")
|
|
self.assertEqual(status, "delivered")
|
|
self.assertEqual(_followups(conn, task_id="T2")[0]["mode"], "direct")
|
|
|
|
def test_completed_target_revives(self):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status="completed")
|
|
status, detail = deliver_peer_message(conn, "T1", "T2", "回来一下")
|
|
self.assertEqual(status, "revived")
|
|
self.assertEqual(detail, "")
|
|
row = _task_row(conn, "T2")
|
|
self.assertEqual(row["status"], "queued")
|
|
self.assertEqual(row["resume_message"],
|
|
peer_message_prefix("T1") + "回来一下")
|
|
self.assertEqual(_followups(conn, task_id="T2"), [])
|
|
|
|
def test_revive_blocked_when_resume_occupied(self):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status="completed", resume_message="旧消息")
|
|
status, detail = deliver_peer_message(conn, "T1", "T2", "新消息")
|
|
self.assertEqual(status, "rejected")
|
|
self.assertIn("pending resume", detail)
|
|
self.assertEqual(_task_row(conn, "T2")["resume_message"], "旧消息")
|
|
|
|
def test_failed_and_interrupted_revive(self):
|
|
for st in ("failed", "interrupted"):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status=st)
|
|
status, _ = deliver_peer_message(conn, "T1", "T2", "回来")
|
|
self.assertEqual(status, "revived")
|
|
self.assertEqual(_task_row(conn, "T2")["status"], "queued")
|
|
|
|
def test_goal_targets_rejected(self):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status="running", verify_command="true")
|
|
status, _ = deliver_peer_message(conn, "T1", "T2", "x")
|
|
self.assertEqual(status, "rejected")
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status="running", goal_mode=1)
|
|
status, _ = deliver_peer_message(conn, "T1", "T2", "x")
|
|
self.assertEqual(status, "rejected")
|
|
|
|
def test_self_cross_project_merged_missing_rejected(self):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", project_id="p2")
|
|
_task(conn, "T3", status="merged")
|
|
self.assertEqual(
|
|
deliver_peer_message(conn, "T1", "T1", "x")[0], "rejected")
|
|
self.assertEqual(
|
|
deliver_peer_message(conn, "T1", "T2", "x")[0], "rejected")
|
|
self.assertEqual(
|
|
deliver_peer_message(conn, "T1", "T3", "x")[0], "rejected")
|
|
self.assertEqual(
|
|
deliver_peer_message(conn, "T1", "NOPE", "x")[0], "rejected")
|
|
self.assertEqual(
|
|
deliver_peer_message(conn, "NOPE", "T1", "x")[0], "rejected")
|
|
# 拒绝不写任何东西、不改状态。
|
|
self.assertEqual(_followups(conn), [])
|
|
self.assertEqual(_task_row(conn, "T3")["status"], "merged")
|
|
|
|
def test_body_validation(self):
|
|
conn = _conn()
|
|
_task(conn, "T1")
|
|
_task(conn, "T2", status="running")
|
|
self.assertEqual(
|
|
deliver_peer_message(conn, "T1", "T2", " ")[0], "rejected")
|
|
self.assertEqual(
|
|
deliver_peer_message(conn, "T1", "T2", "x" * 20001)[0],
|
|
"rejected")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|