"""Unit tests for Synthesizer.""" from unittest.mock import AsyncMock, Mock, patch import pytest from src.llm.synthesizer import Synthesizer, SynthesizerError from src.models.schemas import SearchResult class TestSynthesizer: """Test cases for Synthesizer.""" @pytest.fixture def synthesizer(self): return Synthesizer(api_key="sk-test-key") def test_init_without_api_key_raises(self): """Test that initialization without API key raises error.""" with patch.dict("os.environ", {}, clear=True): with pytest.raises(SynthesizerError) as exc_info: Synthesizer() assert "API key required" in str(exc_info.value) def test_init_with_env_var(self): """Test initialization with environment variable.""" with patch.dict("os.environ", {"RESEARCH_BRIDGE_KIMI_API_KEY": "sk-env-key"}): synth = Synthesizer() assert synth.api_key == "sk-env-key" def test_default_headers_set(self): """Test that required User-Agent header is set.""" synth = Synthesizer(api_key="sk-test") assert "User-Agent" in synth.DEFAULT_HEADERS assert synth.DEFAULT_HEADERS["User-Agent"] == "KimiCLI/0.77" def test_format_search_results(self, synthesizer): """Test formatting of search results.""" results = [ SearchResult( title="Test Title", url="https://example.com", content="Test content", source="google" ), SearchResult( title="Second Title", url="https://test.com", content=None, source="bing" ) ] formatted = synthesizer._format_search_results(results) assert "[1] Test Title" in formatted assert "URL: https://example.com" in formatted assert "Test content" in formatted assert "[2] Second Title" in formatted assert "No snippet available" in formatted def test_build_prompt(self, synthesizer): """Test prompt building.""" results = [ SearchResult( title="Python Asyncio", url="https://docs.python.org", content="Asyncio docs", source="google" ) ] prompt = synthesizer._build_prompt("what is asyncio", results) assert "User Query: what is asyncio" in prompt assert "Python Asyncio" in prompt assert "docs.python.org" in prompt @pytest.mark.asyncio async def test_synthesize_success(self, synthesizer): """Test successful synthesis.""" mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message.content = "Asyncio is a library..." mock_response.usage = Mock() mock_response.usage.total_tokens = 100 mock_response.usage.prompt_tokens = 80 mock_response.usage.completion_tokens = 20 mock_client = AsyncMock() mock_client.chat.completions.create = AsyncMock(return_value=mock_response) with patch.object(synthesizer, '_client', mock_client): results = [ SearchResult( title="Test", url="https://example.com", content="Content", source="google" ) ] result = await synthesizer.synthesize("test query", results) assert result.content == "Asyncio is a library..." assert result.tokens_used == 100 assert result.prompt_tokens == 80 assert result.completion_tokens == 20 assert len(result.sources) == 1 @pytest.mark.asyncio async def test_synthesize_truncates_results(self, synthesizer): """Test that synthesis truncates to top 5 results.""" mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message.content = "Answer" mock_response.usage = None mock_client = AsyncMock() mock_client.chat.completions.create = AsyncMock(return_value=mock_response) # Create 10 results results = [ SearchResult( title=f"Result {i}", url=f"https://example{i}.com", content=f"Content {i}", source="google" ) for i in range(10) ] with patch.object(synthesizer, '_client', mock_client): result = await synthesizer.synthesize("test", results) # Should only use first 5 assert len(result.sources) == 5 @pytest.mark.asyncio async def test_synthesize_api_error(self, synthesizer): """Test handling of API errors.""" mock_client = AsyncMock() mock_client.chat.completions.create = AsyncMock( side_effect=Exception("API Error") ) with patch.object(synthesizer, '_client', mock_client): results = [ SearchResult( title="Test", url="https://example.com", content="Content", source="google" ) ] with pytest.raises(SynthesizerError) as exc_info: await synthesizer.synthesize("test", results) assert "Kimi API error" in str(exc_info.value) @pytest.mark.asyncio async def test_health_check_success(self, synthesizer): """Test successful health check.""" mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message.content = "Hi" mock_client = AsyncMock() mock_client.chat.completions.create = AsyncMock(return_value=mock_response) with patch.object(synthesizer, '_client', mock_client): result = await synthesizer.health_check() assert result is True @pytest.mark.asyncio async def test_health_check_failure(self, synthesizer): """Test failed health check.""" mock_client = AsyncMock() mock_client.chat.completions.create = AsyncMock( side_effect=Exception("Connection error") ) with patch.object(synthesizer, '_client', mock_client): result = await synthesizer.health_check() assert result is False