Initial commit: Research Bridge API with Podman support
This commit is contained in:
134
tests/unit/test_models.py
Normal file
134
tests/unit/test_models.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Unit tests for Pydantic models."""
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.models.schemas import (
|
||||
HealthResponse,
|
||||
ResearchRequest,
|
||||
ResearchResponse,
|
||||
SearchRequest,
|
||||
SearchResponse,
|
||||
SearchResult,
|
||||
)
|
||||
|
||||
|
||||
class TestSearchRequest:
|
||||
"""Test cases for SearchRequest validation."""
|
||||
|
||||
def test_valid_request(self):
|
||||
"""Test valid search request."""
|
||||
request = SearchRequest(q="python asyncio", engines=["google"], page=1)
|
||||
assert request.q == "python asyncio"
|
||||
assert request.engines == ["google"]
|
||||
assert request.page == 1
|
||||
|
||||
def test_default_engines(self):
|
||||
"""Test default engines."""
|
||||
request = SearchRequest(q="test")
|
||||
assert "google" in request.engines
|
||||
assert "bing" in request.engines
|
||||
|
||||
def test_empty_query_fails(self):
|
||||
"""Test empty query fails validation."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SearchRequest(q="", engines=["google"])
|
||||
assert "String should have at least 1 character" in str(exc_info.value)
|
||||
|
||||
def test_query_too_long_fails(self):
|
||||
"""Test query exceeding max length fails."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SearchRequest(q="x" * 501, engines=["google"])
|
||||
assert "String should have at most 500 characters" in str(exc_info.value)
|
||||
|
||||
def test_page_must_be_positive(self):
|
||||
"""Test page number must be positive."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SearchRequest(q="test", page=0)
|
||||
assert "Input should be greater than or equal to 1" in str(exc_info.value)
|
||||
|
||||
|
||||
class TestResearchRequest:
|
||||
"""Test cases for ResearchRequest validation."""
|
||||
|
||||
def test_valid_request(self):
|
||||
"""Test valid research request."""
|
||||
request = ResearchRequest(
|
||||
query="python asyncio",
|
||||
depth="deep",
|
||||
sources=["web", "news"]
|
||||
)
|
||||
assert request.query == "python asyncio"
|
||||
assert request.depth == "deep"
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default values."""
|
||||
request = ResearchRequest(query="test")
|
||||
assert request.depth == "shallow"
|
||||
assert request.sources == ["web"]
|
||||
assert request.language == "en"
|
||||
assert request.omit_raw is False
|
||||
|
||||
def test_invalid_depth_fails(self):
|
||||
"""Test invalid depth fails."""
|
||||
with pytest.raises(ValidationError):
|
||||
ResearchRequest(query="test", depth="invalid")
|
||||
|
||||
def test_invalid_language_fails(self):
|
||||
"""Test invalid language code fails."""
|
||||
with pytest.raises(ValidationError):
|
||||
ResearchRequest(query="test", language="english")
|
||||
|
||||
|
||||
class TestSearchResult:
|
||||
"""Test cases for SearchResult validation."""
|
||||
|
||||
def test_valid_result(self):
|
||||
"""Test valid search result."""
|
||||
result = SearchResult(
|
||||
title="Python Documentation",
|
||||
url="https://docs.python.org",
|
||||
content="Python docs",
|
||||
source="google",
|
||||
score=0.95
|
||||
)
|
||||
assert result.title == "Python Documentation"
|
||||
assert str(result.url) == "https://docs.python.org/"
|
||||
|
||||
def test_title_required(self):
|
||||
"""Test title is required."""
|
||||
with pytest.raises(ValidationError):
|
||||
SearchResult(url="https://example.com", source="google")
|
||||
|
||||
def test_invalid_url_fails(self):
|
||||
"""Test invalid URL fails."""
|
||||
with pytest.raises(ValidationError):
|
||||
SearchResult(title="Test", url="not-a-url", source="google")
|
||||
|
||||
|
||||
class TestHealthResponse:
|
||||
"""Test cases for HealthResponse."""
|
||||
|
||||
def test_valid_response(self):
|
||||
"""Test valid health response."""
|
||||
response = HealthResponse(
|
||||
status="healthy",
|
||||
searxng_connected=True,
|
||||
kimi_coding_available=False
|
||||
)
|
||||
assert response.status == "healthy"
|
||||
assert response.version == "0.1.0"
|
||||
|
||||
|
||||
class TestResearchResponse:
|
||||
"""Test cases for ResearchResponse."""
|
||||
|
||||
def test_phase1_response(self):
|
||||
"""Test Phase 1 response without synthesis."""
|
||||
response = ResearchResponse(
|
||||
query="test",
|
||||
depth="shallow",
|
||||
synthesis=None,
|
||||
metadata={"phase": "1"}
|
||||
)
|
||||
assert response.synthesis is None
|
||||
assert response.metadata["phase"] == "1"
|
||||
260
tests/unit/test_router.py
Normal file
260
tests/unit/test_router.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""Unit tests for API router."""
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.app import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = create_app()
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
class TestHealthEndpoint:
|
||||
"""Test cases for health endpoint."""
|
||||
|
||||
def test_health_searxng_healthy(self, client):
|
||||
"""Test health check when SearXNG is up."""
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.health_check = AsyncMock(return_value=True)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "degraded" # No Kimi key configured in test
|
||||
assert data["searxng_connected"] is True
|
||||
assert data["kimi_coding_available"] is False
|
||||
|
||||
def test_health_searxng_down(self, client):
|
||||
"""Test health check when SearXNG is down."""
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.health_check = AsyncMock(return_value=False)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "degraded"
|
||||
assert data["searxng_connected"] is False
|
||||
|
||||
|
||||
class TestSearchEndpoint:
|
||||
"""Test cases for search endpoint."""
|
||||
|
||||
def test_search_get_success(self, client):
|
||||
"""Test GET search with successful response."""
|
||||
mock_response = Mock()
|
||||
mock_response.query = "python"
|
||||
mock_response.results = []
|
||||
mock_response.total = 0
|
||||
mock_response.page = 1
|
||||
mock_response.metadata = {}
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.get("/search?q=python")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["query"] == "python"
|
||||
assert "results" in data
|
||||
|
||||
def test_search_post_success(self, client):
|
||||
"""Test POST search with successful response."""
|
||||
mock_response = Mock()
|
||||
mock_response.query = "asyncio"
|
||||
mock_response.results = []
|
||||
mock_response.total = 0
|
||||
mock_response.page = 1
|
||||
mock_response.metadata = {}
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.post("/search", json={
|
||||
"q": "asyncio",
|
||||
"engines": ["google"],
|
||||
"page": 1
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_search_validation_error(self, client):
|
||||
"""Test search with invalid parameters."""
|
||||
response = client.get("/search?q=a")
|
||||
# Just test that it accepts the request
|
||||
assert response.status_code in [200, 502] # 502 if no SearXNG
|
||||
|
||||
|
||||
class TestResearchEndpoint:
|
||||
"""Test cases for research endpoint (Phase 2 - with synthesis)."""
|
||||
|
||||
def test_research_phase2_with_synthesis(self, client):
|
||||
"""Test research endpoint returns synthesis (Phase 2)."""
|
||||
from src.models.schemas import SearchResult
|
||||
import src.api.router as router_module
|
||||
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = [
|
||||
SearchResult(title="Test", url="https://example.com", source="google")
|
||||
]
|
||||
mock_search_response.total = 1
|
||||
mock_search_response.page = 1
|
||||
|
||||
mock_synthesis = Mock()
|
||||
mock_synthesis.content = "This is a synthesized answer."
|
||||
mock_synthesis.sources = [{"index": 1, "title": "Test", "url": "https://example.com"}]
|
||||
mock_synthesis.tokens_used = 100
|
||||
|
||||
# Temporarily set API key in router module
|
||||
original_key = router_module.KIMI_API_KEY
|
||||
router_module.KIMI_API_KEY = "sk-test"
|
||||
|
||||
try:
|
||||
with patch("src.api.router.SearXNGClient") as mock_search_class, \
|
||||
patch("src.api.router.Synthesizer") as mock_synth_class:
|
||||
|
||||
# Mock SearXNG
|
||||
mock_search_instance = AsyncMock()
|
||||
mock_search_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_search_instance.__aenter__ = AsyncMock(return_value=mock_search_instance)
|
||||
mock_search_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_search_class.return_value = mock_search_instance
|
||||
|
||||
# Mock Synthesizer
|
||||
mock_synth_instance = AsyncMock()
|
||||
mock_synth_instance.synthesize = AsyncMock(return_value=mock_synthesis)
|
||||
mock_synth_instance.__aenter__ = AsyncMock(return_value=mock_synth_instance)
|
||||
mock_synth_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_synth_class.return_value = mock_synth_instance
|
||||
|
||||
response = client.post("/research", json={
|
||||
"query": "python asyncio",
|
||||
"depth": "shallow",
|
||||
"sources": ["web"]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["query"] == "python asyncio"
|
||||
assert data["depth"] == "shallow"
|
||||
assert data["synthesis"] == "This is a synthesized answer."
|
||||
assert data["metadata"]["phase"] == "2"
|
||||
assert len(data["sources"]) == 1
|
||||
finally:
|
||||
router_module.KIMI_API_KEY = original_key
|
||||
|
||||
def test_research_no_api_key_returns_message(self, client):
|
||||
"""Test research endpoint without API key returns appropriate message."""
|
||||
from src.models.schemas import SearchResult
|
||||
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = [
|
||||
SearchResult(title="Test", url="https://example.com", source="google")
|
||||
]
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
# Ensure no API key
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
with patch("src.api.router.KIMI_API_KEY", None):
|
||||
response = client.post("/research", json={
|
||||
"query": "test",
|
||||
"sources": ["web"]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "not configured" in data["synthesis"].lower() or "API key" in data["synthesis"]
|
||||
|
||||
def test_research_no_results(self, client):
|
||||
"""Test research endpoint with no search results."""
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = []
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.post("/research", json={
|
||||
"query": "xyzabc123nonexistent",
|
||||
"sources": ["web"]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "no results" in data["synthesis"].lower()
|
||||
|
||||
def test_research_with_omit_raw(self, client):
|
||||
"""Test research endpoint with omit_raw=true."""
|
||||
from src.models.schemas import SearchResult
|
||||
import src.api.router as router_module
|
||||
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = [
|
||||
SearchResult(title="Test", url="https://example.com", source="google")
|
||||
]
|
||||
|
||||
mock_synthesis = Mock()
|
||||
mock_synthesis.content = "Answer"
|
||||
mock_synthesis.sources = []
|
||||
mock_synthesis.tokens_used = 50
|
||||
|
||||
original_key = router_module.KIMI_API_KEY
|
||||
router_module.KIMI_API_KEY = "sk-test"
|
||||
|
||||
try:
|
||||
with patch("src.api.router.SearXNGClient") as mock_search_class, \
|
||||
patch("src.api.router.Synthesizer") as mock_synth_class:
|
||||
|
||||
mock_search_instance = AsyncMock()
|
||||
mock_search_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_search_instance.__aenter__ = AsyncMock(return_value=mock_search_instance)
|
||||
mock_search_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_search_class.return_value = mock_search_instance
|
||||
|
||||
mock_synth_instance = AsyncMock()
|
||||
mock_synth_instance.synthesize = AsyncMock(return_value=mock_synthesis)
|
||||
mock_synth_instance.__aenter__ = AsyncMock(return_value=mock_synth_instance)
|
||||
mock_synth_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_synth_class.return_value = mock_synth_instance
|
||||
|
||||
response = client.post("/research", json={
|
||||
"query": "test",
|
||||
"omit_raw": True
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["raw_results"] is None
|
||||
finally:
|
||||
router_module.KIMI_API_KEY = original_key
|
||||
140
tests/unit/test_searxng_client.py
Normal file
140
tests/unit/test_searxng_client.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Unit tests for SearXNG client."""
|
||||
import json
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from httpx import Response
|
||||
|
||||
from src.models.schemas import SearchRequest
|
||||
from src.search.searxng import SearXNGClient, SearXNGError
|
||||
|
||||
|
||||
class TestSearXNGClient:
|
||||
"""Test cases for SearXNGClient."""
|
||||
|
||||
@pytest.fixture
|
||||
def client(self):
|
||||
return SearXNGClient(base_url="http://test:8080")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_success(self, client):
|
||||
"""Test successful search request."""
|
||||
mock_response = {
|
||||
"results": [
|
||||
{
|
||||
"title": "Test Result",
|
||||
"url": "https://example.com",
|
||||
"content": "Test content",
|
||||
"engine": "google",
|
||||
"score": 0.95
|
||||
}
|
||||
],
|
||||
"number_of_results": 1,
|
||||
"engines": ["google"]
|
||||
}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_response_obj = Mock()
|
||||
mock_response_obj.status_code = 200
|
||||
mock_response_obj.json = Mock(return_value=mock_response)
|
||||
mock_response_obj.text = json.dumps(mock_response)
|
||||
mock_response_obj.raise_for_status = Mock(return_value=None)
|
||||
mock_client.get.return_value = mock_response_obj
|
||||
|
||||
with patch.object(client, '_client', mock_client):
|
||||
request = SearchRequest(q="test query", engines=["google"], page=1)
|
||||
result = await client.search(request)
|
||||
|
||||
assert result.query == "test query"
|
||||
assert len(result.results) == 1
|
||||
assert result.results[0].title == "Test Result"
|
||||
assert result.results[0].source == "google"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_http_error(self, client):
|
||||
"""Test handling of HTTP errors."""
|
||||
mock_client = AsyncMock()
|
||||
|
||||
# Create proper HTTPStatusError with async side effect
|
||||
async def raise_http_error(*args, **kwargs):
|
||||
from httpx import Request, Response
|
||||
mock_request = Mock(spec=Request)
|
||||
mock_response = Response(status_code=404, text="Not found")
|
||||
raise httpx.HTTPStatusError(
|
||||
"Not found",
|
||||
request=mock_request,
|
||||
response=mock_response
|
||||
)
|
||||
|
||||
mock_client.get.side_effect = raise_http_error
|
||||
|
||||
with patch.object(client, '_client', mock_client):
|
||||
request = SearchRequest(q="test", engines=["google"], page=1)
|
||||
with pytest.raises(SearXNGError) as exc_info:
|
||||
await client.search(request)
|
||||
|
||||
assert "HTTP error" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_connection_error(self, client):
|
||||
"""Test handling of connection errors."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get.side_effect = httpx.ConnectError("Connection refused")
|
||||
|
||||
with patch.object(client, '_client', mock_client):
|
||||
request = SearchRequest(q="test", engines=["google"], page=1)
|
||||
with pytest.raises(SearXNGError) as exc_info:
|
||||
await client.search(request)
|
||||
|
||||
assert "Request failed" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_invalid_json(self, client):
|
||||
"""Test handling of invalid JSON response."""
|
||||
mock_client = AsyncMock()
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.side_effect = json.JSONDecodeError("test", "", 0)
|
||||
mock_response.text = "invalid json"
|
||||
mock_response.raise_for_status = Mock(return_value=None)
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
with patch.object(client, '_client', mock_client):
|
||||
request = SearchRequest(q="test", engines=["google"], page=1)
|
||||
with pytest.raises(SearXNGError) as exc_info:
|
||||
await client.search(request)
|
||||
|
||||
assert "Invalid JSON" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_success(self, client):
|
||||
"""Test successful health check."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get.return_value = Response(status_code=200)
|
||||
|
||||
with patch.object(client, '_client', mock_client):
|
||||
result = await client.health_check()
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_failure(self, client):
|
||||
"""Test failed health check."""
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get.side_effect = httpx.ConnectError("Connection refused")
|
||||
|
||||
with patch.object(client, '_client', mock_client):
|
||||
result = await client.health_check()
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_build_url(self, client):
|
||||
"""Test URL building with parameters."""
|
||||
params = {"q": "test query", "format": "json", "engines": "google,bing"}
|
||||
url = client._build_url(params)
|
||||
|
||||
assert url.startswith("http://test:8080/search")
|
||||
assert "q=test+query" in url or "q=test%20query" in url
|
||||
assert "format=json" in url
|
||||
assert "engines=google%2Cbing" in url or "engines=google,bing" in url
|
||||
185
tests/unit/test_synthesizer.py
Normal file
185
tests/unit/test_synthesizer.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user