Files
deerflow-code/offline-backend-20260512/backend/tests/test_scheduled_task_name_uniqueness.py
T
吉鑫升andClaude Opus 4.7 358d5c72a6 feat: 业务库切换 MySQL + 修复定时任务运行详情递归与重名 500
- 业务 ORM 数据支持 MySQL 后端(asyncmy),新增 PortableJSON/
  BeijingDateTime 可移植列类型、迁移脚本与切库脚本
- 修复 GET /api/scheduled-tasks/runs/{id} 的 _load_run 无限递归
  (误调用自身,应委托 service.get_run)导致的 RecursionError 500
- 定时任务名按用户唯一:create/update/admin_update 主动校验,
  冲突抛 DuplicateTaskNameError,路由转 409 而非裸 IntegrityError 500
- 补充回归测试 test_scheduled_task_run_lookup / _name_uniqueness

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:34:14 +08:00

117 lines
4.3 KiB
Python

"""Tests for per-user scheduled-task name uniqueness.
Task names are unique per user (``uq_scheduled_tasks_user_name``). The service
proactively rejects collisions with :class:`DuplicateTaskNameError` so the API
returns a 409 instead of letting a raw DB ``IntegrityError`` surface as a 500.
"""
from __future__ import annotations
from datetime import UTC, datetime
import pytest
from deerflow.persistence.scheduled_tasks import MemoryScheduledTaskStore
from deerflow.runtime.scheduler import DuplicateTaskNameError
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
def _seed_task(store: MemoryScheduledTaskStore, *, task_id: str, user_id: str, name: str) -> dict:
task = {
"task_id": task_id,
"user_id": user_id,
"scheduler_thread_id": f"thread-{user_id}",
"name": name,
"prompt": "hello",
"schedule_text": "0 9 * * *",
"cron_expr": "0 9 * * *",
"timezone": "UTC",
"enabled": True,
"next_run_at": datetime.now(UTC),
"execution_context": {},
}
store._tasks[task_id] = task # noqa: SLF001
return task
@pytest.mark.asyncio
async def test_create_task_rejects_duplicate_name(store):
"""A second task with the same (user_id, name) is refused before any insert."""
_seed_task(store, task_id="t1", user_id="alice", name="日报")
service = _build_service(store)
with pytest.raises(DuplicateTaskNameError):
await service.create_task(user_id="alice", name="日报", prompt="p", schedule="0 9 * * *")
@pytest.mark.asyncio
async def test_create_task_treats_whitespace_padded_name_as_duplicate(store):
"""The name is trimmed before the uniqueness check, matching what is stored."""
_seed_task(store, task_id="t1", user_id="alice", name="日报")
service = _build_service(store)
with pytest.raises(DuplicateTaskNameError):
await service.create_task(user_id="alice", name=" 日报 ", prompt="p", schedule="0 9 * * *")
@pytest.mark.asyncio
async def test_create_task_allows_same_name_for_different_user(store):
"""The constraint is per user — another user may reuse the name.
The create proceeds past the name check and only fails later because the
test service has no app wired for scheduler-thread setup; the point here is
that no DuplicateTaskNameError is raised.
"""
_seed_task(store, task_id="t1", user_id="alice", name="日报")
service = _build_service(store)
with pytest.raises(Exception) as exc_info:
await service.create_task(user_id="bob", name="日报", prompt="p", schedule="0 9 * * *")
assert not isinstance(exc_info.value, DuplicateTaskNameError)
@pytest.mark.asyncio
async def test_update_task_rejects_rename_to_existing_name(store):
"""Renaming a task onto a sibling's name is refused."""
_seed_task(store, task_id="t1", user_id="alice", name="日报")
_seed_task(store, task_id="t2", user_id="alice", name="周报")
service = _build_service(store)
with pytest.raises(DuplicateTaskNameError):
await service.update_task("alice", "t2", {"name": "日报"})
@pytest.mark.asyncio
async def test_update_task_allows_keeping_own_name(store):
"""Re-saving a task without changing its name is not a self-collision."""
_seed_task(store, task_id="t1", user_id="alice", name="日报")
service = _build_service(store)
updated = await service.update_task("alice", "t1", {"name": "日报", "prompt": "new"})
assert updated is not None
assert updated["name"] == "日报"
assert updated["prompt"] == "new"
@pytest.mark.asyncio
async def test_admin_update_task_rejects_rename_to_existing_name(store):
"""Admin cross-user rename still honours the owner's name uniqueness."""
_seed_task(store, task_id="t1", user_id="alice", name="日报")
_seed_task(store, task_id="t2", user_id="alice", name="周报")
service = _build_service(store)
with pytest.raises(DuplicateTaskNameError):
await service.admin_update_task("t2", {"name": "日报"})