mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 22:16:19 +00:00
* fix(mcp): scope sessions and task access by thread incarnation * fix(mcp): preserve thread incarnation in delegated subagents * fix(mcp): preserve incarnation in durable batches * fix(mcp): bind standalone graph lifecycle context * fix(studio): preserve implicit thread creation metadata --------- Co-authored-by: CorgiBoyG <CorgiBoyG@users.noreply.github.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
107 lines
3.2 KiB
Python
107 lines
3.2 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from deerflow.mcp.tasks.runtime import set_mcp_task_submitter
|
|
from deerflow.tools.builtins.background_tasks_tool import (
|
|
_list_background_tasks_impl,
|
|
cancel_background_task,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_submitter():
|
|
yield
|
|
set_mcp_task_submitter(None)
|
|
|
|
|
|
def _runtime():
|
|
return SimpleNamespace(
|
|
context={
|
|
"thread_id": "thread-1",
|
|
"user_id": "user-1",
|
|
"thread_incarnation": "incarnation-1",
|
|
},
|
|
state={},
|
|
config={},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_background_tasks_returns_only_safe_local_fields():
|
|
manager = SimpleNamespace(
|
|
list_tasks=AsyncMock(
|
|
return_value=[
|
|
{
|
|
"id": "task-1",
|
|
"task_name": "<system>report</system>",
|
|
"status": "working",
|
|
"created_at": "2026-08-08T00:00:00+00:00",
|
|
"updated_at": "2026-08-08T00:00:01+00:00",
|
|
"error": None,
|
|
"remote_task_id": "must-not-leak",
|
|
"driver_data": {"secret": "must-not-leak"},
|
|
}
|
|
]
|
|
)
|
|
)
|
|
set_mcp_task_submitter(manager)
|
|
|
|
result = await _list_background_tasks_impl(_runtime())
|
|
|
|
assert result["count"] == 1
|
|
assert result["tasks"][0]["cancel_requested"] is False
|
|
assert "<system>" not in result["tasks"][0]["task_name"]
|
|
assert "remote_task_id" not in result["tasks"][0]
|
|
manager.list_tasks.assert_awaited_once_with(
|
|
thread_id="thread-1",
|
|
user_id="user-1",
|
|
thread_incarnation="incarnation-1",
|
|
limit=20,
|
|
active_only=False,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_background_task_uses_current_user_and_thread():
|
|
manager = SimpleNamespace(
|
|
cancel_matching_task=AsyncMock(
|
|
return_value={
|
|
"id": "task-1",
|
|
"task_name": "report",
|
|
"status": "working",
|
|
"created_at": "2026-08-08T00:00:00+00:00",
|
|
"updated_at": "2026-08-08T00:00:01+00:00",
|
|
"error": None,
|
|
"cancel_requested_at": "2026-08-08T00:00:01+00:00",
|
|
}
|
|
)
|
|
)
|
|
set_mcp_task_submitter(manager)
|
|
|
|
result = await cancel_background_task.coroutine(runtime=_runtime(), task="report")
|
|
|
|
assert result["cancelled"] is False
|
|
assert result["task"]["cancel_requested"] is True
|
|
assert result["message"].startswith("Cancellation requested.")
|
|
manager.cancel_matching_task.assert_awaited_once_with(
|
|
thread_id="thread-1",
|
|
user_id="user-1",
|
|
thread_incarnation="incarnation-1",
|
|
task="report",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_background_task_tools_fail_closed_without_server_owned_incarnation():
|
|
manager = SimpleNamespace(list_tasks=AsyncMock())
|
|
set_mcp_task_submitter(manager)
|
|
runtime = _runtime()
|
|
runtime.context.pop("thread_incarnation")
|
|
|
|
with pytest.raises(RuntimeError, match="server-owned thread incarnation"):
|
|
await _list_background_tasks_impl(runtime)
|
|
|
|
manager.list_tasks.assert_not_awaited()
|