claude-os/tests/test_health.py
2025-12-02 15:28:25 -06:00

443 lines
16 KiB
Python

"""
Tests for health check functionality.
"""
import pytest
from unittest.mock import patch, MagicMock
from app.core.health import (
check_ollama_health,
check_sqlite_health,
wait_for_services
)
@pytest.mark.unit
class TestOllamaHealth:
"""Test Ollama health check functionality."""
@patch('app.core.health.requests.get')
def test_ollama_healthy(self, mock_get):
"""Test Ollama health check when service is healthy."""
# Mock successful response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"models": [
{"name": "llama3.1:latest"},
{"name": "nomic-embed-text"}
]
}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = check_ollama_health()
assert result["status"] == "healthy"
assert "models" in result
assert len(result["models"]) == 2
assert "llama3.1:latest" in result["models"]
assert "nomic-embed-text" in result["models"]
assert result["url"] == "http://localhost:11434"
@patch('app.core.health.requests.get')
def test_ollama_unhealthy_connection_error(self, mock_get):
"""Test Ollama health check with connection error."""
# Mock connection error
mock_get.side_effect = Exception("Connection refused")
result = check_ollama_health()
assert result["status"] == "unhealthy"
assert "error" in result
assert "Connection refused" in result["error"]
assert result["url"] == "http://localhost:11434"
@patch('app.core.health.requests.get')
def test_ollama_unhealthy_timeout(self, mock_get):
"""Test Ollama health check with timeout."""
# Mock timeout error
import requests
mock_get.side_effect = requests.exceptions.Timeout("Request timed out")
result = check_ollama_health()
assert result["status"] == "unhealthy"
assert "error" in result
assert "timed out" in result["error"].lower()
assert result["url"] == "http://localhost:11434"
@patch('app.core.health.requests.get')
def test_ollama_unhealthy_connection_refused(self, mock_get):
"""Test Ollama health check with connection refused."""
# Mock connection refused error
import requests
mock_get.side_effect = requests.exceptions.ConnectionError("Connection refused")
result = check_ollama_health()
assert result["status"] == "unhealthy"
assert "error" in result
assert "Connection refused" in result["error"]
assert result["url"] == "http://localhost:11434"
@patch('app.core.health.requests.get')
def test_ollama_unhealthy_http_error(self, mock_get):
"""Test Ollama health check with HTTP error."""
# Mock HTTP error
import requests
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("500 Server Error")
mock_get.return_value = mock_response
result = check_ollama_health()
assert result["status"] == "unhealthy"
assert "error" in result
assert "500 Server Error" in result["error"]
assert result["url"] == "http://localhost:11434"
@patch('app.core.health.requests.get')
def test_ollama_empty_models_list(self, mock_get):
"""Test Ollama health check with empty models list."""
# Mock response with no models
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"models": []}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = check_ollama_health()
assert result["status"] == "healthy"
assert "models" in result
assert len(result["models"]) == 0
@patch('app.core.health.requests.get')
def test_ollama_malformed_response(self, mock_get):
"""Test Ollama health check with malformed response."""
# Mock malformed JSON response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.side_effect = ValueError("No JSON object could be decoded")
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = check_ollama_health()
assert result["status"] == "unhealthy"
assert "error" in result
assert "No JSON object could be decoded" in result["error"]
assert result["url"] == "http://localhost:11434"
@pytest.mark.unit
class TestSQLiteHealth:
"""Test SQLite health check functionality."""
@patch('app.core.sqlite_manager.get_sqlite_manager')
def test_sqlite_healthy(self, mock_get_db):
"""Test SQLite health check when service is healthy."""
# Mock successful database connection
mock_db = MagicMock()
mock_db.list_collections.return_value = [
{"name": "kb1"},
{"name": "kb2"}
]
mock_get_db.return_value = mock_db
result = check_sqlite_health()
assert result["status"] == "healthy"
assert result["collections"] == 2
@patch('app.core.sqlite_manager.get_sqlite_manager')
def test_sqlite_unhealthy(self, mock_get_db):
"""Test SQLite health check when service is unhealthy."""
# Mock database connection error
mock_get_db.side_effect = Exception("Connection failed")
result = check_sqlite_health()
assert result["status"] == "unhealthy"
assert "error" in result
assert "Database access failed" in result["error"]
@patch('app.core.sqlite_manager.get_sqlite_manager')
def test_sqlite_empty_database(self, mock_get_db):
"""Test SQLite health check with empty database."""
# Mock empty database
mock_db = MagicMock()
mock_db.list_collections.return_value = []
mock_get_db.return_value = mock_db
result = check_sqlite_health()
assert result["status"] == "healthy"
assert result["collections"] == 0
@patch('app.core.sqlite_manager.get_sqlite_manager')
def test_sqlite_list_collections_error(self, mock_get_db):
"""Test SQLite health check when list_collections fails."""
# Mock list_collections error
mock_db = MagicMock()
mock_db.list_collections.side_effect = Exception("Query failed")
mock_get_db.return_value = mock_db
result = check_sqlite_health()
assert result["status"] == "unhealthy"
assert "error" in result
assert "Query failed" in result["error"]
@pytest.mark.unit
class TestWaitForServices:
"""Test wait_for_services functionality."""
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_immediate_success(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test waiting for services when they're immediately healthy."""
# Mock healthy services
mock_ollama_check.return_value = {"status": "healthy"}
mock_sqlite_check.return_value = {"status": "healthy"}
result = wait_for_services(max_retries=3, delay=0.1)
assert result is True
# Should not need to sleep
mock_sleep.assert_not_called()
# Should check services once
mock_ollama_check.assert_called_once()
mock_sqlite_check.assert_called_once()
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_eventual_success(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test waiting for services that become healthy after retries."""
# Mock services that fail first, then succeed
mock_ollama_check.side_effect = [
{"status": "unhealthy"},
{"status": "unhealthy"},
{"status": "healthy"}
]
mock_sqlite_check.return_value = {"status": "healthy"}
result = wait_for_services(max_retries=3, delay=0.1)
assert result is True
# Should sleep between retries
assert mock_sleep.call_count == 2 # After first and second attempts
# Should check services multiple times
assert mock_ollama_check.call_count == 3
assert mock_sqlite_check.call_count == 3
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_timeout(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test waiting for services that never become healthy."""
# Mock services that always fail
mock_ollama_check.return_value = {"status": "unhealthy"}
mock_sqlite_check.return_value = {"status": "unhealthy"}
result = wait_for_services(max_retries=3, delay=0.1)
assert result is False
# Should sleep between retries
assert mock_sleep.call_count == 2 # After first and second attempts
# Should check services max_retries times
assert mock_ollama_check.call_count == 3
assert mock_sqlite_check.call_count == 3
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_partial_failure(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test waiting when one service is always unhealthy."""
# Mock Ollama always fails, SQLite succeeds
mock_ollama_check.return_value = {"status": "unhealthy"}
mock_sqlite_check.return_value = {"status": "healthy"}
result = wait_for_services(max_retries=3, delay=0.1)
assert result is False
# Should still check both services each time
assert mock_ollama_check.call_count == 3
assert mock_sqlite_check.call_count == 3
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_default_parameters(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test wait_for_services with default parameters."""
# Mock healthy services
mock_ollama_check.return_value = {"status": "healthy"}
mock_sqlite_check.return_value = {"status": "healthy"}
result = wait_for_services()
assert result is True
# Should not sleep with immediate success
mock_sleep.assert_not_called()
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_logging(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test that wait_for_services logs progress."""
# Mock services that fail then succeed
mock_ollama_check.side_effect = [
{"status": "unhealthy"},
{"status": "healthy"}
]
mock_sqlite_check.return_value = {"status": "healthy"}
with patch('app.core.health.logger.info') as mock_logger:
wait_for_services(max_retries=2, delay=0.1)
# Should log progress
assert mock_logger.call_count >= 1
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_error_during_check(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test wait_for_services when health check raises exception."""
# Mock health check that raises exception - but the function catches it
# and returns unhealthy status
mock_ollama_check.return_value = {"status": "unhealthy"}
mock_sqlite_check.return_value = {"status": "healthy"}
result = wait_for_services(max_retries=2, delay=0.1)
# Should return False when one service is unhealthy
assert result is False
# Should still attempt all retries
assert mock_ollama_check.call_count == 2
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_custom_delay(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test wait_for_services with custom delay."""
# Mock services that need retries
mock_ollama_check.side_effect = [
{"status": "unhealthy"},
{"status": "healthy"}
]
mock_sqlite_check.return_value = {"status": "healthy"}
result = wait_for_services(max_retries=2, delay=0.5)
assert result is True
# Should use custom delay
mock_sleep.assert_called_once_with(0.5)
@patch('app.core.health.check_ollama_health')
@patch('app.core.health.check_sqlite_health')
@patch('app.core.health.time.sleep')
def test_wait_for_services_custom_retries(self, mock_sleep, mock_sqlite_check, mock_ollama_check):
"""Test wait_for_services with custom retry count."""
# Mock services that need all retries
mock_ollama_check.side_effect = [
{"status": "unhealthy"},
{"status": "unhealthy"},
{"status": "unhealthy"},
{"status": "healthy"}
]
mock_sqlite_check.return_value = {"status": "healthy"}
result = wait_for_services(max_retries=4, delay=0.1)
assert result is True
# Should use custom retry count
assert mock_ollama_check.call_count == 4
assert mock_sqlite_check.call_count == 4
assert mock_sleep.call_count == 3
@pytest.mark.integration
class TestHealthIntegration:
"""Integration tests for health checks."""
@patch('app.core.health.requests.get')
def test_real_ollama_check_structure(self, mock_get):
"""Test that Ollama check has correct structure."""
# Mock realistic response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"models": [
{
"name": "llama3.1:latest",
"modified_at": "2023-01-01T00:00:00Z",
"size": 4000000000
},
{
"name": "nomic-embed-text",
"modified_at": "2023-01-01T00:00:00Z",
"size": 100000000
}
]
}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = check_ollama_health()
# Check response structure
assert "status" in result
assert "models" in result
assert "url" in result
assert isinstance(result["models"], list)
assert len(result["models"]) == 2
@patch('app.core.sqlite_manager.get_sqlite_manager')
def test_real_sqlite_check_structure(self, mock_get_db):
"""Test that SQLite check has correct structure."""
# Mock realistic database
mock_db = MagicMock()
mock_db.list_collections.return_value = [
{"name": "kb1", "type": "generic"},
{"name": "kb2", "type": "code"},
{"name": "kb3", "type": "documentation"}
]
mock_get_db.return_value = mock_db
result = check_sqlite_health()
# Check response structure
assert "status" in result
assert "collections" in result
assert isinstance(result["collections"], int)
assert result["collections"] == 3
@patch('app.core.health.requests.get')
def test_health_check_error_handling(self, mock_get):
"""Test that health checks handle errors gracefully."""
# Mock connection error
mock_get.side_effect = Exception("Connection failed")
result = check_ollama_health()
# Should handle error gracefully
assert result["status"] == "unhealthy"
assert "error" in result
def test_health_check_timeout_configuration(self):
"""Test that health checks respect timeout configuration."""
with patch('app.core.health.requests.get') as mock_get:
# Mock timeout
import requests
mock_get.side_effect = requests.exceptions.Timeout("Request timed out")
result = check_ollama_health()
# Should handle timeout
assert result["status"] == "unhealthy"
assert "timed out" in result["error"].lower()