mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* 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>
606 lines
27 KiB
Python
606 lines
27 KiB
Python
"""Unit tests for the Sofya community web search and fetch tools."""
|
|
|
|
import json
|
|
import logging
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_api_key_warned():
|
|
"""Reset the module-level warning flag before each test."""
|
|
import deerflow.community.sofya.tools as sofya_mod
|
|
|
|
sofya_mod._api_key_warned = set()
|
|
yield
|
|
sofya_mod._api_key_warned = set()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config_with_key():
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
tool_config = MagicMock()
|
|
tool_config.model_extra = {"api_key": "test-sofya-key", "max_results": 5}
|
|
mock.return_value.get_tool_config.return_value = tool_config
|
|
yield mock
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config_no_key():
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
tool_config = MagicMock()
|
|
tool_config.model_extra = {}
|
|
mock.return_value.get_tool_config.return_value = tool_config
|
|
yield mock
|
|
|
|
|
|
def _make_response(payload: object) -> MagicMock:
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = payload
|
|
mock_resp.raise_for_status = MagicMock()
|
|
return mock_resp
|
|
|
|
|
|
def _make_search_response(results: list) -> MagicMock:
|
|
return _make_response({"query": "test", "results": results})
|
|
|
|
|
|
def _make_fetch_response(results: list) -> MagicMock:
|
|
return _make_response({"results": results})
|
|
|
|
|
|
class TestGetApiKey:
|
|
def test_returns_config_key_when_present(self):
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
tool_config = MagicMock()
|
|
tool_config.model_extra = {"api_key": "from-config"}
|
|
mock.return_value.get_tool_config.return_value = tool_config
|
|
|
|
from deerflow.community.sofya.tools import _get_api_key
|
|
|
|
assert _get_api_key("web_search") == "from-config"
|
|
|
|
def test_falls_back_to_env_when_config_key_whitespace(self):
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
tool_config = MagicMock()
|
|
tool_config.model_extra = {"api_key": " "}
|
|
mock.return_value.get_tool_config.return_value = tool_config
|
|
with patch.dict("os.environ", {"SOFYA_API_KEY": "env-key"}):
|
|
from deerflow.community.sofya.tools import _get_api_key
|
|
|
|
assert _get_api_key("web_search") == "env-key"
|
|
|
|
def test_uses_env_when_tool_is_not_configured(self):
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
mock.return_value.get_tool_config.return_value = None
|
|
with patch.dict("os.environ", {"SOFYA_API_KEY": "env-only"}):
|
|
from deerflow.community.sofya.tools import _get_api_key
|
|
|
|
assert _get_api_key("web_fetch") == "env-only"
|
|
|
|
def test_returns_none_when_no_key_anywhere(self):
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
mock.return_value.get_tool_config.return_value = None
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
from deerflow.community.sofya.tools import _get_api_key
|
|
|
|
assert _get_api_key("web_search") is None
|
|
|
|
def test_returns_none_when_env_key_whitespace(self):
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
mock.return_value.get_tool_config.return_value = None
|
|
with patch.dict("os.environ", {"SOFYA_API_KEY": " "}):
|
|
from deerflow.community.sofya.tools import _get_api_key
|
|
|
|
assert _get_api_key("web_search") is None
|
|
|
|
def test_reads_config_for_requested_tool_name(self):
|
|
with patch("deerflow.community.sofya.tools.get_app_config") as mock:
|
|
tool_config = MagicMock()
|
|
tool_config.model_extra = {"api_key": "fetch-key"}
|
|
mock.return_value.get_tool_config.return_value = tool_config
|
|
|
|
from deerflow.community.sofya.tools import _get_api_key
|
|
|
|
assert _get_api_key("web_fetch") == "fetch-key"
|
|
mock.return_value.get_tool_config.assert_called_with("web_fetch")
|
|
|
|
|
|
class TestCoerceMaxResults:
|
|
def test_returns_value_when_valid_positive_int(self):
|
|
from deerflow.community.sofya.tools import _coerce_max_results
|
|
|
|
assert _coerce_max_results(3) == 3
|
|
|
|
def test_returns_value_for_numeric_string(self):
|
|
from deerflow.community.sofya.tools import _coerce_max_results
|
|
|
|
assert _coerce_max_results("7") == 7
|
|
|
|
def test_caps_value_at_default_maximum(self):
|
|
from deerflow.community.sofya.tools import _coerce_max_results
|
|
|
|
assert _coerce_max_results(999) == 20
|
|
|
|
def test_returns_default_for_non_numeric_string(self):
|
|
from deerflow.community.sofya.tools import _coerce_max_results
|
|
|
|
assert _coerce_max_results("oops") == 5
|
|
|
|
def test_returns_default_for_none(self):
|
|
from deerflow.community.sofya.tools import _coerce_max_results
|
|
|
|
assert _coerce_max_results(None) == 5
|
|
|
|
def test_returns_default_for_zero_or_negative(self):
|
|
from deerflow.community.sofya.tools import _coerce_max_results
|
|
|
|
assert _coerce_max_results(0) == 5
|
|
assert _coerce_max_results(-3) == 5
|
|
|
|
|
|
class TestMissingKeyMessage:
|
|
def test_warns_once_per_tool_name(self, caplog):
|
|
import deerflow.community.sofya.tools as sofya_mod
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
sofya_mod._missing_key_message("web_search")
|
|
sofya_mod._missing_key_message("web_search")
|
|
|
|
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
|
assert len(warnings) == 1
|
|
assert "web_search" in warnings[0].getMessage()
|
|
|
|
def test_warns_separately_for_each_tool(self, caplog):
|
|
import deerflow.community.sofya.tools as sofya_mod
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
sofya_mod._missing_key_message("web_search")
|
|
sofya_mod._missing_key_message("web_fetch")
|
|
|
|
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
|
assert len(warnings) == 2
|
|
|
|
|
|
class TestWebSearchTool:
|
|
def test_basic_search_returns_normalized_results(self, mock_config_with_key):
|
|
results = [
|
|
{"title": "Result 1", "url": "https://example.com/1", "content": "Page content 1", "description": "Snippet 1"},
|
|
{"title": "Result 2", "url": "https://example.com/2", "content": "Page content 2", "description": "Snippet 2"},
|
|
]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "python tutorial"}))
|
|
|
|
assert parsed["query"] == "python tutorial"
|
|
assert parsed["total_results"] == 2
|
|
assert parsed["results"][0] == {"title": "Result 1", "url": "https://example.com/1", "content": "Page content 1"}
|
|
assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer test-sofya-key"
|
|
assert mock_post.call_args.kwargs["json"]["query"] == "python tutorial"
|
|
assert mock_post.call_args.kwargs["json"]["search_depth"] == "basic"
|
|
assert "freshness" not in mock_post.call_args.kwargs["json"]
|
|
|
|
def test_falls_back_to_description_when_content_is_empty(self, mock_config_with_key):
|
|
results = [{"title": "Result", "url": "https://example.com", "content": "", "description": "Snippet"}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["results"][0]["content"] == "Snippet"
|
|
|
|
def test_time_range_is_sent_as_freshness(self, mock_config_with_key):
|
|
results = [{"title": "Result", "url": "https://example.com", "content": "Body"}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
web_search_tool.invoke({"query": "test", "time_range": "week"})
|
|
|
|
assert mock_post.call_args.kwargs["json"]["freshness"] == "week"
|
|
|
|
def test_search_depth_can_be_set_from_config(self, mock_config_with_key):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"search_depth": "snippets",
|
|
}
|
|
results = [{"title": "Result", "url": "https://example.com", "description": "Snippet"}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
web_search_tool.invoke({"query": "test"})
|
|
|
|
assert mock_post.call_args.kwargs["json"]["search_depth"] == "snippets"
|
|
|
|
def test_non_string_content_does_not_raise(self, mock_config_with_key):
|
|
results = [
|
|
{"title": "Numeric", "url": "https://example.com/1", "content": 12345},
|
|
{"title": "Listy", "url": "https://example.com/2", "content": None, "description": ["a", "b"]},
|
|
]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["results"][0]["content"] == "12345"
|
|
assert parsed["results"][1]["content"] == "['a', 'b']"
|
|
|
|
def test_result_content_is_capped_by_default(self, mock_config_with_key):
|
|
results = [{"title": "Result", "url": "https://example.com", "content": "x" * 9000}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert len(parsed["results"][0]["content"]) == 2000
|
|
|
|
def test_contents_max_characters_from_config(self, mock_config_with_key):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"contents_max_characters": 100,
|
|
}
|
|
results = [{"title": "Result", "url": "https://example.com", "content": "x" * 9000}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert len(parsed["results"][0]["content"]) == 100
|
|
|
|
def test_contents_max_characters_zero_disables_the_cap(self, mock_config_with_key):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"contents_max_characters": 0,
|
|
}
|
|
results = [{"title": "Result", "url": "https://example.com", "content": "x" * 9000}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert len(parsed["results"][0]["content"]) == 9000
|
|
|
|
def test_invalid_contents_max_characters_falls_back_to_default(self):
|
|
from deerflow.community.sofya.tools import _coerce_content_limit
|
|
|
|
assert _coerce_content_limit("oops") == 2000
|
|
assert _coerce_content_limit(None) == 2000
|
|
assert _coerce_content_limit(-5) == 2000
|
|
assert _coerce_content_limit(0) == 0
|
|
assert _coerce_content_limit("150") == 150
|
|
|
|
def test_default_search_stays_under_the_externalize_threshold(self, mock_config_with_key):
|
|
"""Five capped results must stay inline rather than being persisted to disk."""
|
|
results = [{"title": f"R{i}", "url": f"https://example.com/{i}", "content": "x" * 20000} for i in range(5)]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
output = web_search_tool.invoke({"query": "test"})
|
|
|
|
assert len(output) < 12000
|
|
|
|
def test_caller_max_results_wins_over_config(self, mock_config_with_key):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"max_results": 5,
|
|
}
|
|
results = [{"title": f"R{i}", "url": f"https://x.com/{i}", "content": f"C{i}"} for i in range(10)]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test", "max_results": 8}))
|
|
|
|
assert parsed["total_results"] == 8
|
|
assert mock_post.call_args.kwargs["json"]["max_results"] == 8
|
|
|
|
def test_unsupported_search_depth_falls_back_with_warning(self, mock_config_with_key, caplog):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"search_depth": "advanced",
|
|
}
|
|
results = [{"title": "Result", "url": "https://example.com", "content": "Body"}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
web_search_tool.invoke({"query": "test"})
|
|
|
|
assert mock_post.call_args.kwargs["json"]["search_depth"] == "basic"
|
|
assert any("search_depth" in r.getMessage() for r in caplog.records if r.levelno == logging.WARNING)
|
|
|
|
def test_search_depth_is_normalized(self, mock_config_with_key):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"search_depth": " Snippets ",
|
|
}
|
|
results = [{"title": "Result", "url": "https://example.com", "description": "Snippet"}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
web_search_tool.invoke({"query": "test"})
|
|
|
|
assert mock_post.call_args.kwargs["json"]["search_depth"] == "snippets"
|
|
|
|
def test_respects_max_results_from_config(self, mock_config_with_key):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"max_results": 3,
|
|
}
|
|
results = [{"title": f"R{i}", "url": f"https://x.com/{i}", "content": f"C{i}"} for i in range(10)]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["total_results"] == 3
|
|
assert mock_post.call_args.kwargs["json"]["max_results"] == 3
|
|
|
|
def test_config_max_results_is_capped(self, mock_config_with_key):
|
|
mock_config_with_key.return_value.get_tool_config.return_value.model_extra = {
|
|
"api_key": "test-key",
|
|
"max_results": 999,
|
|
}
|
|
results = [{"title": f"R{i}", "url": f"https://x.com/{i}", "content": f"C{i}"} for i in range(30)]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["total_results"] == 20
|
|
assert mock_post.call_args.kwargs["json"]["max_results"] == 20
|
|
|
|
def test_max_results_parameter_accepted(self, mock_config_no_key):
|
|
"""Tool accepts max_results as a call parameter when config does not override it."""
|
|
results = [{"title": f"R{i}", "url": f"https://x.com/{i}", "content": f"C{i}"} for i in range(10)]
|
|
|
|
with patch.dict("os.environ", {"SOFYA_API_KEY": "env-key"}):
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test", "max_results": 2}))
|
|
|
|
assert parsed["total_results"] == 2
|
|
|
|
def test_empty_results_return_error_json(self, mock_config_with_key):
|
|
"""An empty result list returns a structured error, matching ddg_search convention."""
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response([])
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "no results"}))
|
|
|
|
assert parsed["error"] == "No results found"
|
|
assert parsed["query"] == "no results"
|
|
|
|
def test_unexpected_results_type_returns_error_json(self, mock_config_with_key):
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_response({"results": "nope"})
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["error"] == "Sofya returned an unexpected response format"
|
|
|
|
def test_http_error_returns_error_json(self, mock_config_with_key):
|
|
request = httpx.Request("POST", "https://sofya.co/v1/search")
|
|
response = httpx.Response(402, text="Insufficient credits", request=request)
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = httpx.HTTPStatusError("error", request=request, response=response)
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = mock_resp
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["error"] == "Sofya API error: HTTP 402"
|
|
|
|
def test_network_error_returns_error_json(self, mock_config_with_key):
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.side_effect = httpx.ConnectError("boom")
|
|
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["error"] == "boom"
|
|
|
|
def test_missing_key_returns_error_json(self, mock_config_no_key):
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
from deerflow.community.sofya.tools import web_search_tool
|
|
|
|
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
|
|
|
assert parsed["error"] == "SOFYA_API_KEY is not configured"
|
|
assert parsed["query"] == "test"
|
|
|
|
|
|
class TestWebFetchTool:
|
|
def test_returns_title_and_content(self, mock_config_with_key):
|
|
results = [{"title": "Example Page", "url": "https://example.com", "content": "# Markdown body", "success": True}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_post = mock_client_cls.return_value.__enter__.return_value.post
|
|
mock_post.return_value = _make_fetch_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "# Example Page\n\n# Markdown body"
|
|
assert mock_post.call_args.kwargs["json"] == {"urls": ["https://example.com"]}
|
|
|
|
def test_truncates_long_content(self, mock_config_with_key):
|
|
results = [{"title": "Long", "url": "https://example.com", "content": "x" * 9000, "success": True}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert len(result) == len("# Long\n\n") + 4096
|
|
|
|
def test_non_string_content_does_not_raise(self, mock_config_with_key):
|
|
results = [{"title": "Numeric", "url": "https://example.com", "content": 12345, "success": True}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "# Numeric\n\n12345"
|
|
|
|
def test_missing_content_still_reports_no_content(self, mock_config_with_key):
|
|
results = [{"title": "Empty", "url": "https://example.com", "content": None, "success": True}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "Error: No content found"
|
|
|
|
def test_falls_back_to_untitled(self, mock_config_with_key):
|
|
results = [{"title": "", "url": "https://example.com", "content": "Body", "success": True}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "# Untitled\n\nBody"
|
|
|
|
def test_failed_result_returns_its_error(self, mock_config_with_key):
|
|
results = [{"url": "https://example.com", "success": False, "error": "404 Not Found"}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "Error: 404 Not Found"
|
|
|
|
def test_empty_content_returns_error(self, mock_config_with_key):
|
|
results = [{"title": "Empty", "url": "https://example.com", "content": "", "success": True}]
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(results)
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "Error: No content found"
|
|
|
|
def test_no_results_returns_error(self, mock_config_with_key):
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response([])
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "Error: No results found"
|
|
|
|
def test_http_error_returns_error_string(self, mock_config_with_key):
|
|
request = httpx.Request("POST", "https://sofya.co/v1/fetch")
|
|
response = httpx.Response(401, text="Invalid API key", request=request)
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = httpx.HTTPStatusError("error", request=request, response=response)
|
|
|
|
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
|
mock_client_cls.return_value.__enter__.return_value.post.return_value = mock_resp
|
|
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
|
|
|
assert result == "Error: Sofya API error: HTTP 401"
|
|
|
|
def test_missing_key_returns_error_string(self, mock_config_no_key):
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
from deerflow.community.sofya.tools import web_fetch_tool
|
|
|
|
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
|