This commit was merged in pull request #1239.
This commit is contained in:
149
tests/unit/test_research_tools.py
Normal file
149
tests/unit/test_research_tools.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Unit tests for src/timmy/research_tools.py.
|
||||
|
||||
Refs #1237
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
# ── Stub serpapi before any import of research_tools ─────────────────────────
|
||||
|
||||
_serpapi_stub = ModuleType("serpapi")
|
||||
_google_search_mock = MagicMock()
|
||||
_serpapi_stub.GoogleSearch = _google_search_mock
|
||||
sys.modules.setdefault("serpapi", _serpapi_stub)
|
||||
|
||||
|
||||
# ── google_web_search ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestGoogleWebSearch:
|
||||
"""google_web_search returns results or degrades gracefully."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_empty_string_when_no_api_key(self, monkeypatch):
|
||||
monkeypatch.delenv("SERPAPI_API_KEY", raising=False)
|
||||
from timmy.research_tools import google_web_search
|
||||
|
||||
result = await google_web_search("test query")
|
||||
assert result == ""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_warning_when_no_api_key(self, monkeypatch, caplog):
|
||||
import logging
|
||||
|
||||
monkeypatch.delenv("SERPAPI_API_KEY", raising=False)
|
||||
from timmy.research_tools import google_web_search
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="timmy.research_tools"):
|
||||
await google_web_search("test query")
|
||||
|
||||
assert any("SERPAPI_API_KEY" in rec.message for rec in caplog.records)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calls_google_search_with_api_key(self, monkeypatch):
|
||||
monkeypatch.setenv("SERPAPI_API_KEY", "fake-key-123")
|
||||
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.get_dict.return_value = {"organic_results": [{"title": "Result"}]}
|
||||
|
||||
with patch("timmy.research_tools.GoogleSearch", return_value=mock_instance) as mock_cls:
|
||||
from timmy.research_tools import google_web_search
|
||||
|
||||
result = await google_web_search("hello world")
|
||||
|
||||
mock_cls.assert_called_once()
|
||||
call_params = mock_cls.call_args[0][0]
|
||||
assert call_params["q"] == "hello world"
|
||||
assert call_params["api_key"] == "fake-key-123"
|
||||
mock_instance.get_dict.assert_called_once()
|
||||
assert "organic_results" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_string_result(self, monkeypatch):
|
||||
monkeypatch.setenv("SERPAPI_API_KEY", "key")
|
||||
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.get_dict.return_value = {"answer": 42}
|
||||
|
||||
with patch("timmy.research_tools.GoogleSearch", return_value=mock_instance):
|
||||
from timmy.research_tools import google_web_search
|
||||
|
||||
result = await google_web_search("query")
|
||||
|
||||
assert isinstance(result, str)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passes_query_to_params(self, monkeypatch):
|
||||
monkeypatch.setenv("SERPAPI_API_KEY", "k")
|
||||
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.get_dict.return_value = {}
|
||||
|
||||
with patch("timmy.research_tools.GoogleSearch", return_value=mock_instance) as mock_cls:
|
||||
from timmy.research_tools import google_web_search
|
||||
|
||||
await google_web_search("specific search term")
|
||||
|
||||
params = mock_cls.call_args[0][0]
|
||||
assert params["q"] == "specific search term"
|
||||
|
||||
|
||||
# ── get_llm_client ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestGetLLMClient:
|
||||
"""get_llm_client returns a client with a completion method."""
|
||||
|
||||
def test_returns_non_none_client(self):
|
||||
from timmy.research_tools import get_llm_client
|
||||
|
||||
client = get_llm_client()
|
||||
assert client is not None
|
||||
|
||||
def test_client_has_completion_method(self):
|
||||
from timmy.research_tools import get_llm_client
|
||||
|
||||
client = get_llm_client()
|
||||
assert hasattr(client, "completion")
|
||||
assert callable(client.completion)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completion_returns_object_with_text(self):
|
||||
from timmy.research_tools import get_llm_client
|
||||
|
||||
client = get_llm_client()
|
||||
result = await client.completion("test prompt", max_tokens=100)
|
||||
assert hasattr(result, "text")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completion_text_is_string(self):
|
||||
from timmy.research_tools import get_llm_client
|
||||
|
||||
client = get_llm_client()
|
||||
result = await client.completion("any prompt", max_tokens=50)
|
||||
assert isinstance(result.text, str)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completion_text_contains_prompt(self):
|
||||
from timmy.research_tools import get_llm_client
|
||||
|
||||
client = get_llm_client()
|
||||
result = await client.completion("my prompt", max_tokens=50)
|
||||
assert "my prompt" in result.text
|
||||
|
||||
def test_each_call_returns_new_client(self):
|
||||
from timmy.research_tools import get_llm_client
|
||||
|
||||
client_a = get_llm_client()
|
||||
client_b = get_llm_client()
|
||||
# Both should be functional clients (not necessarily the same instance)
|
||||
assert hasattr(client_a, "completion")
|
||||
assert hasattr(client_b, "completion")
|
||||
Reference in New Issue
Block a user