- Flask web app with Perplexity-style search + synthesis pipeline - Degoog → SearXNG → DuckDuckGo cascading search backends - Gemini LLM for query expansion + answer synthesis with citations - MCP server exposing search tools via stdio - 57 unit tests covering all modules
136 lines
4.7 KiB
Python
136 lines
4.7 KiB
Python
"""Tests for sibilla.ai — GeminiClient with mocked API."""
|
|
|
|
import pytest
|
|
from unittest import mock
|
|
from sibilla.ai import GeminiClient, SynthesisResult
|
|
|
|
|
|
class TestSynthesisResult:
|
|
def test_defaults(self):
|
|
sr = SynthesisResult(answer="")
|
|
assert sr.answer == ""
|
|
assert sr.model == ""
|
|
assert sr.tokens_used == 0
|
|
assert sr.error is None
|
|
|
|
|
|
class TestGeminiClient:
|
|
def test_no_api_key_returns_error(self):
|
|
client = GeminiClient(api_key="")
|
|
result = client.synthesize("system", "user")
|
|
assert result.answer == ""
|
|
assert "GOOGLE_API_KEY" in result.error
|
|
|
|
def test_lazy_client_raises_without_key(self):
|
|
client = GeminiClient(api_key="")
|
|
with pytest.raises(ValueError, match="GOOGLE_API_KEY"):
|
|
_ = client.client
|
|
|
|
def test_synthesize_success(self):
|
|
client = GeminiClient(api_key="fake-key")
|
|
mock_genai = mock.MagicMock()
|
|
client._client = mock_genai
|
|
|
|
mock_part = mock.MagicMock()
|
|
mock_part.text = "This is the answer."
|
|
mock_content = mock.MagicMock()
|
|
mock_content.parts = [mock_part]
|
|
mock_candidate = mock.MagicMock()
|
|
mock_candidate.content = mock_content
|
|
mock_usage = mock.MagicMock()
|
|
mock_usage.total_token_count = 42
|
|
mock_response = mock.MagicMock()
|
|
mock_response.candidates = [mock_candidate]
|
|
mock_response.usage_metadata = mock_usage
|
|
|
|
mock_genai.models.generate_content.return_value = mock_response
|
|
|
|
result = client.synthesize("system prompt", "user query")
|
|
|
|
assert result.answer == "This is the answer."
|
|
assert result.tokens_used == 42
|
|
assert result.model == "gemini-2.5-flash"
|
|
assert result.error is None
|
|
|
|
def test_synthesize_no_candidates(self):
|
|
client = GeminiClient(api_key="fake-key")
|
|
mock_genai = mock.MagicMock()
|
|
client._client = mock_genai
|
|
|
|
mock_response = mock.MagicMock()
|
|
mock_response.candidates = []
|
|
mock_response.usage_metadata = None
|
|
mock_genai.models.generate_content.return_value = mock_response
|
|
|
|
result = client.synthesize("s", "u")
|
|
assert result.answer == ""
|
|
assert result.error is None
|
|
|
|
def test_synthesize_api_exception(self):
|
|
client = GeminiClient(api_key="fake-key")
|
|
mock_genai = mock.MagicMock()
|
|
client._client = mock_genai
|
|
|
|
mock_genai.models.generate_content.side_effect = RuntimeError("API down")
|
|
|
|
result = client.synthesize("s", "u")
|
|
assert result.error == "API down"
|
|
assert result.answer == ""
|
|
|
|
def test_rewrite_query_no_api_key(self):
|
|
client = GeminiClient(api_key="")
|
|
variants = client.rewrite_query("test query", num_variants=3)
|
|
assert variants == ["test query"]
|
|
|
|
def test_rewrite_query_success(self):
|
|
client = GeminiClient(api_key="fake-key")
|
|
mock_genai = mock.MagicMock()
|
|
client._client = mock_genai
|
|
|
|
mock_part = mock.MagicMock()
|
|
mock_part.text = "rewritten query 1\nrewritten query 2\nrewritten query 3"
|
|
mock_content = mock.MagicMock()
|
|
mock_content.parts = [mock_part]
|
|
mock_candidate = mock.MagicMock()
|
|
mock_candidate.content = mock_content
|
|
mock_response = mock.MagicMock()
|
|
mock_response.candidates = [mock_candidate]
|
|
mock_genai.models.generate_content.return_value = mock_response
|
|
|
|
variants = client.rewrite_query("original", num_variants=3)
|
|
|
|
assert len(variants) == 4
|
|
assert variants[0] == "original"
|
|
assert "rewritten query 1" in variants
|
|
|
|
def test_rewrite_query_strips_bullets(self):
|
|
client = GeminiClient(api_key="fake-key")
|
|
mock_genai = mock.MagicMock()
|
|
client._client = mock_genai
|
|
|
|
mock_part = mock.MagicMock()
|
|
mock_part.text = "- bullet query\n• dot query"
|
|
mock_content = mock.MagicMock()
|
|
mock_content.parts = [mock_part]
|
|
mock_candidate = mock.MagicMock()
|
|
mock_candidate.content = mock_content
|
|
mock_response = mock.MagicMock()
|
|
mock_response.candidates = [mock_candidate]
|
|
mock_genai.models.generate_content.return_value = mock_response
|
|
|
|
variants = client.rewrite_query("original", num_variants=2)
|
|
|
|
assert "bullet query" in variants
|
|
assert "dot query" in variants
|
|
assert not any(v.startswith("-") for v in variants)
|
|
|
|
def test_rewrite_query_exception_fallback(self):
|
|
client = GeminiClient(api_key="fake-key")
|
|
mock_genai = mock.MagicMock()
|
|
client._client = mock_genai
|
|
|
|
mock_genai.models.generate_content.side_effect = RuntimeError("boom")
|
|
|
|
variants = client.rewrite_query("original", num_variants=2)
|
|
assert variants == ["original"]
|