Initial commit: Sibilla — self-hosted Perplexity-like AI search engine
- 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
This commit is contained in:
1 parent
ea09bb4f4f
commit
f3fe13c60e
14 files changed
+2099
-31
No files matched your search
@@ -0,0 +1,135 @@
|
||||
"""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"]
|
||||
Reference in new issue
Block a user