mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* fix(lark): retry the Windows install lock instead of failing after ~10s msvcrt.LK_LOCK gives up after roughly ten seconds and surfaces contention as Permission denied, while a real install (download + setup of the lark/sandbox CLI) can hold the lock far longer. Retry the non-blocking acquisition on the Windows fallback branch so concurrent Gateway workers serialize instead of failing, mirroring the extensions manager's lock introduced in #6020. The POSIX flock branch is unchanged. Closes #6879 * test(lark): inject msvcrt stub with raising=False for Linux runners The regression test patches the module-level ``msvcrt`` attribute, which only exists on Windows (the POSIX branch imports ``fcntl`` instead), so ``monkeypatch.setattr`` with the default ``raising=True`` raised AttributeError on Linux CI (backend-unit-tests shard 4). Pass ``raising=False`` so the stub is injected on POSIX runners: ``fcntl`` is patched to ``None`` first, and ``_exclusive_install_lock`` then takes the Windows fallback branch against the fake. Windows behavior is unchanged. Scope: backend/tests/test_lark_cli_install_lock.py, 1 line. Verification: pytest tests/test_lark_cli_install_lock.py on Windows (Python 3.12, pytest 9.0.3) -- 3 passed incl. the real-contention case; Linux CI rerun is the authoritative check. * fix(lark): harden and share advisory lock retries --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
123 lines
4.4 KiB
Python
123 lines
4.4 KiB
Python
"""MCP resource links must reach the persistent artifact registry through the graph."""
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from _agent_e2e_helpers import build_single_tool_call_model
|
|
from langchain.agents import create_agent
|
|
from langchain_core.messages import ToolMessage
|
|
from langchain_mcp_adapters.tools import load_mcp_tools
|
|
from mcp.types import CallToolResult, ListToolsResult, ResourceLink, Tool
|
|
|
|
from deerflow.agents.middlewares.artifact_capture_middleware import ArtifactCaptureMiddleware
|
|
from deerflow.agents.thread_state import ThreadState
|
|
from deerflow.config.extensions_config import ExtensionsConfig
|
|
from deerflow.config.paths import Paths
|
|
from deerflow.mcp.session_pool import MCPSessionPool
|
|
from deerflow.mcp.tools import _RawServerDiscovery, _wrap_discovered_server_tools
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def session_pool():
|
|
pool = MCPSessionPool()
|
|
try:
|
|
yield pool
|
|
finally:
|
|
await pool.close_all()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"transport,mime,suffix",
|
|
[
|
|
("stdio", "application/pdf", "pdf"),
|
|
("stdio", "image/png", "png"),
|
|
("streamable_http", "image/png", "png"),
|
|
],
|
|
)
|
|
async def test_resource_link_captured_by_agent_graph(transport, mime, suffix, monkeypatch, tmp_path, session_pool):
|
|
url = f"https://example.com/report.{suffix}"
|
|
calls = []
|
|
|
|
class Session:
|
|
async def initialize(self):
|
|
pass
|
|
|
|
async def list_tools(self, **kwargs):
|
|
return ListToolsResult(
|
|
tools=[
|
|
Tool(
|
|
name="export",
|
|
description="Export a report",
|
|
inputSchema={"type": "object", "properties": {}},
|
|
)
|
|
]
|
|
)
|
|
|
|
async def call_tool(self, name, arguments, **kwargs):
|
|
calls.append((name, arguments))
|
|
return CallToolResult(
|
|
content=[
|
|
ResourceLink(
|
|
type="resource_link",
|
|
name="report",
|
|
uri=url,
|
|
mimeType=mime,
|
|
)
|
|
]
|
|
)
|
|
|
|
@asynccontextmanager
|
|
async def fake_session(*args, **kwargs):
|
|
yield Session()
|
|
|
|
# Replace only the external MCP session and the filesystem base directory.
|
|
# Discovery, pooling/call protection, tool execution, and capture are real.
|
|
monkeypatch.setattr("langchain_mcp_adapters.tools.create_session", fake_session)
|
|
monkeypatch.setattr("langchain_mcp_adapters.sessions.create_session", fake_session)
|
|
monkeypatch.setattr("deerflow.mcp.tools.get_paths", lambda: Paths(tmp_path))
|
|
connection = {"transport": "stdio", "command": "offline-mcp-fixture"} if transport == "stdio" else {"transport": "streamable_http", "url": "https://example.com/mcp"}
|
|
discovered = await load_mcp_tools(
|
|
None,
|
|
connection=connection,
|
|
server_name="reports",
|
|
tool_name_prefix=True,
|
|
)
|
|
wrapped = _wrap_discovered_server_tools(
|
|
ExtensionsConfig(),
|
|
"reports",
|
|
connection,
|
|
_RawServerDiscovery(discovered, []),
|
|
tool_name_prefix=True,
|
|
session_init_timeout=None,
|
|
pool=session_pool,
|
|
binding=None,
|
|
ownership_domain="deployment",
|
|
connection_scope="deployment",
|
|
)
|
|
model = build_single_tool_call_model(tool_name="reports_export", tool_args={}, tool_call_id="export-1")
|
|
graph = create_agent(
|
|
model=model,
|
|
tools=list(wrapped.tools),
|
|
middleware=[ArtifactCaptureMiddleware()],
|
|
state_schema=ThreadState,
|
|
context_schema=dict,
|
|
)
|
|
state = await graph.ainvoke(
|
|
{"messages": [("user", "Export a report")]},
|
|
context={"thread_id": "report-thread", "thread_incarnation": "fixture-incarnation"},
|
|
)
|
|
(message,) = [m for m in state["messages"] if isinstance(m, ToolMessage)]
|
|
assert calls == [("export", {})]
|
|
assert message.status == "success"
|
|
if mime.startswith("image/"):
|
|
assert message.content[0]["type"] == "image"
|
|
assert message.content[0]["url"] == url
|
|
entries = state.get("tool_artifacts", [])
|
|
(entry,) = entries
|
|
assert entry["real_ref"] == url
|
|
assert entry["artifact_type"] == ("image" if mime.startswith("image/") else "file")
|
|
assert entry["mime_type"] == mime
|
|
assert entry["tool_name"] == "reports_export"
|