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