deer-flow/backend/tests/test_infoquest_http_timeout.py
yc2bgr8 3d921afa5f
feat(community): migrate infoquest client to httpx.AsyncClient (#5782)
* feat(community): migrate infoquest client to httpx.AsyncClient

Replace blocking sync requests.post calls with httpx.AsyncClient in the
InfoQuest community tool, as tracked in backend/docs/TODO.md planned
features. fetch/web_search/image_search and their raw-results helpers are
now async, keeping the 30s transport timeout and external behavior
(return values, error strings) unchanged.

The langchain tool wrappers in infoquest/tools.py become async functions;
web_fetch offloads readability extraction to a thread (same pattern as
the already-migrated jina_ai tool). Tests are updated to mock
httpx.AsyncClient instead of requests.post.

* test(community): use AsyncMock for infoquest fetch in relative-links test

The infoquest web_fetch_tool now awaits client.fetch, so the test stub
must return an awaitable instead of a sync lambda.

* fix(community): follow InfoQuest API redirects like the sync client did

Address the review note on #5782: requests.post follows redirects by
default while httpx.AsyncClient defaults to follow_redirects=False, so
a 3xx answer from the reader/search endpoints would have surfaced as
'Error: fetch API returned status 30x' (or fallen into the generic
error path after raise_for_status) instead of the final content.

All three AsyncClient call sites now pass follow_redirects=True. Adds
regression tests asserting each client is constructed with
follow_redirects=True, and documents the parity in the test contract.

* test(community): fix InfoQuest mock endpoint typo
2026-09-25 20:58:17 +08:00

66 lines
3.0 KiB
Python

"""InfoQuest's remote crawl timeout must not leave local HTTP waits unbounded."""
import json
from unittest.mock import AsyncMock, MagicMock
import httpx
import pytest
from deerflow.community.infoquest.infoquest_client import InfoQuestClient
_SEARCH_PAYLOAD = {"search_result": {"results": []}}
def _mock_async_client_cls() -> MagicMock:
client = MagicMock()
client.__aenter__.return_value = client
client.__aexit__.return_value = None
client.post = AsyncMock()
cls = MagicMock()
cls.return_value = client
return cls
def _install_response(monkeypatch, response: httpx.Response):
mock_async_client_cls = _mock_async_client_cls()
mock_async_client_cls.return_value.post.return_value = response
monkeypatch.setattr("deerflow.community.infoquest.infoquest_client.httpx.AsyncClient", mock_async_client_cls)
monkeypatch.setenv("INFOQUEST_API_KEY", "test-placeholder")
return mock_async_client_cls
@pytest.mark.anyio
@pytest.mark.parametrize("operation", ["fetch", "web_search", "image_search"])
async def test_infoquest_sets_transport_timeout(monkeypatch, operation):
post_url = "https://reader.infoquest.bytepluses.com" if operation == "fetch" else "https://search.infoquest.bytepluses.com"
if operation == "fetch":
response = httpx.Response(200, text=json.dumps({"reader_result": "<p>Content</p>"}), request=httpx.Request("POST", post_url))
else:
response = httpx.Response(200, json=_SEARCH_PAYLOAD, request=httpx.Request("POST", post_url))
mock_async_client_cls = _install_response(monkeypatch, response)
client = InfoQuestClient(fetch_timeout=10, fetch_navigation_timeout=20)
result = await getattr(client, operation)("https://example.com" if operation == "fetch" else "query")
assert result == ("<p>Content</p>" if operation == "fetch" else "[]")
mock_async_client_cls.return_value.post.assert_awaited_once()
assert mock_async_client_cls.return_value.post.call_args.kwargs["timeout"] == 30
if operation == "fetch":
assert mock_async_client_cls.return_value.post.call_args.kwargs["json"]["timeout"] == 10
assert mock_async_client_cls.return_value.post.call_args.kwargs["json"]["navi_timeout"] == 20
@pytest.mark.anyio
@pytest.mark.parametrize("operation", ["fetch", "web_search", "image_search"])
@pytest.mark.parametrize("error_type", [httpx.ConnectTimeout, httpx.ReadTimeout])
async def test_infoquest_transport_timeout_returns_existing_error(monkeypatch, operation, error_type):
mock_async_client_cls = _mock_async_client_cls()
mock_async_client_cls.return_value.post.side_effect = error_type("synthetic timeout")
monkeypatch.setattr("deerflow.community.infoquest.infoquest_client.httpx.AsyncClient", mock_async_client_cls)
monkeypatch.setenv("INFOQUEST_API_KEY", "test-placeholder")
result = await getattr(InfoQuestClient(), operation)("https://example.com" if operation == "fetch" else "query")
assert result.startswith("Error:")
assert "synthetic timeout" in result