from contextlib import contextmanager
from unittest.mock import patch
import pytest
def test_harness_web_search_wrapper_fetches_urls_from_web_tools():
"""Paid wrapper should call web_tools search and fetch page content for URL-only results."""
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper import (
BochaSearchAPIWrapper,
)
wrapper = BochaSearchAPIWrapper(
search_api_key=bytearray(b"bocha-key"),
search_url="",
max_web_search_results=2,
)
with patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"WebPaidSearchTool._bocha_search_sync",
return_value={
"provider": "bocha",
"answer": "fallback answer",
"urls": ["https://example.com/news"],
},
) as mock_search, patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"WebFetchWebpageAdapter.fetch_webpage_sync",
return_value={
"url": "https://example.com/news",
"status_code": 200,
"title": "Fetched title",
"content": "Fetched body",
},
) as mock_fetch:
result = wrapper.results("test query")
assert result == [
{
"title": "Fetched title",
"url": "https://example.com/news",
"content": "Fetched body",
"source": "bocha",
}
]
mock_search.assert_called_once_with(query="test query", max_results=2, timeout_seconds=60)
mock_fetch.assert_called_once_with("https://example.com/news", 60)
def test_harness_web_search_wrapper_truncates_prefetched_content():
"""Paid wrapper should keep long fetched content bounded before returning results."""
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper import (
BochaSearchAPIWrapper,
)
from openjiuwen_deepsearch.common.common_constants import MAX_COLLECTOR_DOC_CONTENT_LENGTH
wrapper = BochaSearchAPIWrapper(
search_api_key=bytearray(b"bocha-key"),
search_url="",
max_web_search_results=1,
)
long_content = "A" * (MAX_COLLECTOR_DOC_CONTENT_LENGTH + 200)
with patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"WebPaidSearchTool._bocha_search_sync",
return_value={
"provider": "bocha",
"answer": "fallback answer",
"urls": ["https://example.com/long"],
},
), patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"WebFetchWebpageAdapter.fetch_webpage_sync",
return_value={
"url": "https://example.com/long",
"status_code": 200,
"title": "Long title",
"content": long_content,
},
):
result = wrapper.results("test query")
assert len(result) == 1
assert result[0]["url"] == "https://example.com/long"
assert len(result[0]["content"]) == MAX_COLLECTOR_DOC_CONTENT_LENGTH
assert result[0]["content"].endswith("...")
@pytest.mark.asyncio
async def test_harness_web_search_wrapper_async_delegates_to_sync_results():
"""Paid wrapper should expose the same async interface as existing search wrappers."""
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper import (
PerplexitySearchAPIWrapper,
)
wrapper = PerplexitySearchAPIWrapper(
search_api_key=bytearray(b"pplx-key"),
search_url="",
max_web_search_results=1,
)
with patch.object(
wrapper,
"results",
return_value=[{"title": "A", "url": "https://example.com", "content": "B"}],
) as mock_results:
result = await wrapper.aresults("async query")
assert result == [{"title": "A", "url": "https://example.com", "content": "B"}]
mock_results.assert_called_once_with("async query")
def test_web_search_mapping_uses_harness_adapter_for_bocha_and_perplexity():
"""Research mapping should keep Bocha/Perplexity on the harness web_tools adapter."""
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper import (
BochaSearchAPIWrapper,
PerplexitySearchAPIWrapper,
)
from openjiuwen_deepsearch.framework.openjiuwen.tools.web_search import search_engine_mapping
assert search_engine_mapping["bocha"] is BochaSearchAPIWrapper
assert search_engine_mapping["perplexity"] is PerplexitySearchAPIWrapper
def test_harness_web_search_omits_url_env_when_search_url_empty():
"""Empty search_url should let web_tools use its own defaults."""
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper import (
BochaSearchAPIWrapper,
)
captured_env = []
@contextmanager
def capture_env(values):
captured_env.append(values)
yield
wrapper = BochaSearchAPIWrapper(
search_api_key=bytearray(b"bocha-key"),
search_url="",
max_web_search_results=1,
)
with patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"_temporary_env",
side_effect=lambda values: capture_env(values),
), patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"WebPaidSearchTool._bocha_search_sync",
return_value={"provider": "bocha", "answer": "", "urls": []},
):
wrapper.results("query")
assert captured_env == [{"BOCHA_API_KEY": "bocha-key"}]
def test_harness_web_search_injects_custom_url_only_when_web_tools_can_override():
"""Configured search_url should be injected only for web_tools-overridable providers."""
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper import (
BochaSearchAPIWrapper,
)
captured_env = []
@contextmanager
def capture_env(values):
captured_env.append(values)
yield
wrapper = BochaSearchAPIWrapper(
search_api_key=bytearray(b"bocha-key"),
search_url="https://custom-bocha.example.com/search",
max_web_search_results=1,
)
with patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"_temporary_env",
side_effect=lambda values: capture_env(values),
), patch(
"openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.harness_web_search.api_wrapper."
"WebPaidSearchTool._bocha_search_sync",
return_value={"provider": "bocha", "answer": "", "urls": []},
):
wrapper.results("query")
assert captured_env == [
{
"BOCHA_API_KEY": "bocha-key",
"BOCHA_API_URL": "https://custom-bocha.example.com/search",
}
]
def test_web_search_engine_config_accepts_all_web_search_engines():
"""Agent config should accept all registered web search engine names directly."""
from openjiuwen_deepsearch.config.config import WebSearchEngineConfig
for engine in ("bocha", "jina", "perplexity", "serper"):
config = WebSearchEngineConfig(search_engine_name=engine)
assert config.search_engine_name == engine