mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 13:40:37 +00:00
* fix(acp): fail fast when an ACP agent exits and drain its stderr An ACP agent that exited before answering (missing credentials, a wrong argument) left invoke_acp_agent waiting for the whole timeout_seconds budget (30 minutes by default) and then suggested raising the timeout. The pinned SDK never fails pending requests when the agent's stdout reaches EOF, so nothing noticed the process was gone. The SDK also opens the agent's stderr as a pipe and never reads it. An agent that logged more than asyncio's pipe buffering absorbs blocked on its next stderr write and stalled the invocation the same way. Race the ACP exchange against the agent process: when it exits (or a newer SDK reports the connection closed), allow a short grace for responses still buffered in the pipe, then report the exit code. Drain stderr continuously into a bounded tail that is logged on failure and kept out of model-visible text. * fix(acp): log the full stderr tail on timeout and pin tail retention Log the timeout tail after the SDK context has terminated and reaped the agent, so lines still in flight when it stalled are included. Make the exit test write more stderr than the retained tail, so keeping the start of the stream instead of its end fails the assertion on the fatal line.
84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
"""Regression test for ACP invocation setup on the event loop.
|
|
|
|
The configured command is a bare name backed by a launcher shim on a controlled
|
|
``PATH``, so the gate really runs the ``shutil.which`` lookup (``os.access``)
|
|
instead of short-circuiting on a name that resolves nowhere.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import acp
|
|
import pytest
|
|
|
|
from deerflow.config.acp_config import ACPAgentConfig
|
|
from deerflow.tools.builtins import invoke_acp_agent_tool as acp_tool
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
async def test_invoke_acp_agent_setup_does_not_block_event_loop(monkeypatch, tmp_path) -> None:
|
|
from deerflow.config import paths as paths_module
|
|
|
|
configured_paths = SimpleNamespace(
|
|
base_dir=tmp_path,
|
|
acp_workspace_dir=lambda thread_id, user_id=None: tmp_path / "threads" / thread_id / "acp-workspace",
|
|
)
|
|
monkeypatch.setattr(paths_module, "get_paths", lambda: configured_paths)
|
|
config_path = tmp_path / "extensions_config.json"
|
|
await asyncio.to_thread(
|
|
config_path.write_text,
|
|
'{"mcpServers": {"test-server": {"enabled": true, "type": "http", "url": "https://example.test/mcp"}}, "skills": {}}',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_path))
|
|
|
|
shim_dir = tmp_path / "bin"
|
|
shim_dir.mkdir()
|
|
shim = shim_dir / ("test-agent.cmd" if sys.platform == "win32" else "test-agent")
|
|
shim.write_text("@echo off\n" if sys.platform == "win32" else "#!/bin/sh\n", encoding="utf-8")
|
|
if sys.platform != "win32":
|
|
shim.chmod(0o755)
|
|
monkeypatch.setenv("PATH", str(shim_dir))
|
|
|
|
captured: dict[str, Any] = {}
|
|
|
|
class _Connection:
|
|
async def initialize(self, **kwargs: Any) -> None:
|
|
captured["initialize"] = kwargs
|
|
|
|
async def new_session(self, **kwargs: Any) -> SimpleNamespace:
|
|
captured["new_session"] = kwargs
|
|
return SimpleNamespace(session_id="session-1")
|
|
|
|
async def prompt(self, **kwargs: Any) -> None:
|
|
captured["prompt"] = kwargs
|
|
|
|
@contextlib.asynccontextmanager
|
|
async def fake_spawn_agent_process(client, command, *args, env=None, cwd=None):
|
|
captured["cwd"] = cwd
|
|
# An agent process that stays alive for the whole exchange.
|
|
yield _Connection(), SimpleNamespace(stderr=None, wait=asyncio.Event().wait)
|
|
|
|
monkeypatch.setattr(acp, "spawn_agent_process", fake_spawn_agent_process)
|
|
|
|
tool = acp_tool.build_invoke_acp_agent_tool(
|
|
{"test-agent": ACPAgentConfig(command="test-agent", description="Test agent")},
|
|
)
|
|
result = await tool.coroutine(
|
|
agent="test-agent",
|
|
prompt="run",
|
|
config={"configurable": {"thread_id": "thread-1"}},
|
|
)
|
|
|
|
expected_cwd = tmp_path / "threads" / "thread-1" / "acp-workspace"
|
|
assert result == "(no response)"
|
|
assert captured["cwd"] == str(expected_cwd)
|
|
assert captured["new_session"]["mcp_servers"] == [{"name": "test-server", "type": "http", "url": "https://example.test/mcp", "headers": []}]
|
|
assert expected_cwd.is_dir()
|