import asyncio from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock import pytest from app.services.ai_engine.base import AIEngineAdapter, AIQueryResult, EngineType def _make_result( engine_type: EngineType, query: str = "test query", has_brand: bool = False, has_competitor: bool = False, ) -> AIQueryResult: return AIQueryResult( engine_type=engine_type, query=query, raw_response="some response", citations=[], has_brand_citation=has_brand, has_competitor_citation=has_competitor, brand_context="brand context" if has_brand else None, competitor_contexts=["comp context"] if has_competitor else [], response_time_ms=100, timestamp=datetime.now(UTC), ) class _StubAdapter(AIEngineAdapter): def __init__(self, engine_type: EngineType, result: AIQueryResult | None = None, side_effect=None): super().__init__(api_key="test-key") self._engine_type = engine_type self._result = result self._side_effect = side_effect async def query(self, query: str, brand_name: str, competitor_names: list[str] | None = None) -> AIQueryResult: if self._side_effect: raise self._side_effect return self._result def get_engine_type(self) -> EngineType: return self._engine_type def _get_env_key(self) -> str | None: return "" class TestBatchQueryServiceInit: @pytest.mark.asyncio async def test_init_with_adapters(self): from app.services.ai_engine.batch_query import BatchQueryService adapters = { "chatgpt": _StubAdapter(EngineType.CHATGPT), "perplexity": _StubAdapter(EngineType.PERPLEXITY), } service = BatchQueryService(adapters) assert service.adapters is adapters assert len(service.adapters) == 2 class TestBatchQuerySingleEngine: @pytest.mark.asyncio async def test_query_single_success(self): from app.services.ai_engine.batch_query import BatchQueryService expected = _make_result(EngineType.CHATGPT, has_brand=True) adapters = {"chatgpt": _StubAdapter(EngineType.CHATGPT, result=expected)} service = BatchQueryService(adapters) result = await service.query_single( EngineType.CHATGPT, "best insurance", "BrandX", ["CompY"] ) assert result == expected assert result.engine_type == EngineType.CHATGPT assert result.has_brand_citation is True @pytest.mark.asyncio async def test_query_single_unknown_engine(self): from app.services.ai_engine.batch_query import BatchQueryService adapters = {"chatgpt": _StubAdapter(EngineType.CHATGPT)} service = BatchQueryService(adapters) with pytest.raises(ValueError, match="Unknown engine type"): await service.query_single(EngineType.KIMI, "test", "BrandX") class TestBatchQueryParallel: @pytest.mark.asyncio async def test_query_batch_multiple_engines(self): from app.services.ai_engine.batch_query import BatchQueryService r1 = _make_result(EngineType.CHATGPT, has_brand=True) r2 = _make_result(EngineType.PERPLEXITY, has_brand=False) adapters = { "chatgpt": _StubAdapter(EngineType.CHATGPT, result=r1), "perplexity": _StubAdapter(EngineType.PERPLEXITY, result=r2), } service = BatchQueryService(adapters) results = await service.query_batch( [EngineType.CHATGPT, EngineType.PERPLEXITY], "best insurance", "BrandX", ) assert len(results) == 2 engine_types = {r.engine_type for r in results} assert engine_types == {EngineType.CHATGPT, EngineType.PERPLEXITY} @pytest.mark.asyncio async def test_query_batch_partial_failure(self): from app.services.ai_engine.batch_query import BatchQueryService r1 = _make_result(EngineType.CHATGPT, has_brand=True) adapters = { "chatgpt": _StubAdapter(EngineType.CHATGPT, result=r1), "perplexity": _StubAdapter( EngineType.PERPLEXITY, side_effect=Exception("API error") ), } service = BatchQueryService(adapters) results = await service.query_batch( [EngineType.CHATGPT, EngineType.PERPLEXITY], "best insurance", "BrandX", ) assert len(results) == 1 assert results[0].engine_type == EngineType.CHATGPT @pytest.mark.asyncio async def test_query_batch_all_fail(self): from app.services.ai_engine.batch_query import BatchQueryService adapters = { "chatgpt": _StubAdapter(EngineType.CHATGPT, side_effect=Exception("err")), "perplexity": _StubAdapter(EngineType.PERPLEXITY, side_effect=Exception("err")), } service = BatchQueryService(adapters) results = await service.query_batch( [EngineType.CHATGPT, EngineType.PERPLEXITY], "test", "BrandX", ) assert results == [] class TestBatchQueryAggregation: @pytest.mark.asyncio async def test_results_aggregation(self): from app.services.ai_engine.batch_query import BatchQueryService r1 = _make_result(EngineType.CHATGPT, has_brand=True, has_competitor=False) r2 = _make_result(EngineType.PERPLEXITY, has_brand=False, has_competitor=True) r3 = _make_result(EngineType.KIMI, has_brand=True, has_competitor=True) adapters = { "chatgpt": _StubAdapter(EngineType.CHATGPT, result=r1), "perplexity": _StubAdapter(EngineType.PERPLEXITY, result=r2), "kimi": _StubAdapter(EngineType.KIMI, result=r3), } service = BatchQueryService(adapters) results = await service.query_batch( [EngineType.CHATGPT, EngineType.PERPLEXITY, EngineType.KIMI], "test", "BrandX", ["CompY"], ) assert len(results) == 3 brand_cited = [r for r in results if r.has_brand_citation] competitor_cited = [r for r in results if r.has_competitor_citation] assert len(brand_cited) == 2 assert len(competitor_cited) == 2 class TestCitationRateCalculation: def test_brand_citation_rate(self): from app.services.ai_engine.batch_query import BatchQueryService results = [ _make_result(EngineType.CHATGPT, has_brand=True), _make_result(EngineType.PERPLEXITY, has_brand=True), _make_result(EngineType.KIMI, has_brand=False), ] service = BatchQueryService({}) rate = service.calculate_citation_rate(results) assert rate["total_engines"] == 3 assert rate["brand_citation_count"] == 2 assert rate["brand_citation_rate"] == pytest.approx(2 / 3) def test_competitor_citation_rate(self): from app.services.ai_engine.batch_query import BatchQueryService results = [ _make_result(EngineType.CHATGPT, has_competitor=True), _make_result(EngineType.PERPLEXITY, has_competitor=False), _make_result(EngineType.KIMI, has_competitor=True), _make_result(EngineType.WENXIN, has_competitor=True), ] service = BatchQueryService({}) rate = service.calculate_citation_rate(results) assert rate["total_engines"] == 4 assert rate["competitor_citation_count"] == 3 assert rate["competitor_citation_rate"] == pytest.approx(3 / 4) def test_empty_results(self): from app.services.ai_engine.batch_query import BatchQueryService service = BatchQueryService({}) rate = service.calculate_citation_rate([]) assert rate["total_engines"] == 0 assert rate["brand_citation_count"] == 0 assert rate["brand_citation_rate"] == 0 assert rate["competitor_citation_count"] == 0 assert rate["competitor_citation_rate"] == 0