fix(tools): make the search coercion guards catch OverflowError (#5957)

* fix(tools): make the search coercion guards catch OverflowError

The community search tools coerce max_results (and Sofya's content
limit) from YAML config or tool parameters through guards that catch
only TypeError and ValueError. `int(float('inf'))` raises OverflowError
instead, so a YAML `.inf` (or a JSON `Infinity`) value still crashes
the coercion - SearXNG's search even fails wholesale with
'cannot convert float infinity to integer'.

Catch OverflowError alongside them so an infinite value falls back to
the default like every other invalid input. NaN is already covered by
ValueError.

Signed-off-by: yetuge <2219677952@qq.com>

* test(tools): move the GroundRoute inf test to module scope

The regression test was appended inside TestWebSearchTool, ending the
class early and turning its last two methods into uncollected nested
functions. Move it after TestWebFetchTool so pytest collects all 20
tests again.

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
This commit is contained in:
yetuge 2026-09-28 14:35:42 +08:00 committed by GitHub
parent 1e26f00664
commit 5092a45943
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 68 additions and 7 deletions

View File

@ -56,7 +56,7 @@ def _coerce_max_results(
) -> int:
try:
coerced = int(value)
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
logger.warning(
"Invalid Brave Search max_results=%r; using default %s",
value,

View File

@ -55,7 +55,7 @@ def _get_api_key(tool_name: str) -> str | None:
def _coerce_max_results(value: object, *, default: int = _DEFAULT_MAX_RESULTS) -> int:
try:
coerced = int(value)
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
logger.warning("Invalid GroundRoute max_results=%r; using default %s", value, default)
coerced = default
return max(1, min(coerced, _MAX_RESULTS_CAP))

View File

@ -46,7 +46,7 @@ async def web_search_tool(query: str, time_range: SearchTimeRange | None = None)
else:
try:
max_results = int(raw)
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
logger.warning(
"Invalid SearXNG max_results=%r; using default %s",
raw,

View File

@ -40,7 +40,7 @@ def _coerce_max_results(value: object, default: int = 5, max_allowed: int = _SER
"""Coerce config/parameter input into a bounded positive result count."""
try:
count = int(value)
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
return default
if count <= 0:
return default

View File

@ -58,7 +58,7 @@ def _coerce_max_results(
) -> int:
try:
coerced = int(value)
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
logger.warning("Invalid Serply max_results=%r; using default %s", value, default)
coerced = default
if coerced < 1:

View File

@ -46,7 +46,7 @@ def _coerce_max_results(value: object, default: int = 5, max_allowed: int = _SOF
"""Coerce config/parameter input into a bounded positive result count."""
try:
count = int(value) # type: ignore[call-overload]
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
return default
if count <= 0:
return default
@ -57,7 +57,7 @@ def _coerce_content_limit(value: object, default: int = _DEFAULT_CONTENTS_MAX_CH
"""Coerce the per-result content limit. 0 means no limit; anything invalid falls back to the default."""
try:
limit = int(value) # type: ignore[call-overload]
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
return default
return limit if limit >= 0 else default

View File

@ -731,3 +731,10 @@ def test_package_exports_image_search_tool():
from deerflow.community.brave.tools import image_search_tool as direct_image_search_tool
assert image_search_tool is direct_image_search_tool
def test_coerce_max_results_inf_falls_back_to_default():
"""A YAML `.inf` max_results must fall back to the default, not crash."""
import deerflow.community.brave.tools as brave_mod
assert brave_mod._coerce_max_results(float("inf")) == brave_mod._DEFAULT_MAX_RESULTS

View File

@ -328,3 +328,10 @@ class TestWebFetchTool:
patcher.stop()
assert result == "Error: No results found"
def test_coerce_max_results_inf_falls_back_to_default():
"""A YAML `.inf` max_results must fall back to the default, not crash."""
import deerflow.community.groundroute.tools as groundroute_mod
assert groundroute_mod._coerce_max_results(float("inf")) == groundroute_mod._DEFAULT_MAX_RESULTS

View File

@ -298,3 +298,24 @@ class TestSearxngTools:
await tools.web_search_tool.ainvoke({"query": "latest release", "time_range": "week"})
mock_client.search.assert_called_once_with("latest release", max_results=5, time_range="week")
@patch("deerflow.community.searxng.tools._get_searxng_client")
async def test_web_search_tool_inf_max_results_falls_back_to_default(self, mock_get_client):
"""A YAML `.inf` max_results must fall back to the default, not error out."""
mock_client = MagicMock()
mock_client.search = AsyncMock(
return_value=[
{"title": "Result 1", "url": "https://example.com/1", "content": "Desc 1"},
]
)
mock_get_client.return_value = mock_client
with patch(
"deerflow.community.searxng.tools._get_tool_config",
return_value={"max_results": float("inf")},
):
result = await tools.web_search_tool.ainvoke("test query")
data = json.loads(result)
assert "error" not in data
assert mock_client.search.call_args.kwargs["max_results"] == 5

View File

@ -1212,3 +1212,10 @@ def test_package_exports_image_search_tool():
from deerflow.community.serper.tools import image_search_tool as direct_image_search_tool
assert image_search_tool is direct_image_search_tool
def test_coerce_max_results_inf_falls_back_to_default():
"""A YAML `.inf` max_results must fall back to the default, not crash."""
import deerflow.community.serper.tools as serper_mod
assert serper_mod._coerce_max_results(float("inf")) == 5

View File

@ -316,3 +316,10 @@ class TestWebSearchTool:
assert result["total_results"] == 1
assert mock_get.call_args.kwargs["headers"]["X-Api-Key"] == "env-key"
def test_coerce_max_results_inf_falls_back_to_default():
"""A YAML `.inf` max_results must fall back to the default, not crash."""
import deerflow.community.serply.tools as serply_mod
assert serply_mod._coerce_max_results(float("inf")) == serply_mod._DEFAULT_MAX_RESULTS

View File

@ -591,3 +591,15 @@ class TestWebFetchTool:
result = web_fetch_tool.invoke({"url": "https://example.com"})
assert result == "Error: SOFYA_API_KEY is not configured"
class TestCoerceInfFallsBackToDefault:
def test_coerce_max_results_inf_returns_default(self):
from deerflow.community.sofya.tools import _coerce_max_results
assert _coerce_max_results(float("inf")) == 5
def test_coerce_content_limit_inf_returns_default(self):
from deerflow.community.sofya.tools import _coerce_content_limit
assert _coerce_content_limit(float("inf")) == 2000