sync update from LAN repo (source dhk-1 @ 3b1ec5b4)
This commit is contained in:
1 parent
da6521b6cb
commit
264e2200d6
995 files changed
+148105
-13399
No files matched your search
@@ -0,0 +1,152 @@
|
||||
"""Actual async entry points must not block another request's MCP discovery.
|
||||
|
||||
Run each scenario in a child process: the regression blocks the event loop, so
|
||||
an asyncio timeout alone cannot fail the test or clean up the waiting threads.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entrypoint", ["open_chat", "open_chat_stream", "workflow"])
|
||||
def test_async_entrypoint_coexists_with_cold_mcp_discovery(entrypoint):
|
||||
backend_root = Path(__file__).resolve().parents[1]
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = os.pathsep.join(filter(None, (str(backend_root), env.get("PYTHONPATH"))))
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(Path(__file__).resolve()), entrypoint],
|
||||
cwd=backend_root,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=40,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "concurrent MCP initialization completed" in result.stdout
|
||||
|
||||
|
||||
async def _exercise_entrypoint(entrypoint: str) -> None:
|
||||
import httpx
|
||||
from fastapi import FastAPI
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
from app.gateway.routers import open_chat
|
||||
from app.gateway.workflow_agent_runner import WorkflowAgentRunner
|
||||
from deerflow.agents.lead_agent import agent as lead_agent
|
||||
from deerflow.config import app_config
|
||||
from deerflow.mcp import cache, tools
|
||||
from deerflow.runtime.runs.worker import build_agent_with_timing
|
||||
|
||||
owner_loop = asyncio.get_running_loop()
|
||||
discovery_started = asyncio.Event()
|
||||
release_discovery = asyncio.Event()
|
||||
built_thread = contextvars.ContextVar("entrypoint_built_thread", default=None)
|
||||
discovery_calls = 0
|
||||
|
||||
async def discover():
|
||||
nonlocal discovery_calls
|
||||
assert asyncio.get_running_loop() is owner_loop
|
||||
discovery_calls += 1
|
||||
discovery_started.set()
|
||||
await release_discovery.wait()
|
||||
return ["discovered tool"]
|
||||
|
||||
class Agent:
|
||||
def __init__(self, thread_id):
|
||||
self.thread_id = thread_id
|
||||
|
||||
async def ainvoke(self, state, *, config):
|
||||
assert built_thread.get() == self.thread_id
|
||||
return {"messages": [AIMessage(content="answer")]}
|
||||
|
||||
async def astream(self, state, *, config, stream_mode):
|
||||
assert built_thread.get() == self.thread_id
|
||||
yield "values", {"messages": [AIMessage(content="answer")]}
|
||||
|
||||
def factory(config):
|
||||
assert cache.get_cached_mcp_tools() == ["discovered tool"]
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
built_thread.set(thread_id)
|
||||
return Agent(thread_id)
|
||||
|
||||
factory._deerflow_background_build = True
|
||||
app = FastAPI()
|
||||
app.include_router(open_chat.router)
|
||||
|
||||
@app.get("/probe")
|
||||
async def probe():
|
||||
return {"ok": True}
|
||||
|
||||
with pytest.MonkeyPatch.context() as patch:
|
||||
patch.setattr(cache, "_mcp_tools_cache", None)
|
||||
patch.setattr(cache, "_cache_initialized", False)
|
||||
patch.setattr(cache, "_config_mtime", None)
|
||||
patch.setattr(cache, "_initialization_lock", asyncio.Lock())
|
||||
patch.setattr(cache, "_get_config_mtime", lambda: 1.0)
|
||||
patch.setattr(tools, "get_mcp_tools", discover)
|
||||
patch.setattr(open_chat, "make_lead_agent", factory)
|
||||
patch.setattr(lead_agent, "make_lead_agent", factory)
|
||||
patch.setattr(
|
||||
app_config,
|
||||
"get_app_config",
|
||||
lambda: SimpleNamespace(workflows=SimpleNamespace(agent_recursion_limit=100)),
|
||||
)
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
|
||||
# This is the same background construction path used by run_agent.
|
||||
first = asyncio.create_task(
|
||||
build_agent_with_timing(factory, {"configurable": {"thread_id": "normal-chat"}}, None, "normal-chat")
|
||||
)
|
||||
second = None
|
||||
try:
|
||||
await asyncio.wait_for(discovery_started.wait(), timeout=5)
|
||||
if entrypoint == "workflow":
|
||||
second = asyncio.create_task(
|
||||
WorkflowAgentRunner(app).run_agent(
|
||||
run_id="test-run", node_id="test-node", owner_id="test-owner", agent_id="test-agent", prompt="hello"
|
||||
)
|
||||
)
|
||||
else:
|
||||
path = "/api/open/chat/stream" if entrypoint == "open_chat_stream" else "/api/open/chat"
|
||||
second = asyncio.create_task(client.post(path, json={"message": "hello", "include_files": False}))
|
||||
|
||||
# Let the real HTTP handler / workflow runner enter construction
|
||||
# while discovery holds its lock and is awaiting network work.
|
||||
await asyncio.sleep(0.05)
|
||||
assert not first.done() and not second.done()
|
||||
for _ in range(5):
|
||||
response = await asyncio.wait_for(client.get("/probe"), timeout=1)
|
||||
assert response.json() == {"ok": True}
|
||||
await asyncio.sleep(0)
|
||||
finally:
|
||||
release_discovery.set()
|
||||
results = await asyncio.wait_for(asyncio.gather(first, *([second] if second else [])), timeout=5)
|
||||
|
||||
assert discovery_calls == 1
|
||||
assert built_thread.get() is None # Other request contexts remain isolated.
|
||||
result = results[1]
|
||||
if entrypoint == "workflow":
|
||||
assert result["text"] == "answer"
|
||||
assert result["thread_id"] == "wf-test-run-test-node"
|
||||
elif entrypoint == "open_chat_stream":
|
||||
assert result.status_code == 200
|
||||
assert "event: result" in result.text
|
||||
assert '"answer": "answer"' in result.text
|
||||
assert "event: error" not in result.text
|
||||
else:
|
||||
assert result.status_code == 200
|
||||
assert result.json()["answer"] == "answer"
|
||||
print("concurrent MCP initialization completed", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_exercise_entrypoint(sys.argv[1]))
|
||||
Reference in new issue
Block a user