- 新模块 cloud/marker_tools.py:纯逻辑解析、校验、快照格式化、 控制门禁、派生代数与直接上限计数 - sandbox 两处解析提取五种标记块;worker 在 5 处投递点与 _deliver_peer_messages 同款位置执行,报告排成后续消息下一轮回父任务 - wait 每块 cap 到 8 个目标(与服务端 /wait 对齐,spawn 原先的 10 个收紧), 每轮最多 2 块;取消/重试门禁:同项目、非自己、非 goal、 派生链或同创建人,另查 create_task / approve_merge 权限, 查不到时收紧到仅派生链内 - goal 与协调员任务不解析;单测 30 个,cloud 全量 1043 个、4 跳过、0 失败
647 lines
27 KiB
Python
647 lines
27 KiB
Python
"""任务内标记工具单测:解析、门禁、快照格式化与 worker 投递。
|
|
|
|
不跑真实模型、不联网:解析与门禁是纯函数;worker 投递用
|
|
init_db 建的临时 SQLite 库 + 最小任务行,直接调
|
|
_deliver_marker_tools,不走 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:
|
|
import cloud.marker_tools as mt
|
|
from cloud.marker_tools import (
|
|
ancestry_relation,
|
|
check_control_gate,
|
|
extract_task_cancels,
|
|
extract_task_creates,
|
|
extract_task_polls,
|
|
extract_task_retries,
|
|
extract_wait_threads,
|
|
format_snapshot,
|
|
)
|
|
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]))
|
|
import cloud.marker_tools as mt
|
|
from cloud.marker_tools import (
|
|
ancestry_relation,
|
|
check_control_gate,
|
|
extract_task_cancels,
|
|
extract_task_creates,
|
|
extract_task_polls,
|
|
extract_task_retries,
|
|
extract_wait_threads,
|
|
format_snapshot,
|
|
)
|
|
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"}
|
|
|
|
|
|
def _mem_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, parent_task_id TEXT,"
|
|
" coordinator_part TEXT, verify_command TEXT,"
|
|
" goal_mode INTEGER, result_text TEXT, error_text TEXT,"
|
|
" executor TEXT, model TEXT, actual_model TEXT,"
|
|
" turn_id TEXT, last_heartbeat REAL, worktree_host TEXT,"
|
|
" pr_url TEXT, pr_number INTEGER, environment_id TEXT)")
|
|
return conn
|
|
|
|
|
|
def _mem_task(conn, task_id, project_id="p1", status="queued",
|
|
created_by="tester", parent=None, part=None,
|
|
verify_command=None, goal_mode=None, result=None):
|
|
conn.execute(
|
|
"INSERT INTO tasks(id, project_id, created_by, status,"
|
|
" parent_task_id, coordinator_part, verify_command, goal_mode,"
|
|
" result_text, error_text, executor, model, actual_model,"
|
|
" turn_id, last_heartbeat, worktree_host, pr_url, pr_number,"
|
|
" environment_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL,"
|
|
" NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL)",
|
|
(task_id, project_id, created_by, status, parent, part,
|
|
verify_command, goal_mode, result))
|
|
conn.commit()
|
|
|
|
|
|
class ExtractTestCase(unittest.TestCase):
|
|
def test_create_basic_and_caps(self):
|
|
blocks = "".join(
|
|
"```task-create\n{\"name\": \"n%d\", \"prompt\": \"do %d\"}\n```\n"
|
|
% (i, i) for i in range(7))
|
|
creates = extract_task_creates(blocks)
|
|
self.assertEqual(len(creates), 5)
|
|
self.assertEqual(creates[0]["name"], "n0")
|
|
self.assertEqual(creates[0]["prompt"], "do 0")
|
|
|
|
def test_create_invalid_blocks_skipped(self):
|
|
text = (
|
|
"```task-create\nnot json\n```\n"
|
|
"```task-create\n{\"prompt\": \" \"}\n```\n"
|
|
"```task-create\n{\"prompt\": \"" + "x" * 20001 + "\"}\n```\n"
|
|
"```task-create\n{\"prompt\": \"ok\", \"effort\": \"huge\"}\n```\n"
|
|
"```task-create\n{\"prompt\": \"ok\", \"executor\": \"mars\"}\n```\n"
|
|
"```task-create\n{\"prompt\": \"good\"}\n```\n")
|
|
creates = extract_task_creates(text)
|
|
self.assertEqual(len(creates), 1)
|
|
self.assertEqual(creates[0]["prompt"], "good")
|
|
|
|
def test_create_fence_at_line_start_truncates_to_invalid(self):
|
|
# 行首的围栏会把块截断成非法 JSON 而被跳过(非贪婪匹配);
|
|
# 同行内的围栏不构成块边界,能正常解析(见下一断言的对照)。
|
|
text = ("```task-create\n{\"prompt\": \"a\n```\n b\"}\n```\n"
|
|
"```task-create\n{\"prompt\": \"good\"}\n```\n")
|
|
creates = extract_task_creates(text)
|
|
self.assertEqual([c["prompt"] for c in creates], ["good"])
|
|
inline = extract_task_creates(
|
|
"```task-create\n{\"prompt\": \"a ``` b\"}\n```\n")
|
|
self.assertEqual([c["prompt"] for c in inline], ["a ``` b"])
|
|
|
|
def test_poll_clamps_and_read_alias(self):
|
|
text = (
|
|
"```task-poll\n{\"task_id\": \"T1\", \"interval_seconds\": 999,"
|
|
" \"timeout_seconds\": 99999, \"chars\": 1}\n```\n"
|
|
"```task-read\n{\"task_id\": \"T2\"}\n```\n"
|
|
"```task-read\n{\"task_id\": \"T3\", \"wait_seconds\": 30}\n```\n")
|
|
polls = extract_task_polls(text)
|
|
self.assertEqual(len(polls), 3)
|
|
self.assertEqual(polls[0]["interval_seconds"], 60)
|
|
self.assertEqual(polls[0]["timeout_seconds"], 3600)
|
|
self.assertEqual(polls[0]["chars"], 200)
|
|
self.assertEqual(polls[1]["timeout_seconds"], 0)
|
|
self.assertEqual(polls[2]["timeout_seconds"], 30)
|
|
self.assertEqual(polls[2]["interval_seconds"], 30)
|
|
|
|
def test_poll_max_five_and_missing_id_skipped(self):
|
|
blocks = "".join(
|
|
"```task-poll\n{\"task_id\": \"T%d\"}\n```\n" % i
|
|
for i in range(6))
|
|
blocks += "```task-poll\n{\"interval_seconds\": 5}\n```\n"
|
|
self.assertEqual(len(extract_task_polls(blocks)), 5)
|
|
|
|
def test_wait_cap_eight_not_ten_and_max_two_blocks(self):
|
|
ids = ["T%d" % i for i in range(12)]
|
|
text = (
|
|
"```wait-threads\n%s\n```\n"
|
|
"```wait_threads\n{\"task_ids\": [\"A\"]}\n```\n"
|
|
"```wait-threads\n{\"task_ids\": [\"B\"]}\n```\n"
|
|
% json.dumps({"task_ids": ids}))
|
|
waits = extract_wait_threads(text)
|
|
# 每块最多 8 个(与服务端 /wait 对齐,不是 spawn 原先的 10 个),
|
|
# 每轮最多 2 块。
|
|
self.assertEqual(len(waits), 2)
|
|
self.assertEqual(len(waits[0]["task_ids"]), 8)
|
|
self.assertEqual(waits[0]["task_ids"][0], "T0")
|
|
self.assertEqual(waits[1]["task_ids"], ["A"])
|
|
|
|
def test_wait_invalid_blocks_skipped(self):
|
|
text = (
|
|
"```wait-threads\nnot json\n```\n"
|
|
"```wait-threads\n{\"task_ids\": []}\n```\n"
|
|
"```wait-threads\n{\"task_ids\": \"not-a-list\"}\n```\n"
|
|
"```wait-threads\n{\"task_ids\": [\"A\", \"A\", \" \", \"B\"]}\n```\n")
|
|
waits = extract_wait_threads(text)
|
|
self.assertEqual(len(waits), 1)
|
|
self.assertEqual(waits[0]["task_ids"], ["A", "B"])
|
|
|
|
def test_control_extract_caps_and_aliases(self):
|
|
cancels = "".join(
|
|
"```task-cancel\n{\"task_id\": \"C%d\"}\n```\n" % i
|
|
for i in range(6))
|
|
self.assertEqual(len(extract_task_cancels(cancels)), 5)
|
|
retries = extract_task_retries(
|
|
"```task_retry\n{\"task_id\": \"R1\"}\n```\n"
|
|
"```task-retry\n{\"to_task_id\": \"R2\"}\n```\n"
|
|
"```task-retry\n{\"no_id\": true}\n```\n")
|
|
self.assertEqual([r["task_id"] for r in retries], ["R1", "R2"])
|
|
|
|
|
|
class GateTestCase(unittest.TestCase):
|
|
def _sender(self, task_id="PM", project_id="p1", created_by="tester"):
|
|
return {"id": task_id, "project_id": project_id,
|
|
"created_by": created_by, "verify_command": None,
|
|
"goal_mode": None}
|
|
|
|
def _target(self, task_id="E1", project_id="p1", status="running",
|
|
created_by="tester", verify_command=None, goal_mode=None):
|
|
return {"id": task_id, "project_id": project_id, "status": status,
|
|
"created_by": created_by, "verify_command": verify_command,
|
|
"goal_mode": goal_mode}
|
|
|
|
def test_cancel_terminal_rejected(self):
|
|
for status in ("completed", "failed", "interrupted", "merged",
|
|
"merge-failed"):
|
|
ok, _ = check_control_gate(
|
|
self._target(status=status), self._sender(),
|
|
"descendant", "cancel")
|
|
self.assertFalse(ok, status)
|
|
|
|
def test_retry_only_three_states(self):
|
|
for status in ("failed", "interrupted", "merge-failed"):
|
|
ok, _ = check_control_gate(
|
|
self._target(status=status), self._sender(),
|
|
"descendant", "retry")
|
|
self.assertTrue(ok, status)
|
|
for status in ("completed", "merged", "queued", "running"):
|
|
ok, _ = check_control_gate(
|
|
self._target(status=status), self._sender(),
|
|
"descendant", "retry")
|
|
self.assertFalse(ok, status)
|
|
|
|
def test_cross_project_self_goal_merged_rejected(self):
|
|
sender = self._sender()
|
|
ok, _ = check_control_gate(
|
|
self._target(project_id="p2"), sender, "descendant",
|
|
"cancel")
|
|
self.assertFalse(ok)
|
|
ok, _ = check_control_gate(
|
|
self._target(task_id="PM"), sender, "descendant", "cancel")
|
|
self.assertFalse(ok)
|
|
ok, _ = check_control_gate(
|
|
self._target(verify_command="make test"), sender,
|
|
"descendant", "cancel")
|
|
self.assertFalse(ok)
|
|
ok, _ = check_control_gate(
|
|
self._target(goal_mode=1), sender, "descendant", "retry")
|
|
self.assertFalse(ok)
|
|
ok, _ = check_control_gate(
|
|
self._target(status="merged"), sender, "descendant",
|
|
"cancel")
|
|
self.assertFalse(ok)
|
|
|
|
def test_relation_other_rejected_same_creator_passes(self):
|
|
ok, _ = check_control_gate(
|
|
self._target(created_by="other"), self._sender(), "other",
|
|
"cancel")
|
|
self.assertFalse(ok)
|
|
ok, _ = check_control_gate(
|
|
self._target(created_by="tester"), self._sender(),
|
|
"same_creator", "cancel")
|
|
self.assertTrue(ok)
|
|
|
|
def test_ancestry_chain(self):
|
|
conn = _mem_conn()
|
|
_mem_task(conn, "PM")
|
|
_mem_task(conn, "E1", parent="PM")
|
|
_mem_task(conn, "E2", parent="E1")
|
|
_mem_task(conn, "X1", created_by="other")
|
|
sender = {"id": "PM", "project_id": "p1", "created_by": "tester"}
|
|
target_e2 = mt.fetch_task_snapshot(conn, "E2")
|
|
self.assertEqual(ancestry_relation(conn, sender, target_e2),
|
|
"descendant")
|
|
target_x = mt.fetch_task_snapshot(conn, "X1")
|
|
self.assertEqual(ancestry_relation(conn, sender, target_x),
|
|
"other")
|
|
target_e1 = mt.fetch_task_snapshot(conn, "E1")
|
|
self.assertEqual(ancestry_relation(conn, sender, target_e1),
|
|
"descendant")
|
|
|
|
|
|
class SnapshotTestCase(unittest.TestCase):
|
|
def test_format_terminal_truncates(self):
|
|
snap = {"id": "T1", "status": "completed",
|
|
"result_text": "r" * 5000, "error_text": "",
|
|
"executor": "docker", "model": "m", "actual_model": None,
|
|
"turn_id": None, "last_heartbeat": None,
|
|
"worktree_host": None}
|
|
text = format_snapshot(snap, 200)
|
|
self.assertIn("状态:completed", text)
|
|
self.assertIn("r" * 200, text)
|
|
self.assertNotIn("r" * 201, text)
|
|
|
|
def test_format_nonterminal_timeout_cancelled(self):
|
|
base = {"id": "T1", "status": "running", "result_text": "",
|
|
"error_text": "", "executor": None, "model": None,
|
|
"actual_model": None, "turn_id": None,
|
|
"last_heartbeat": None, "worktree_host": None}
|
|
self.assertIn("仍在运行或排队",
|
|
format_snapshot(base, 200))
|
|
self.assertIn("等待超时",
|
|
format_snapshot(base, 200, timed_out=True))
|
|
self.assertIn("父任务被取消",
|
|
format_snapshot(base, 200, cancelled=True))
|
|
|
|
|
|
class SandboxExtractTestCase(unittest.TestCase):
|
|
def _text(self, *blocks):
|
|
return "\n".join(blocks)
|
|
|
|
def test_local_and_docker_extract_all_four(self):
|
|
text = self._text(
|
|
"```task-create\n{\"prompt\": \"do x\"}\n```",
|
|
"```task-poll\n{\"task_id\": \"T1\"}\n```",
|
|
"```wait-threads\n{\"task_ids\": [\"T1\", \"T2\"]}\n```",
|
|
"```task-cancel\n{\"task_id\": \"T2\"}\n```",
|
|
"```task-retry\n{\"task_id\": \"T3\"}\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_creates"]), 1)
|
|
self.assertEqual(len(local["task_polls"]), 1)
|
|
self.assertEqual(len(local["wait_threads"]), 1)
|
|
self.assertEqual(len(local["task_cancels"]), 1)
|
|
self.assertEqual(len(local["task_retries"]), 1)
|
|
docker = _parse_host_result("t1", fake)
|
|
self.assertEqual(len(docker["task_creates"]), 1)
|
|
self.assertEqual(len(docker["wait_threads"]), 1)
|
|
self.assertEqual(len(docker["task_retries"]), 1)
|
|
|
|
|
|
class WorkerDeliverTestCase(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", parent=None, **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, "do work", status, created_by,
|
|
now, now))
|
|
if parent is not None:
|
|
conn.execute(
|
|
"UPDATE tasks SET parent_task_id = ? WHERE id = ?",
|
|
(parent, task_id))
|
|
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 _worker(self):
|
|
return Worker(self.db_path, "/fake/host.py", env=FAKE_ENV)
|
|
|
|
def _sender(self, worker):
|
|
return worker.get_task("PM")
|
|
|
|
def _followup_bodies(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
rows = conn.execute(
|
|
"SELECT body FROM task_followups"
|
|
" WHERE task_id = 'PM'").fetchall()
|
|
return [row[0] for row in rows]
|
|
finally:
|
|
conn.close()
|
|
|
|
def _status(self, task_id):
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
row = conn.execute(
|
|
"SELECT status, result_text FROM tasks WHERE id = ?",
|
|
(task_id,)).fetchone()
|
|
return row[0], row[1]
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_deliver_create_and_poll(self):
|
|
worker = self._worker()
|
|
sender = self._sender(worker)
|
|
worker._deliver_marker_tools(sender, {
|
|
"status": "completed",
|
|
"task_creates": [{"name": "E1", "prompt": "do e1",
|
|
"model": None, "effort": None,
|
|
"executor": None}],
|
|
"task_polls": [],
|
|
"wait_threads": [],
|
|
"task_cancels": [],
|
|
"task_retries": []})
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
row = conn.execute(
|
|
"SELECT id, parent_task_id, status, created_by FROM tasks"
|
|
" WHERE parent_task_id = 'PM'").fetchone()
|
|
finally:
|
|
conn.close()
|
|
self.assertIsNotNone(row)
|
|
self.assertEqual(row[1], "PM")
|
|
self.assertEqual(row[2], "queued")
|
|
self.assertEqual(row[3], "tester")
|
|
|
|
def test_deliver_poll_reports_snapshot(self):
|
|
self._insert("E1", status="completed", parent="PM",
|
|
result_text="done-thing")
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [{"task_id": "E1", "interval_seconds": 5,
|
|
"timeout_seconds": 0, "chars": 2000}],
|
|
"wait_threads": [], "task_cancels": [], "task_retries": []})
|
|
bodies = self._followup_bodies()
|
|
self.assertEqual(len(bodies), 1)
|
|
self.assertIn("done-thing", bodies[0])
|
|
|
|
def test_deliver_wait_reports_all_targets(self):
|
|
self._insert("E1", status="completed", parent="PM",
|
|
result_text="first-done")
|
|
self._insert("E2", status="queued", parent="PM")
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [],
|
|
"wait_threads": [{"task_ids": ["E1", "E2"],
|
|
"interval_seconds": 5,
|
|
"timeout_seconds": 0, "chars": 2000}],
|
|
"task_cancels": [], "task_retries": []})
|
|
bodies = self._followup_bodies()
|
|
self.assertEqual(len(bodies), 1)
|
|
self.assertIn("E1", bodies[0])
|
|
self.assertIn("E2", bodies[0])
|
|
self.assertIn("first-done", bodies[0])
|
|
|
|
def test_deliver_cancel_descendant(self):
|
|
self._insert("E1", status="running", parent="PM")
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [{"task_id": "E1"}],
|
|
"task_retries": []})
|
|
self.assertEqual(self._status("E1")[0], "interrupted")
|
|
|
|
def test_deliver_cancel_cross_project_and_terminal_rejected(self):
|
|
self._insert("X1", status="running", project_id="p2")
|
|
self._insert("E1", status="completed", parent="PM")
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [{"task_id": "X1"}, {"task_id": "E1"}],
|
|
"task_retries": []})
|
|
self.assertEqual(self._status("X1")[0], "running")
|
|
self.assertEqual(self._status("E1")[0], "completed")
|
|
bodies = self._followup_bodies()
|
|
self.assertTrue(any("拒绝" in body for body in bodies))
|
|
|
|
def test_deliver_retry_clears_old_result(self):
|
|
self._insert("E1", status="failed", parent="PM",
|
|
result_text="old-result", error_text="old-error")
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
conn.execute(
|
|
"UPDATE tasks SET actual_model = 'm' WHERE id = 'E1'")
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [],
|
|
"task_retries": [{"task_id": "E1"}]})
|
|
status, result = self._status("E1")
|
|
self.assertEqual(status, "queued")
|
|
self.assertIsNone(result)
|
|
|
|
def test_deliver_retry_denied_without_approve_merge(self):
|
|
self._member("tester", "viewer")
|
|
self._insert("E1", status="failed", parent="PM",
|
|
result_text="old-result")
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [],
|
|
"task_retries": [{"task_id": "E1"}]})
|
|
self.assertEqual(self._status("E1")[0], "failed")
|
|
bodies = self._followup_bodies()
|
|
self.assertTrue(any("拒绝" in body for body in bodies))
|
|
|
|
def test_deliver_goal_and_coordinator_sender_ignored(self):
|
|
self._insert("G1", status="running", verify_command="make test")
|
|
worker = self._worker()
|
|
goal_sender = worker.get_task("G1")
|
|
worker._deliver_marker_tools(goal_sender, {
|
|
"status": "completed",
|
|
"task_creates": [{"name": "E1", "prompt": "do e1",
|
|
"model": None, "effort": None,
|
|
"executor": None}],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [], "task_retries": []})
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
count = conn.execute(
|
|
"SELECT COUNT(*) FROM tasks"
|
|
" WHERE parent_task_id = 'G1'").fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
self.assertEqual(count, 0)
|
|
|
|
def test_deliver_create_quota_exceeded_skips(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS user_quotas("
|
|
"user_id TEXT PRIMARY KEY, quota_usd_micros BIGINT,"
|
|
" updated_at DOUBLE PRECISION)")
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO user_quotas"
|
|
"(user_id, quota_usd_micros, updated_at)"
|
|
" VALUES('tester', 0, 0)")
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed",
|
|
"task_creates": [{"name": "E1", "prompt": "do e1",
|
|
"model": None, "effort": None,
|
|
"executor": None}],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [], "task_retries": []})
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
count = conn.execute(
|
|
"SELECT COUNT(*) FROM tasks"
|
|
" WHERE parent_task_id = 'PM'").fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
self.assertEqual(count, 0)
|
|
bodies = self._followup_bodies()
|
|
self.assertTrue(any("配额" in body for body in bodies))
|
|
|
|
def test_deliver_create_direct_limit_rejected(self):
|
|
for i in range(20):
|
|
self._insert("OLD%d" % i, status="completed", parent="PM")
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed",
|
|
"task_creates": [{"name": "E1", "prompt": "do e1",
|
|
"model": None, "effort": None,
|
|
"executor": None}],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [], "task_retries": []})
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
count = conn.execute(
|
|
"SELECT COUNT(*) FROM tasks"
|
|
" WHERE parent_task_id = 'PM'").fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
self.assertEqual(count, 20)
|
|
bodies = self._followup_bodies()
|
|
self.assertTrue(any("上限" in body for body in bodies))
|
|
|
|
def test_deliver_create_depth_limit_rejected(self):
|
|
self._insert("A", status="completed", parent="PM")
|
|
self._insert("B", status="completed", parent="A")
|
|
self._insert("C", status="running", parent="B")
|
|
worker = self._worker()
|
|
sender_c = worker.get_task("C")
|
|
worker._deliver_marker_tools(sender_c, {
|
|
"status": "completed",
|
|
"task_creates": [{"name": "E1", "prompt": "do e1",
|
|
"model": None, "effort": None,
|
|
"executor": None}],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [], "task_retries": []})
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
count = conn.execute(
|
|
"SELECT COUNT(*) FROM tasks"
|
|
" WHERE parent_task_id = 'C'").fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
self.assertEqual(count, 0)
|
|
|
|
def test_deliver_wait_timeout_reports_pending(self):
|
|
self._insert("E1", status="queued", parent="PM")
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [],
|
|
"wait_threads": [{"task_ids": ["E1"],
|
|
"interval_seconds": 1,
|
|
"timeout_seconds": 1, "chars": 2000}],
|
|
"task_cancels": [], "task_retries": []})
|
|
bodies = self._followup_bodies()
|
|
self.assertEqual(len(bodies), 1)
|
|
self.assertIn("E1", bodies[0])
|
|
self.assertIn("等待超时", bodies[0])
|
|
|
|
def test_deliver_control_auth_failure_falls_back_to_descendant(self):
|
|
from unittest import mock
|
|
self._insert("E1", status="running", parent="PM")
|
|
self._insert("S1", status="running")
|
|
worker = self._worker()
|
|
with mock.patch("cloud.worker.AuthStore",
|
|
side_effect=RuntimeError("auth down")):
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "completed", "task_creates": [],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [{"task_id": "E1"},
|
|
{"task_id": "S1"}],
|
|
"task_retries": []})
|
|
# 权限查不到时:派生链内仍可取消,同创建人-only 被拒绝。
|
|
self.assertEqual(self._status("E1")[0], "interrupted")
|
|
self.assertEqual(self._status("S1")[0], "running")
|
|
|
|
def test_deliver_non_completed_round_ignored(self):
|
|
worker = self._worker()
|
|
worker._deliver_marker_tools(self._sender(worker), {
|
|
"status": "failed",
|
|
"task_creates": [{"name": "E1", "prompt": "do e1",
|
|
"model": None, "effort": None,
|
|
"executor": None}],
|
|
"task_polls": [], "wait_threads": [],
|
|
"task_cancels": [], "task_retries": []})
|
|
self.assertEqual(self._followup_bodies(), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|