"""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")