""" Tests for Ollama integration service. """ import pytest import json from unittest.mock import AsyncMock, patch, MagicMock from services.ollama_service import OllamaService @pytest.mark.asyncio async def test_ollama_service_initialization(): """Test OllamaService initializes with correct defaults.""" service = OllamaService() assert service.base_url == "http://localhost:11434" assert service.model == "llama3:8b" assert service.timeout == 30.0 @pytest.mark.asyncio async def test_ollama_service_custom_base_url(): """Test OllamaService accepts custom base URL.""" custom_url = "http://custom-ollama:11434" service = OllamaService(base_url=custom_url) assert service.base_url == custom_url @pytest.mark.asyncio async def test_sample_text_short(): """Test sampling short text returns full text.""" service = OllamaService() text = "Short text" samples = service._sample_text(text, chunk_size=5000, num_samples=4) assert len(samples) == 1 assert samples[0] == text @pytest.mark.asyncio async def test_sample_text_long(): """Test sampling long text returns multiple samples.""" service = OllamaService() # Create text with position markers so we can verify different samples text = "".join([f"Position{i:05d} " for i in range(2500)]) # ~25k characters samples = service._sample_text(text, chunk_size=5000, num_samples=4) assert len(samples) == 4 assert all(len(s) <= 5000 for s in samples) # Samples should be from different parts - check they contain different position markers assert "Position00000" in samples[0] # First sample should have early positions assert "Position00000" not in samples[-1] # Last sample should have later positions @pytest.mark.asyncio async def test_sample_text_medium(): """Test sampling medium text returns appropriate number of samples.""" service = OllamaService() text = "B" * 8000 # 8k characters samples = service._sample_text(text, chunk_size=5000, num_samples=4) # Should get 2 samples (8000 / 5000 = 1.6, so 2 samples) assert len(samples) >= 1 assert all(len(s) <= 5000 for s in samples) @pytest.mark.asyncio async def test_build_prompt(): """Test prompt builder creates correct format.""" service = OllamaService() text = "Alice: Hello there!\nBob: Hi Alice!" prompt = service._build_prompt(text) assert "speakers" in prompt assert "JSON" in prompt assert text in prompt @pytest.mark.asyncio async def test_parse_response_with_speakers_key(): """Test parsing JSON response with 'speakers' key.""" service = OllamaService() response = '{"speakers": ["Alice", "Bob", "Narrator"]}' speakers = service._parse_response(response) assert speakers == ["Alice", "Bob", "Narrator"] @pytest.mark.asyncio async def test_parse_response_with_characters_key(): """Test parsing JSON response with 'characters' key.""" service = OllamaService() response = '{"characters": ["Alice", "Bob"]}' speakers = service._parse_response(response) assert speakers == ["Alice", "Bob"] @pytest.mark.asyncio async def test_parse_response_with_roles_key(): """Test parsing JSON response with 'roles' key.""" service = OllamaService() response = '{"roles": ["Narrator", "Alice"]}' speakers = service._parse_response(response) assert speakers == ["Narrator", "Alice"] @pytest.mark.asyncio async def test_parse_response_with_array(): """Test parsing JSON response that is a direct array.""" service = OllamaService() response = '["Alice", "Bob"]' speakers = service._parse_response(response) assert speakers == ["Alice", "Bob"] @pytest.mark.asyncio async def test_parse_response_empty(): """Test parsing empty response.""" service = OllamaService() speakers = service._parse_response("") assert speakers == [] @pytest.mark.asyncio async def test_parse_response_invalid_json(): """Test parsing invalid JSON returns empty list.""" service = OllamaService() response = "This is not JSON" speakers = service._parse_response(response) assert speakers == [] @pytest.mark.asyncio async def test_detect_speakers_success(): """Test successful speaker detection with short text.""" service = OllamaService() text = "Alice: Hello!\nBob: Hi there!" mock_response = MagicMock() mock_response.json.return_value = { "response": '{"speakers": ["Alice", "Bob"]}' } mock_response.raise_for_status = MagicMock() with patch('httpx.AsyncClient') as mock_client: mock_client.return_value.__aenter__.return_value.post = AsyncMock(return_value=mock_response) speakers = await service.detect_speakers(text) assert set(speakers) == {"Alice", "Bob"} @pytest.mark.asyncio async def test_detect_speakers_multiple_samples(): """Test speaker detection with long text requiring multiple samples.""" service = OllamaService() text = "A" * 25000 # Long text requiring multiple samples # Mock responses for different samples responses = [ MagicMock(json=lambda: {"response": '{"speakers": ["Alice", "Bob"]}'}), MagicMock(json=lambda: {"response": '{"speakers": ["Charlie", "Bob"]}'}), MagicMock(json=lambda: {"response": '{"speakers": ["Alice", "David"]}'}), MagicMock(json=lambda: {"response": '{"speakers": ["Eve"]}'}), ] for r in responses: r.raise_for_status = MagicMock() with patch('httpx.AsyncClient') as mock_client: mock_client.return_value.__aenter__.return_value.post = AsyncMock(side_effect=responses) speakers = await service.detect_speakers(text) # Should deduplicate across all samples assert set(speakers) == {"Alice", "Bob", "Charlie", "David", "Eve"} @pytest.mark.asyncio async def test_detect_speakers_timeout(): """Test speaker detection handles timeout.""" service = OllamaService() text = "Some text" with patch('httpx.AsyncClient') as mock_client: import httpx mock_client.return_value.__aenter__.return_value.post = AsyncMock( side_effect=httpx.TimeoutException("Timeout") ) with pytest.raises(Exception, match="timed out"): await service.detect_speakers(text) @pytest.mark.asyncio async def test_detect_speakers_http_error(): """Test speaker detection handles HTTP errors.""" service = OllamaService() text = "Some text" with patch('httpx.AsyncClient') as mock_client: import httpx mock_client.return_value.__aenter__.return_value.post = AsyncMock( side_effect=httpx.HTTPError("Connection failed") ) with pytest.raises(Exception, match="Ollama service error"): await service.detect_speakers(text) @pytest.mark.asyncio async def test_is_available_success(): """Test is_available returns True when service responds.""" service = OllamaService() mock_response = MagicMock() mock_response.status_code = 200 with patch('httpx.AsyncClient') as mock_client: mock_client.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response) available = await service.is_available() assert available is True @pytest.mark.asyncio async def test_is_available_failure(): """Test is_available returns False when service is down.""" service = OllamaService() with patch('httpx.AsyncClient') as mock_client: import httpx mock_client.return_value.__aenter__.return_value.get = AsyncMock( side_effect=httpx.ConnectError("Connection refused") ) available = await service.is_available() assert available is False