mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* feat(mcp): add durable task runtime foundation * fix(chart): sync embedded config version * fix(mcp): isolate task polls during shutdown * feat(mcp): track consecutive poll errors on mcp_tasks poll_attempt_count grows on every claim (successful polls included), so it cannot drive a failure backoff without misjudging normal long tasks. Add consecutive_poll_error_count: incremented when a claim is released after a poll error, reset to zero by any applied snapshot. The backoff/terminal policy that consumes it lands with the first concrete driver. * fix(mcp): harden durable task lifecycle * feat(mcp): add ordinary durable task driver * test(mcp): address durable task review feedback * fix(mcp): preserve submit tool descriptions * fix(mcp): bound remote task calls * fix(mcp): bound persisted task payloads * fix(mcp): preserve task tool error details * fix(mcp): enforce durable task boundaries * test(mcp): cover task config snapshot lifecycle --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from deerflow.config.extensions_config import ExtensionsConfig
|
|
from deerflow.mcp.tasks.runtime import (
|
|
McpTaskConfigurationError,
|
|
set_mcp_task_config_snapshot,
|
|
validate_mcp_task_config_snapshot,
|
|
validate_mcp_task_runtime_configuration,
|
|
)
|
|
|
|
|
|
def _extensions() -> ExtensionsConfig:
|
|
return ExtensionsConfig.model_validate(
|
|
{
|
|
"mcpServers": {
|
|
"reports": {
|
|
"task_toolsets": [
|
|
{
|
|
"name": "reports",
|
|
"submit_tool": "submit_report",
|
|
"status_tool": "status_report",
|
|
"cancel_tool": "cancel_report",
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
def test_configured_task_toolsets_require_enabled_runtime() -> None:
|
|
with pytest.raises(McpTaskConfigurationError, match="mcp_tasks.enabled=true"):
|
|
validate_mcp_task_runtime_configuration(
|
|
mcp_tasks_config=SimpleNamespace(enabled=False),
|
|
extensions_config=_extensions(),
|
|
repository_available=True,
|
|
)
|
|
|
|
|
|
def test_configured_task_toolsets_require_sql_persistence() -> None:
|
|
with pytest.raises(McpTaskConfigurationError, match="database.backend"):
|
|
validate_mcp_task_runtime_configuration(
|
|
mcp_tasks_config=SimpleNamespace(enabled=True),
|
|
extensions_config=_extensions(),
|
|
repository_available=False,
|
|
)
|
|
|
|
|
|
def test_no_task_toolsets_leave_existing_mcp_runtime_unchanged() -> None:
|
|
validate_mcp_task_runtime_configuration(
|
|
mcp_tasks_config=SimpleNamespace(enabled=False),
|
|
extensions_config=ExtensionsConfig(),
|
|
repository_available=False,
|
|
)
|
|
|
|
|
|
def test_task_toolset_server_transport_is_validated_at_startup() -> None:
|
|
extensions = _extensions()
|
|
extensions.mcp_servers["reports"].command = None
|
|
|
|
with pytest.raises(McpTaskConfigurationError, match="requires 'command'"):
|
|
validate_mcp_task_runtime_configuration(
|
|
mcp_tasks_config=SimpleNamespace(enabled=True),
|
|
extensions_config=extensions,
|
|
repository_available=True,
|
|
)
|
|
|
|
|
|
def test_task_enabled_server_changes_require_gateway_restart() -> None:
|
|
startup = _extensions()
|
|
current = _extensions()
|
|
current.mcp_servers["reports"].env["TOKEN"] = "rotated"
|
|
set_mcp_task_config_snapshot(startup)
|
|
try:
|
|
with pytest.raises(McpTaskConfigurationError, match="reports.*restart"):
|
|
validate_mcp_task_config_snapshot(current)
|
|
finally:
|
|
set_mcp_task_config_snapshot(None)
|
|
|
|
|
|
def test_unrelated_extension_changes_do_not_invalidate_task_runtime_snapshot() -> None:
|
|
startup = _extensions()
|
|
current = ExtensionsConfig.model_validate(
|
|
{
|
|
**startup.model_dump(by_alias=True),
|
|
"skills": {"writer": {"enabled": False}},
|
|
"mcpServers": {
|
|
**startup.model_dump(by_alias=True)["mcpServers"],
|
|
"search": {"command": "search-mcp"},
|
|
},
|
|
}
|
|
)
|
|
current.mcp_servers["reports"].description = "Updated Agent-facing description"
|
|
set_mcp_task_config_snapshot(startup)
|
|
try:
|
|
validate_mcp_task_config_snapshot(current)
|
|
finally:
|
|
set_mcp_task_config_snapshot(None)
|
|
|
|
|
|
def test_disabled_task_server_changes_do_not_invalidate_task_runtime_snapshot() -> None:
|
|
startup = _extensions()
|
|
startup.mcp_servers["reports"].enabled = False
|
|
current = _extensions()
|
|
current.mcp_servers["reports"].enabled = False
|
|
current.mcp_servers["reports"].env["TOKEN"] = "rotated"
|
|
set_mcp_task_config_snapshot(startup)
|
|
try:
|
|
validate_mcp_task_config_snapshot(current)
|
|
finally:
|
|
set_mcp_task_config_snapshot(None)
|
|
|
|
|
|
def test_mcp_interceptor_changes_require_gateway_restart_for_task_tools() -> None:
|
|
startup = _extensions()
|
|
current = ExtensionsConfig.model_validate(
|
|
{
|
|
**startup.model_dump(by_alias=True),
|
|
"mcpInterceptors": ["example.interceptor:build"],
|
|
}
|
|
)
|
|
set_mcp_task_config_snapshot(startup)
|
|
try:
|
|
with pytest.raises(McpTaskConfigurationError, match="mcpInterceptors.*restart"):
|
|
validate_mcp_task_config_snapshot(current)
|
|
finally:
|
|
set_mcp_task_config_snapshot(None)
|
|
|
|
|
|
def test_mcp_interceptor_changes_remain_hot_reloadable_without_task_tools() -> None:
|
|
startup = ExtensionsConfig()
|
|
current = ExtensionsConfig.model_validate({"mcpInterceptors": ["example.interceptor:build"]})
|
|
set_mcp_task_config_snapshot(startup)
|
|
try:
|
|
validate_mcp_task_config_snapshot(current)
|
|
finally:
|
|
set_mcp_task_config_snapshot(None)
|