382 lines
16 KiB
Python
382 lines
16 KiB
Python
"""覆盖率 78%->85% 第 4 步:cloud/worker.py _deliver_marker_tools
|
||
剩余分支的真实行为测试。
|
||
|
||
父任务属主用 AuthStore 建真实 owner + 项目成员关系(权限
|
||
True/False 两路都走真实 check_permission);待办/自动化走
|
||
真实 SQLite 表;任务文件走临时 host.py 同级 work 目录。
|
||
绝不跑模型、不碰真实网关。
|
||
"""
|
||
|
||
import os
|
||
import sqlite3
|
||
import tempfile
|
||
import time
|
||
import unittest
|
||
|
||
from cloud.auth import AuthStore
|
||
from cloud.server import init_db
|
||
from cloud.worker import Worker
|
||
|
||
|
||
def _insert_task(db_path, task_id, status="queued", **kw):
|
||
conn = sqlite3.connect(db_path)
|
||
try:
|
||
cols = {"id": task_id, "project_id": kw.pop("project_id", "proj"),
|
||
"prompt": kw.pop("prompt", "do it"), "status": status,
|
||
"created_by": kw.pop("created_by", "owner1"),
|
||
"created_at": kw.pop("created_at", 1000.0),
|
||
"updated_at": kw.pop("updated_at", 1000.0)}
|
||
cols.update(kw)
|
||
names = ", ".join(cols)
|
||
placeholders = ", ".join("?" * len(cols))
|
||
conn.execute("INSERT INTO tasks(%s) VALUES(%s)"
|
||
% (names, placeholders), tuple(cols.values()))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _rows(db_path, sql, params=()):
|
||
conn = sqlite3.connect(db_path)
|
||
try:
|
||
return conn.execute(sql, params).fetchall()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _set(db_path, sql, params=()):
|
||
conn = sqlite3.connect(db_path)
|
||
try:
|
||
conn.execute(sql, params)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
class MarkerBase(unittest.TestCase):
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.addCleanup(self.tmp.cleanup)
|
||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||
init_db(self.db_path)
|
||
auth = AuthStore(self.db_path)
|
||
self.owner_id, _ = auth.create_user("owner")
|
||
auth.create_project("proj", self.owner_id)
|
||
self.outsider_id, _ = auth.create_user("outsider")
|
||
auth.conn.close()
|
||
# host.py 替身放在临时目录:任务文件解析走它同级的
|
||
# work/host-worktrees/<任务名>,不碰真实 /fake 或仓库。
|
||
self.fake_host = os.path.join(self.tmp.name, "host.py")
|
||
with open(self.fake_host, "w") as f:
|
||
f.write("# fake host\n")
|
||
self.worker = Worker(
|
||
self.db_path, self.fake_host,
|
||
env={"XCM_API_KEY": "test-placeholder"},
|
||
heartbeat_file=os.path.join(self.tmp.name, "hb.json"),
|
||
worker_id="cov85-worker4", max_retries=0,
|
||
retry_backoff_seconds=0)
|
||
|
||
def _parent(self, pid="parent1", creator=None):
|
||
if self.worker.get_task(pid) is None:
|
||
_insert_task(self.db_path, pid, status="completed",
|
||
created_by=creator or self.owner_id)
|
||
return self.worker.get_task(pid)
|
||
|
||
def _followups(self, pid):
|
||
return [r[0] for r in _rows(
|
||
self.db_path,
|
||
"SELECT body FROM task_followups WHERE task_id = ?",
|
||
(pid,))]
|
||
|
||
def _deliver(self, pid, result, creator=None):
|
||
parent = self._parent(pid, creator)
|
||
self.worker._deliver_marker_tools(parent, result)
|
||
return self._followups(pid)
|
||
|
||
|
||
class CreatesGateTest(MarkerBase):
|
||
def test_creates_quota_reject(self):
|
||
_set(self.db_path,
|
||
"INSERT INTO user_quotas(user_id, quota_usd_micros,"
|
||
" updated_at) VALUES(?, 0, 0)", (self.owner_id,))
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_creates": [{"prompt": "x", "name": "c",
|
||
"model": None, "effort": None,
|
||
"executor": None}]})
|
||
self.assertEqual(len(bodies), 1)
|
||
self.assertIn("创建跳过", bodies[0])
|
||
self.assertEqual(_rows(
|
||
self.db_path,
|
||
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = 'parent1'"),
|
||
[(0,)])
|
||
|
||
def test_creates_depth_and_direct_caps(self):
|
||
# 直接派生上限:_MARKER_MAX_DIRECT 个子任务已存在时
|
||
# 再创建被拒(报告写清上限,不炸)。
|
||
from cloud.worker import _MARKER_MAX_DIRECT
|
||
for i in range(_MARKER_MAX_DIRECT):
|
||
_insert_task(self.db_path, "kid-%d" % i, status="queued",
|
||
parent_task_id="parent1",
|
||
created_by=self.owner_id)
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_creates": [{"prompt": "x", "name": "c",
|
||
"model": None, "effort": None,
|
||
"executor": None}]})
|
||
self.assertIn("直接派生子任务已达上限", bodies[0])
|
||
|
||
|
||
class AutomationCreateTest(MarkerBase):
|
||
def test_automation_auth_reject(self):
|
||
# 外人(非成员)当父任务属主:权限 False,整轮跳过。
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"automation_creates": [{"prompt": "巡检", "name": "a1",
|
||
"interval_seconds": 3600}]},
|
||
creator=self.outsider_id)
|
||
self.assertIn("自动化创建跳过", bodies[0])
|
||
self.assertIn("无 create_task 权限", bodies[0])
|
||
|
||
def test_automation_past_first_run_reject_and_success(self):
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"automation_creates": [
|
||
{"prompt": "旧的", "name": "old",
|
||
"interval_seconds": 3600,
|
||
"first_run_at": time.time() - 3600},
|
||
{"prompt": "巡检任务", "name": "new",
|
||
"interval_seconds": 3600,
|
||
"first_run_at": time.time() + 3600}]})
|
||
self.assertIn("首次运行时刻已过去", bodies[0])
|
||
self.assertIn("已创建自动化", bodies[0])
|
||
self.assertEqual(_rows(
|
||
self.db_path,
|
||
"SELECT COUNT(*) FROM automations WHERE project_id = 'proj'"),
|
||
[(1,)])
|
||
|
||
def test_automation_quota_reject(self):
|
||
_set(self.db_path,
|
||
"INSERT INTO user_quotas(user_id, quota_usd_micros,"
|
||
" updated_at) VALUES(?, 0, 0)", (self.owner_id,))
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"automation_creates": [{"prompt": "巡检", "name": "a1",
|
||
"interval_seconds": 3600}]})
|
||
self.assertIn("自动化创建跳过", bodies[0])
|
||
self.assertIn("配额检查未通过", bodies[0])
|
||
|
||
|
||
class CancelRetryTest(MarkerBase):
|
||
def test_cancel_descendant_and_retry_failed(self):
|
||
_insert_task(self.db_path, "kid1", status="queued",
|
||
parent_task_id="parent1",
|
||
created_by=self.owner_id)
|
||
_insert_task(self.db_path, "kid2", status="failed",
|
||
parent_task_id="parent1",
|
||
created_by=self.owner_id)
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_cancels": [{"task_id": "kid1"}],
|
||
"task_retries": [{"task_id": "kid2"}]})
|
||
self.assertIn("已取消任务 kid1", bodies[0])
|
||
self.assertIn("已重试任务 kid2", bodies[0])
|
||
statuses = dict(_rows(self.db_path,
|
||
"SELECT id, status FROM tasks"))
|
||
self.assertEqual(statuses["kid1"], "interrupted")
|
||
self.assertEqual(statuses["kid2"], "queued")
|
||
|
||
def test_cancel_other_project_rejected(self):
|
||
_insert_task(self.db_path, "alien", status="queued",
|
||
project_id="other", created_by="someone-else")
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_cancels": [{"task_id": "alien"}]})
|
||
self.assertIn("拒绝", bodies[0])
|
||
self.assertEqual(_rows(
|
||
self.db_path,
|
||
"SELECT status FROM tasks WHERE id = 'alien'"),
|
||
[("queued",)])
|
||
|
||
def test_cancel_missing_target_rejected(self):
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_cancels": [{"task_id": "ghost"}]})
|
||
self.assertIn("拒绝", bodies[0])
|
||
|
||
|
||
class TodoTaskBlocksTest(MarkerBase):
|
||
def test_todo_create_update_list(self):
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"todo_creates": [{"title": "写周报", "description": "周五前",
|
||
"status": "pending",
|
||
"priority": "high"}]})
|
||
self.assertIn("已创建待办", bodies[0])
|
||
todo_id = _rows(self.db_path,
|
||
"SELECT id FROM todos")[0][0]
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"todo_updates": [{"id": todo_id,
|
||
"fields": {"status": "in_progress"}}],
|
||
"todo_lists": [{"status": "in_progress"}]})
|
||
# 第二次 deliver 追加第二条报告:更新与列表都在 bodies[1]。
|
||
self.assertIn("已更新待办", bodies[1])
|
||
self.assertIn("待办列表", bodies[1])
|
||
self.assertIn("写周报", bodies[1])
|
||
|
||
def test_todo_auth_reject_and_bad_refs(self):
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"todo_creates": [{"title": "挂别人任务",
|
||
"task_id": "ghost"}],
|
||
"todo_updates": [{"id": "ghost-todo",
|
||
"fields": {"status": "done"}}],
|
||
"todo_lists": [{}]}, creator=self.outsider_id)
|
||
self.assertIn("拒绝", bodies[0])
|
||
# 外人无 read/create:三类块都被拒,待办表根本未建。
|
||
self.assertEqual(_rows(
|
||
self.db_path,
|
||
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
||
" AND name = 'todos'"), [])
|
||
|
||
def test_todo_create_cross_project_ref_rejected(self):
|
||
_insert_task(self.db_path, "alien", status="queued",
|
||
project_id="other", created_by="someone-else")
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"todo_creates": [{"title": "挂跨项目",
|
||
"task_id": "alien"}]})
|
||
self.assertIn("拒绝", bodies[0])
|
||
|
||
def test_task_list_filters_and_cost_and_quota(self):
|
||
_insert_task(self.db_path, "kid1", status="completed",
|
||
parent_task_id="parent1",
|
||
created_by=self.owner_id, prompt="做加法")
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_lists": [{"status": "completed", "q": "加法",
|
||
"limit": 10}],
|
||
"task_costs": [{"task_id": "kid1"}],
|
||
"quota_status": [{}]})
|
||
self.assertIn("任务列表", bodies[0])
|
||
self.assertIn("kid1", bodies[0])
|
||
self.assertIn("配额", bodies[0])
|
||
|
||
def test_task_list_empty_and_cost_missing(self):
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_lists": [{"status": "failed", "q": None,
|
||
"limit": 10}],
|
||
"task_costs": [{"task_id": "ghost"}]})
|
||
self.assertIn("任务列表:无匹配项", bodies[0])
|
||
self.assertIn("不存在", bodies[0])
|
||
|
||
def test_quota_status_no_creator_rejected(self):
|
||
# created_by 为空:_marker_auth 返回 False,read 路先被拒。
|
||
_insert_task(self.db_path, "parent1", status="completed",
|
||
created_by="")
|
||
parent = self.worker.get_task("parent1")
|
||
self.worker._deliver_marker_tools(parent, {
|
||
"status": "completed", "quota_status": [{}]})
|
||
bodies = self._followups("parent1")
|
||
self.assertIn("拒绝", bodies[0])
|
||
|
||
|
||
class TaskFilesTest(MarkerBase):
|
||
def _worktree(self, tid):
|
||
d = os.path.join(self.tmp.name, "work", "host-worktrees", tid)
|
||
os.makedirs(d, exist_ok=True)
|
||
return d
|
||
|
||
def test_file_read_dir_list_and_missing(self):
|
||
d = self._worktree("kid1")
|
||
with open(os.path.join(d, "a.txt"), "w") as f:
|
||
f.write("hello file")
|
||
with open(os.path.join(d, "b.bin"), "wb") as f:
|
||
f.write(b"\x00\x01\x02binary")
|
||
_insert_task(self.db_path, "kid1", status="completed",
|
||
parent_task_id="parent1",
|
||
created_by=self.owner_id)
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_files": [{"task_id": "kid1", "path": "a.txt"},
|
||
{"task_id": "kid1", "path": ""},
|
||
{"task_id": "kid1", "path": "nope.txt"},
|
||
{"task_id": "kid1", "path": "b.bin"},
|
||
{"task_id": "kid1", "path": "../evil"}]})
|
||
body = bodies[0]
|
||
self.assertIn("hello file", body)
|
||
self.assertIn("a.txt", body)
|
||
self.assertIn("不存在", body)
|
||
self.assertIn("二进制文件", body)
|
||
self.assertIn("路径逃逸", body)
|
||
|
||
def test_file_no_worktree_rejected(self):
|
||
_insert_task(self.db_path, "kid1", status="completed",
|
||
parent_task_id="parent1",
|
||
created_by=self.owner_id)
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_files": [{"task_id": "kid1", "path": "a.txt"}]})
|
||
self.assertIn("没有本地 worktree", bodies[0])
|
||
|
||
def test_file_auth_reject(self):
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_files": [{"task_id": "kid1", "path": "a.txt"}]},
|
||
creator=self.outsider_id)
|
||
self.assertIn("拒绝", bodies[0])
|
||
|
||
|
||
class PollWaitTest(MarkerBase):
|
||
def test_wait_mixed_targets(self):
|
||
_insert_task(self.db_path, "kid1", status="completed",
|
||
parent_task_id="parent1",
|
||
created_by=self.owner_id)
|
||
_insert_task(self.db_path, "alien", status="completed",
|
||
project_id="other", created_by="someone-else")
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"wait_threads": [{"task_ids": ["kid1", "ghost", "alien",
|
||
"parent1"],
|
||
"interval_seconds": 0.05,
|
||
"timeout_seconds": 5, "chars": 200}]})
|
||
body = bodies[0]
|
||
self.assertIn("kid1", body)
|
||
self.assertIn("不存在", body)
|
||
self.assertIn("跨项目", body)
|
||
self.assertIn("不能等待自己", body)
|
||
|
||
def test_poll_self_and_cross_project(self):
|
||
_insert_task(self.db_path, "alien", status="completed",
|
||
project_id="other", created_by="someone-else")
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"task_polls": [{"task_id": "parent1",
|
||
"interval_seconds": 0.05,
|
||
"timeout_seconds": 5, "chars": 200},
|
||
{"task_id": "alien",
|
||
"interval_seconds": 0.05,
|
||
"timeout_seconds": 5, "chars": 200},
|
||
{"task_id": "ghost",
|
||
"interval_seconds": 0.05,
|
||
"timeout_seconds": 5, "chars": 200}]})
|
||
body = bodies[0]
|
||
self.assertIn("不能轮询自己", body)
|
||
self.assertIn("跨项目", body)
|
||
self.assertIn("不存在", body)
|
||
|
||
def test_goal_convert_queues_goal_followup_after_report(self):
|
||
bodies = self._deliver("parent1", {
|
||
"status": "completed",
|
||
"todo_lists": [{}],
|
||
"goal_converts": [{"goal": "收齐周报"}]})
|
||
self.assertEqual(len(bodies), 2)
|
||
self.assertIn("待办列表", bodies[0])
|
||
self.assertEqual(bodies[1], "/goal 收齐周报")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|