File size: 4,410 Bytes
2f073d3 5d50b8b 2f073d3 5d50b8b 2f073d3 5d50b8b 2f073d3 5d50b8b 2f073d3 5d50b8b 2f073d3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | """
Integration tests for HF and Groq service modules (mocked).
Tests retry behavior and error handling.
"""
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
import httpx
from backend.app.services.hf_service import detect_ai_text, get_embeddings, detect_harm
from backend.app.services.groq_service import compute_perplexity
class TestHFService:
@pytest.mark.asyncio
@patch("backend.app.services.hf_service._hf_post", new_callable=AsyncMock)
async def test_detect_ai_text_success(self, mock_post):
mock_post.return_value = [[
{"label": "AI", "score": 0.92},
{"label": "Human", "score": 0.08},
]]
score = await detect_ai_text("Test text for detection")
assert 0.0 <= score <= 1.0
@pytest.mark.asyncio
@patch("backend.app.services.hf_service._hf_post", new_callable=AsyncMock)
async def test_detect_ai_text_fallback(self, mock_post):
"""If primary fails immediately (non-retried error), fallback URL should succeed."""
# Use ConnectError so tenacity does NOT retry (only HTTPStatusError/ConnectError
# with stop_after_attempt=3 would retry, but we want ONE failure then fallback).
# Actually tenacity retries on ConnectError too, so we use a plain Exception
# which is NOT in the retry predicate — it propagates immediately, letting
# detect_ai_text catch it in its try/except and move to the fallback URL.
mock_post.side_effect = [
Exception("Primary failed"), # primary URL -> caught, move on
[[{"label": "FAKE", "score": 0.75}]], # fallback URL -> success
]
with patch("backend.app.services.hf_service.settings") as mock_settings:
mock_settings.HF_DETECTOR_PRIMARY = "https://primary.example.com"
mock_settings.HF_DETECTOR_FALLBACK = "https://fallback.example.com"
score = await detect_ai_text("Test text")
assert 0.0 <= score <= 1.0
@pytest.mark.asyncio
@patch("backend.app.services.hf_service._hf_post", new_callable=AsyncMock)
async def test_get_embeddings_success(self, mock_post):
"""Mock returns a 768-dim vector; assert we get back exactly that vector."""
mock_post.return_value = [0.1] * 768
with patch("backend.app.services.hf_service.settings") as mock_settings:
mock_settings.HF_EMBEDDINGS_PRIMARY = "https://embeddings.example.com"
mock_settings.HF_EMBEDDINGS_FALLBACK = ""
result = await get_embeddings("Test text")
assert len(result) == 768
@pytest.mark.asyncio
@patch("backend.app.services.hf_service._hf_post", new_callable=AsyncMock)
async def test_detect_harm_success(self, mock_post):
mock_post.return_value = [[
{"label": "hate", "score": 0.15},
{"label": "not_hate", "score": 0.85},
]]
score = await detect_harm("Test text")
assert 0.0 <= score <= 1.0
class TestGroqService:
@pytest.mark.asyncio
@patch("backend.app.services.groq_service._groq_chat_completion", new_callable=AsyncMock)
async def test_compute_perplexity_with_logprobs(self, mock_groq):
mock_groq.return_value = {
"choices": [{
"logprobs": {
"content": [
{"logprob": -2.5},
{"logprob": -1.8},
{"logprob": -3.2},
]
}
}]
}
score = await compute_perplexity("Test text for perplexity")
assert score is not None
assert 0.0 <= score <= 1.0
@pytest.mark.asyncio
@patch("backend.app.services.groq_service._groq_chat_completion", new_callable=AsyncMock)
async def test_compute_perplexity_no_logprobs(self, mock_groq):
mock_groq.return_value = {
"choices": [{}],
"usage": {"prompt_tokens": 15},
}
score = await compute_perplexity("Test text without logprobs available")
assert score is None or 0.0 <= score <= 1.0
@pytest.mark.asyncio
@patch("backend.app.services.groq_service._groq_chat_completion", new_callable=AsyncMock)
async def test_compute_perplexity_error(self, mock_groq):
mock_groq.side_effect = Exception("Groq API error")
score = await compute_perplexity("Test text")
assert score is None
|