Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions tests/tools/test_web_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def _clear_web_creds(self, monkeypatch):
monkeypatch.delenv(k, raising=False)

def test_unconfigured_search_emits_top_level_error(self, monkeypatch):
"""``web_search_tool`` with no creds returns ``{"error": "Error searching web: ..."}``
"""``web_search_tool`` with no creds returns ``{"error": "..."}``
β€” matching main's ``tool_error()`` envelope, not a per-result shape.
"""
from tools import web_tools
Expand All @@ -305,9 +305,10 @@ def test_unconfigured_search_emits_top_level_error(self, monkeypatch):

result = json.loads(web_tools.web_search_tool("hello world", limit=3))
assert "error" in result, f"expected top-level 'error' key, got {result}"
# ``Error searching web:`` prefix comes from web_tools' top-level except handler
assert "Error searching web:" in result["error"]
assert "FIRECRAWL_API_KEY" in result["error"]
# Runtime availability guard returns a clear error when the configured
# backend is unavailable (issue #42011).
assert "not available" in result["error"]
assert "hermes tools" in result["error"]
# No per-result burying
assert "results" not in result

Expand Down
56 changes: 56 additions & 0 deletions tests/tools/test_web_tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ def test_web_search_clamps_limit_before_backend_call(self):
fake_provider.name = "parallel"

with patch("tools.web_tools._get_search_backend", return_value="parallel"), \
patch("tools.web_tools._is_backend_available", return_value=True), \
patch("agent.web_search_registry.get_provider", return_value=fake_provider), \
patch("tools.interrupt.is_interrupted", return_value=False), \
patch.object(tools.web_tools._debug, "log_call"), \
Expand Down Expand Up @@ -527,6 +528,7 @@ def test_search_error_response_does_not_expose_diagnostics(self):
fake_provider.name = "firecrawl"

with patch("tools.web_tools._get_search_backend", return_value="firecrawl"), \
patch("tools.web_tools._is_backend_available", return_value=True), \
patch("agent.web_search_registry.get_provider", return_value=fake_provider), \
patch("tools.interrupt.is_interrupted", return_value=False), \
patch.object(tools.web_tools._debug, "log_call") as mock_log_call, \
Expand All @@ -544,6 +546,60 @@ def test_search_error_response_does_not_expose_diagnostics(self):
assert "exception_chain" not in result
assert "traceback" not in result

def test_web_search_returns_clear_error_when_backend_unavailable(self):
"""When the configured backend is unavailable at invocation time,
web_search_tool returns a structured error instead of silently
falling through (issue #42011)."""
import tools.web_tools

with patch("tools.web_tools._get_search_backend", return_value="ddgs"), \
patch("tools.web_tools._is_backend_available", return_value=False), \
patch("tools.interrupt.is_interrupted", return_value=False), \
patch.object(tools.web_tools._debug, "log_call"), \
patch.object(tools.web_tools._debug, "save"):
result = json.loads(tools.web_tools.web_search_tool("test", limit=5))

assert result["success"] is False
assert "ddgs" in result["error"]
assert "pip install ddgs" in result["error"]

def test_web_search_returns_clear_error_for_unavailable_api_backend(self):
"""For non-ddgs backends, the error mentions API keys and hermes tools."""
import tools.web_tools

with patch("tools.web_tools._get_search_backend", return_value="tavily"), \
patch("tools.web_tools._is_backend_available", return_value=False), \
patch("tools.interrupt.is_interrupted", return_value=False), \
patch.object(tools.web_tools._debug, "log_call"), \
patch.object(tools.web_tools._debug, "save"):
result = json.loads(tools.web_tools.web_search_tool("test", limit=5))

assert result["success"] is False
assert "tavily" in result["error"]
assert "hermes tools" in result["error"]

def test_web_search_skips_availability_check_when_no_backend_configured(self):
"""When _get_search_backend returns empty, the guard must not trigger."""
import tools.web_tools

fake_provider = MagicMock(
supports_search=MagicMock(return_value=True),
)
fake_provider.search.return_value = {"success": True, "data": {"web": []}}
fake_provider.name = "brave-free"

with patch("tools.web_tools._get_search_backend", return_value=""), \
patch("tools.web_tools._is_backend_available", return_value=False), \
patch("agent.web_search_registry.get_provider", return_value=None), \
patch("agent.web_search_registry.get_active_search_provider",
return_value=fake_provider), \
patch("tools.interrupt.is_interrupted", return_value=False), \
patch.object(tools.web_tools._debug, "log_call"), \
patch.object(tools.web_tools._debug, "save"):
result = json.loads(tools.web_tools.web_search_tool("test", limit=5))

assert result["success"] is True


class TestCheckWebApiKey:
"""Test suite for check_web_api_key() unified availability check."""
Expand Down
26 changes: 26 additions & 0 deletions tools/web_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,32 @@ def web_search_tool(query: str, limit: int = 5) -> str:
)

backend = _get_search_backend()

# Runtime availability guard: when the configured backend is
# explicitly set but its package / credentials are missing, fail
# early with an actionable error instead of silently falling
# through. check_fn filters the tool from the LLM's schema list
# at get_definitions() time, but the handler can still be reached
# via the tool_call bridge or a stale context β€” issue #42011.
if backend and not _is_backend_available(backend):
return json.dumps(
{
"success": False,
"error": (
f"Error: configured web search backend '{backend}' "
"is not available. "
+ (
"Install the package with: pip install ddgs"
if backend == "ddgs"
else "Check that the required API key or "
"environment variable is set. "
"Run `hermes tools` to reconfigure."
)
),
},
ensure_ascii=False,
)

provider = _wsp_get_provider(backend) if backend else None
if provider is None or not provider.supports_search():
# Fall back to availability-walked active provider when the
Expand Down
Loading