From 5092a45943407624ed36e4a642ad881bdfc86f69 Mon Sep 17 00:00:00 2001 From: yetuge <182123929+yetuge@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:35:42 +0800 Subject: [PATCH] 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> --- .../harness/deerflow/community/brave/tools.py | 2 +- .../deerflow/community/groundroute/tools.py | 2 +- .../deerflow/community/searxng/tools.py | 2 +- .../deerflow/community/serper/tools.py | 2 +- .../deerflow/community/serply/tools.py | 2 +- .../harness/deerflow/community/sofya/tools.py | 4 ++-- backend/tests/test_brave_tools.py | 7 +++++++ backend/tests/test_groundroute_tools.py | 7 +++++++ backend/tests/test_searxng_client.py | 21 +++++++++++++++++++ backend/tests/test_serper_tools.py | 7 +++++++ backend/tests/test_serply_tools.py | 7 +++++++ backend/tests/test_sofya_tools.py | 12 +++++++++++ 12 files changed, 68 insertions(+), 7 deletions(-) diff --git a/backend/packages/harness/deerflow/community/brave/tools.py b/backend/packages/harness/deerflow/community/brave/tools.py index 31a2370470..947a7e076b 100644 --- a/backend/packages/harness/deerflow/community/brave/tools.py +++ b/backend/packages/harness/deerflow/community/brave/tools.py @@ -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, diff --git a/backend/packages/harness/deerflow/community/groundroute/tools.py b/backend/packages/harness/deerflow/community/groundroute/tools.py index d8be3fdd6e..d666e03eb3 100644 --- a/backend/packages/harness/deerflow/community/groundroute/tools.py +++ b/backend/packages/harness/deerflow/community/groundroute/tools.py @@ -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)) diff --git a/backend/packages/harness/deerflow/community/searxng/tools.py b/backend/packages/harness/deerflow/community/searxng/tools.py index 447f9726e3..9eb3e822b6 100644 --- a/backend/packages/harness/deerflow/community/searxng/tools.py +++ b/backend/packages/harness/deerflow/community/searxng/tools.py @@ -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, diff --git a/backend/packages/harness/deerflow/community/serper/tools.py b/backend/packages/harness/deerflow/community/serper/tools.py index 9f960981dd..8ca23ab957 100644 --- a/backend/packages/harness/deerflow/community/serper/tools.py +++ b/backend/packages/harness/deerflow/community/serper/tools.py @@ -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 diff --git a/backend/packages/harness/deerflow/community/serply/tools.py b/backend/packages/harness/deerflow/community/serply/tools.py index 7d476d9597..e0f65062a5 100644 --- a/backend/packages/harness/deerflow/community/serply/tools.py +++ b/backend/packages/harness/deerflow/community/serply/tools.py @@ -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: diff --git a/backend/packages/harness/deerflow/community/sofya/tools.py b/backend/packages/harness/deerflow/community/sofya/tools.py index 92ffcf06b8..b78ff64c7b 100644 --- a/backend/packages/harness/deerflow/community/sofya/tools.py +++ b/backend/packages/harness/deerflow/community/sofya/tools.py @@ -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 diff --git a/backend/tests/test_brave_tools.py b/backend/tests/test_brave_tools.py index 3d57a25b21..8d04f68c9e 100644 --- a/backend/tests/test_brave_tools.py +++ b/backend/tests/test_brave_tools.py @@ -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 diff --git a/backend/tests/test_groundroute_tools.py b/backend/tests/test_groundroute_tools.py index 71e01df261..95de840fae 100644 --- a/backend/tests/test_groundroute_tools.py +++ b/backend/tests/test_groundroute_tools.py @@ -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 diff --git a/backend/tests/test_searxng_client.py b/backend/tests/test_searxng_client.py index f3b8850270..e6b11ba32b 100644 --- a/backend/tests/test_searxng_client.py +++ b/backend/tests/test_searxng_client.py @@ -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 diff --git a/backend/tests/test_serper_tools.py b/backend/tests/test_serper_tools.py index f1bbddc9d4..199d680364 100644 --- a/backend/tests/test_serper_tools.py +++ b/backend/tests/test_serper_tools.py @@ -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 diff --git a/backend/tests/test_serply_tools.py b/backend/tests/test_serply_tools.py index 89fed878e7..557f92df31 100644 --- a/backend/tests/test_serply_tools.py +++ b/backend/tests/test_serply_tools.py @@ -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 diff --git a/backend/tests/test_sofya_tools.py b/backend/tests/test_sofya_tools.py index fa931da283..09d91157d4 100644 --- a/backend/tests/test_sofya_tools.py +++ b/backend/tests/test_sofya_tools.py @@ -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