Files
deerflow-code/offline-backend-20260512/backend/tests/test_scheduled_task_admin_run_delete.py
T
吉鑫升andClaude Opus 4.7 e3cde9be25 feat: 定时任务运行详情执行过程视图 + 管理员下载删除 + MySQL 迁移修复
定时任务:
- 运行详情页新增「执行过程」标签页,按 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>
2026-05-18 21:13:58 +08:00

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