定时任务:
- 运行详情页新增「执行过程」标签页,按 agent_run_id 拉取该次运行的
agent 消息(GET /scheduled-tasks/runs/{id}/messages),运行中每 4s
自动刷新,可观察卡住的任务停在哪一步
- 详情页「返回」按钮回到来源列表(普通/管理员)并重新选中原任务
- 管理员可下载、删除任意用户的执行记录(DELETE /runs/{id} 加管理员兜底)
MySQL 兼容修复:
- 合并迁移 20260518_06 汇合 ai-writing 与 thread-shares 两个 head
- 20260517_02 改用 SQLAlchemy Core 构造,规避保留字 key 的 1064 报错
- list_messages_by_run 各事件存储补 user_id 形参以保持接口一致
测试: 新增 test_scheduled_task_admin_run_delete.py
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
"""Tests for admin (cross-user) deletion of scheduled-task runs.
|
|
|
|
Regular users may only delete runs they own; admins may delete any run. This
|
|
covers the service/store layer that the admin-aware ``DELETE /runs/{id}``
|
|
route relies on.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
|
|
from deerflow.persistence.scheduled_tasks import MemoryScheduledTaskStore
|
|
from deerflow.runtime.scheduler.service import ScheduledTaskService
|
|
|
|
|
|
@pytest.fixture
|
|
def store() -> MemoryScheduledTaskStore:
|
|
return MemoryScheduledTaskStore()
|
|
|
|
|
|
def _build_service(store: MemoryScheduledTaskStore) -> ScheduledTaskService:
|
|
service = ScheduledTaskService(app=None, store=store, poll_interval_seconds=60.0)
|
|
service._element_client = None # noqa: SLF001
|
|
return service
|
|
|
|
|
|
async def _seed_run(store: MemoryScheduledTaskStore, *, run_id: str, task_id: str, user_id: str) -> None:
|
|
await store.create_run_once(
|
|
{
|
|
"id": run_id,
|
|
"task_id": task_id,
|
|
"user_id": user_id,
|
|
"scheduler_thread_id": f"thread-{user_id}",
|
|
"scheduled_for": datetime.now(UTC),
|
|
"status": "succeeded",
|
|
"started_at": datetime.now(UTC),
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_delete_run_removes_any_owner_run(store):
|
|
await _seed_run(store, run_id="r1", task_id="t1", user_id="alice")
|
|
service = _build_service(store)
|
|
|
|
assert await service.admin_delete_run("r1") is True
|
|
assert await store.get_run_any("r1") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_delete_run_missing_returns_false(store):
|
|
service = _build_service(store)
|
|
assert await service.admin_delete_run("nope") is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_owner_delete_run_still_scoped_to_owner(store):
|
|
"""A non-owner, non-subscriber deleting another user's run gets False."""
|
|
await _seed_run(store, run_id="r1", task_id="t1", user_id="alice")
|
|
service = _build_service(store)
|
|
|
|
assert await service.delete_run("bob", "r1") is False
|
|
assert await store.get_run_any("r1") is not None # untouched
|
|
|
|
# The owner can still delete their own run.
|
|
assert await service.delete_run("alice", "r1") is True
|
|
assert await store.get_run_any("r1") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_store_delete_run_any_independent_of_owner(store):
|
|
await _seed_run(store, run_id="r1", task_id="t1", user_id="alice")
|
|
|
|
assert await store.delete_run_any("r1") is True
|
|
assert await store.delete_run_any("r1") is False
|