Files
smart-support/backend/tests/unit/analytics/test_queries.py
Yaojia Wang 19fc9f3289 test: close coverage gaps and add frontend test infrastructure
Backend (516 tests, 94% coverage):
- Add azure_openai endpoint/deployment validation tests (config.py -> 100%)
- Add _total_conversations and _avg_turns direct tests (queries.py -> 100%)
- Add transformer edge cases: list content, string checkpoint, invalid JSON,
  malformed message graceful skip (transformer.py -> 93%)
- Add safety combined status_code+error_message interaction tests
- Fix ambiguous 200/422 assertion to strict 422
- Add E2E pagination shape assertions (total, page, per_page, row count)
- Fix ReplayPool mock to respect LIMIT/OFFSET params

Frontend (23 tests, vitest + happy-dom + @testing-library/react):
- Add vitest infrastructure with happy-dom environment
- Add api.ts tests: success, HTTP error, success=false, URL encoding
- Add DashboardPage tests: loading, data, error, empty states
- Add ReplayListPage tests: loading, empty, data, error, status badge classes
- Add ReplayPage tests: loading, steps, empty, error states
2026-04-06 13:32:10 +02:00

250 lines
8.7 KiB
Python

"""Unit tests for app.analytics.queries."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
pytestmark = pytest.mark.unit
def _make_pool_with_fetchone(result: dict | None) -> MagicMock:
mock_cursor = AsyncMock()
mock_cursor.fetchone = AsyncMock(return_value=result)
mock_conn = AsyncMock()
mock_conn.execute = AsyncMock(return_value=mock_cursor)
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_conn)
mock_ctx.__aexit__ = AsyncMock(return_value=None)
mock_pool = MagicMock()
mock_pool.connection.return_value = mock_ctx
return mock_pool
def _make_pool_with_fetchall(result: list[dict]) -> MagicMock:
mock_cursor = AsyncMock()
mock_cursor.fetchall = AsyncMock(return_value=result)
mock_conn = AsyncMock()
mock_conn.execute = AsyncMock(return_value=mock_cursor)
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_conn)
mock_ctx.__aexit__ = AsyncMock(return_value=None)
mock_pool = MagicMock()
mock_pool.connection.return_value = mock_ctx
return mock_pool
class TestResolutionRate:
@pytest.mark.asyncio
async def test_returns_float(self) -> None:
from app.analytics.queries import resolution_rate
pool = _make_pool_with_fetchone({"rate": 0.85})
result = await resolution_rate(pool, range_days=7)
assert isinstance(result, float)
@pytest.mark.asyncio
async def test_zero_state_returns_zero(self) -> None:
from app.analytics.queries import resolution_rate
pool = _make_pool_with_fetchone(None)
result = await resolution_rate(pool, range_days=7)
assert result == 0.0
@pytest.mark.asyncio
async def test_returns_correct_value(self) -> None:
from app.analytics.queries import resolution_rate
pool = _make_pool_with_fetchone({"rate": 0.75})
result = await resolution_rate(pool, range_days=7)
assert result == 0.75
class TestAgentUsageQuery:
@pytest.mark.asyncio
async def test_returns_tuple(self) -> None:
from app.analytics.queries import agent_usage
pool = _make_pool_with_fetchall([])
result = await agent_usage(pool, range_days=7)
assert isinstance(result, tuple)
@pytest.mark.asyncio
async def test_empty_state_returns_empty_tuple(self) -> None:
from app.analytics.queries import agent_usage
pool = _make_pool_with_fetchall([])
result = await agent_usage(pool, range_days=7)
assert result == ()
@pytest.mark.asyncio
async def test_maps_rows_to_agent_usage_objects(self) -> None:
from app.analytics.models import AgentUsage
from app.analytics.queries import agent_usage
pool = _make_pool_with_fetchall([
{"agent": "order_agent", "count": 10, "percentage": 66.7},
{"agent": "discount_agent", "count": 5, "percentage": 33.3},
])
result = await agent_usage(pool, range_days=7)
assert len(result) == 2
assert isinstance(result[0], AgentUsage)
assert result[0].agent == "order_agent"
assert result[0].count == 10
class TestEscalationRate:
@pytest.mark.asyncio
async def test_returns_float(self) -> None:
from app.analytics.queries import escalation_rate
pool = _make_pool_with_fetchone({"rate": 0.05})
result = await escalation_rate(pool, range_days=7)
assert isinstance(result, float)
@pytest.mark.asyncio
async def test_zero_state_returns_zero(self) -> None:
from app.analytics.queries import escalation_rate
pool = _make_pool_with_fetchone(None)
result = await escalation_rate(pool, range_days=7)
assert result == 0.0
class TestCostPerConversation:
@pytest.mark.asyncio
async def test_returns_float(self) -> None:
from app.analytics.queries import cost_per_conversation
pool = _make_pool_with_fetchone({"avg_cost": 0.03})
result = await cost_per_conversation(pool, range_days=7)
assert isinstance(result, float)
@pytest.mark.asyncio
async def test_zero_state_returns_zero(self) -> None:
from app.analytics.queries import cost_per_conversation
pool = _make_pool_with_fetchone(None)
result = await cost_per_conversation(pool, range_days=7)
assert result == 0.0
class TestInterruptStatsQuery:
@pytest.mark.asyncio
async def test_returns_interrupt_stats(self) -> None:
from app.analytics.models import InterruptStats
from app.analytics.queries import interrupt_stats
pool = _make_pool_with_fetchone(
{"total": 10, "approved": 7, "rejected": 2, "expired": 1}
)
result = await interrupt_stats(pool, range_days=7)
assert isinstance(result, InterruptStats)
assert result.total == 10
assert result.approved == 7
@pytest.mark.asyncio
async def test_zero_state_returns_zeros(self) -> None:
from app.analytics.models import InterruptStats
from app.analytics.queries import interrupt_stats
pool = _make_pool_with_fetchone(None)
result = await interrupt_stats(pool, range_days=7)
assert isinstance(result, InterruptStats)
assert result.total == 0
assert result.approved == 0
assert result.rejected == 0
assert result.expired == 0
class TestTotalConversations:
@pytest.mark.asyncio
async def test_returns_count(self) -> None:
from app.analytics.queries import _total_conversations
pool = _make_pool_with_fetchone({"total": 42})
result = await _total_conversations(pool, range_days=7)
assert result == 42
@pytest.mark.asyncio
async def test_zero_state_returns_zero(self) -> None:
from app.analytics.queries import _total_conversations
pool = _make_pool_with_fetchone(None)
result = await _total_conversations(pool, range_days=7)
assert result == 0
class TestAvgTurns:
@pytest.mark.asyncio
async def test_returns_float(self) -> None:
from app.analytics.queries import _avg_turns
pool = _make_pool_with_fetchone({"avg_turns": 3.5})
result = await _avg_turns(pool, range_days=7)
assert result == 3.5
@pytest.mark.asyncio
async def test_zero_state_returns_zero(self) -> None:
from app.analytics.queries import _avg_turns
pool = _make_pool_with_fetchone(None)
result = await _avg_turns(pool, range_days=7)
assert result == 0.0
class TestGetAnalytics:
@pytest.mark.asyncio
async def test_returns_analytics_result(self) -> None:
from unittest.mock import patch
from app.analytics.models import AnalyticsResult, InterruptStats
from app.analytics.queries import get_analytics
mock_pool = MagicMock()
with (
patch("app.analytics.queries.resolution_rate", return_value=0.85),
patch("app.analytics.queries.escalation_rate", return_value=0.05),
patch("app.analytics.queries.cost_per_conversation", return_value=0.03),
patch("app.analytics.queries.agent_usage", return_value=()),
patch(
"app.analytics.queries.interrupt_stats",
return_value=InterruptStats(),
),
patch("app.analytics.queries._total_conversations", return_value=100),
patch("app.analytics.queries._avg_turns", return_value=4.2),
):
result = await get_analytics(mock_pool, range_days=7)
assert isinstance(result, AnalyticsResult)
assert result.range == "7d"
assert result.total_conversations == 100
assert result.resolution_rate == 0.85
@pytest.mark.asyncio
async def test_zero_state_returns_zeros(self) -> None:
from unittest.mock import patch
from app.analytics.models import AnalyticsResult, InterruptStats
from app.analytics.queries import get_analytics
mock_pool = MagicMock()
with (
patch("app.analytics.queries.resolution_rate", return_value=0.0),
patch("app.analytics.queries.escalation_rate", return_value=0.0),
patch("app.analytics.queries.cost_per_conversation", return_value=0.0),
patch("app.analytics.queries.agent_usage", return_value=()),
patch("app.analytics.queries.interrupt_stats", return_value=InterruptStats()),
patch("app.analytics.queries._total_conversations", return_value=0),
patch("app.analytics.queries._avg_turns", return_value=0.0),
):
result = await get_analytics(mock_pool, range_days=7)
assert isinstance(result, AnalyticsResult)
assert result.total_conversations == 0
assert result.resolution_rate == 0.0
assert result.agent_usage == ()