Files
DocsGPT/tests/agents/test_duckduckgo_tool.py
Alex 574f96341e refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
2026-09-07 10:20:43 +01:00

146 lines
5.2 KiB
Python

"""Tests for docsgpt/agents/tools/duckduckgo.py"""
from unittest.mock import MagicMock, patch
import pytest
from docsgpt.agents.tools.duckduckgo import DuckDuckGoSearchTool
@pytest.fixture
def tool():
return DuckDuckGoSearchTool(config={})
@pytest.mark.unit
class TestDuckDuckGoExecuteAction:
def test_unknown_action_raises(self, tool):
with pytest.raises(ValueError, match="Unknown action"):
tool.execute_action("invalid")
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_web_search_success(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.text.return_value = [
{"title": "Result 1", "href": "https://example.com", "body": "snippet"}
]
mock_client_factory.return_value = mock_client
result = tool.execute_action("ddg_web_search", query="python")
assert result["status_code"] == 200
assert len(result["results"]) == 1
assert "successfully" in result["message"]
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_image_search_success(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.images.return_value = [{"image": "https://img.com/1.jpg"}]
mock_client_factory.return_value = mock_client
result = tool.execute_action("ddg_image_search", query="cats")
assert result["status_code"] == 200
assert len(result["results"]) == 1
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_news_search_success(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.news.return_value = [{"title": "News"}]
mock_client_factory.return_value = mock_client
result = tool.execute_action("ddg_news_search", query="tech")
assert result["status_code"] == 200
assert len(result["results"]) == 1
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_search_error_returns_500(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.text.side_effect = Exception("Network error")
mock_client_factory.return_value = mock_client
result = tool.execute_action("ddg_web_search", query="test")
assert result["status_code"] == 500
assert "failed" in result["message"].lower()
assert result["results"] == []
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_max_results_capped_at_20(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.text.return_value = []
mock_client_factory.return_value = mock_client
tool.execute_action("ddg_web_search", query="test", max_results=100)
call_kwargs = mock_client.text.call_args[1]
assert call_kwargs["max_results"] == 20
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_image_max_results_capped_at_50(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.images.return_value = []
mock_client_factory.return_value = mock_client
tool.execute_action("ddg_image_search", query="test", max_results=200)
call_kwargs = mock_client.images.call_args[1]
assert call_kwargs["max_results"] == 50
@patch("docsgpt.agents.tools.duckduckgo.time.sleep")
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_rate_limit_retries(self, mock_client_factory, mock_sleep, tool):
mock_client = MagicMock()
mock_client.text.side_effect = [
Exception("RateLimit exceeded"),
[{"title": "Result"}],
]
mock_client_factory.return_value = mock_client
result = tool.execute_action("ddg_web_search", query="test")
assert result["status_code"] == 200
assert len(result["results"]) == 1
mock_sleep.assert_called_once()
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_empty_results(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.text.return_value = []
mock_client_factory.return_value = mock_client
result = tool.execute_action("ddg_web_search", query="obscure query")
assert result["status_code"] == 200
assert result["results"] == []
@patch.object(DuckDuckGoSearchTool, "_get_ddgs_client")
def test_none_results(self, mock_client_factory, tool):
mock_client = MagicMock()
mock_client.text.return_value = None
mock_client_factory.return_value = mock_client
result = tool.execute_action("ddg_web_search", query="test")
assert result["status_code"] == 200
assert result["results"] == []
@pytest.mark.unit
class TestDuckDuckGoMetadata:
def test_actions_metadata(self, tool):
meta = tool.get_actions_metadata()
assert len(meta) == 3
names = {a["name"] for a in meta}
assert "ddg_web_search" in names
assert "ddg_image_search" in names
assert "ddg_news_search" in names
def test_config_requirements(self, tool):
assert tool.get_config_requirements() == {}
def test_custom_timeout(self):
tool = DuckDuckGoSearchTool(config={"timeout": 30})
assert tool.timeout == 30