"""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": "日报"})