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

694 lines
24 KiB
Python

"""
Tests for Redis configuration functionality.
"""
import pytest
import json
import time
from unittest.mock import patch, MagicMock
from app.core.redis_config import RedisConfig
@pytest.fixture(autouse=True)
def reset_redis_singleton():
"""Reset Redis singleton before each test."""
RedisConfig._redis_instance = None
yield
RedisConfig._redis_instance = None
@pytest.mark.unit
class TestRedisConfig:
"""Test RedisConfig class."""
def test_redis_config_constants(self):
"""Test Redis configuration constants."""
assert RedisConfig.REDIS_HOST == "localhost"
assert RedisConfig.REDIS_PORT == 6379
assert RedisConfig.REDIS_DB == 0
assert RedisConfig.REDIS_PASSWORD is None
assert RedisConfig.CONVERSATION_CHANNEL_TEMPLATE == "claude-os:conversation:{project_id}"
assert RedisConfig.LEARNING_CHANNEL == "claude-os:learning"
assert RedisConfig.NOTIFICATION_CHANNEL == "claude-os:notifications"
assert RedisConfig.LEARNING_QUEUE == "claude-os:learning"
assert RedisConfig.PROMPT_QUEUE == "claude-os:prompts"
assert RedisConfig.INGEST_QUEUE == "claude-os:ingest"
assert RedisConfig.PROMPT_KEY_TEMPLATE == "claude_os:prompt:{project_id}:{detection_id}"
assert RedisConfig.CONFIRMATION_KEY_TEMPLATE == "claude_os:prompt:{project_id}:{detection_id}:confirmed"
assert RedisConfig.WATCHED_MESSAGES_KEY_TEMPLATE == "claude_os:watched:{project_id}"
@patch('app.core.redis_config.Redis')
def test_get_redis_singleton(self, mock_redis_class):
"""Test that get_redis returns singleton."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
# Get instance twice
redis1 = RedisConfig.get_redis()
redis2 = RedisConfig.get_redis()
# Should create only once
mock_redis_class.assert_called_once()
assert redis1 is redis2
assert redis1 is mock_redis
@patch('app.core.redis_config.Redis')
def test_get_redis_connection_parameters(self, mock_redis_class):
"""Test that get_redis uses correct connection parameters."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
redis = RedisConfig.get_redis()
# Should create with correct parameters
mock_redis_class.assert_called_once_with(
host="localhost",
port=6379,
db=0,
password=None,
decode_responses=True
)
@patch('app.core.redis_config.Redis')
def test_get_redis_connection_failure(self, mock_redis_class):
"""Test that get_redis handles connection failure."""
mock_redis = MagicMock()
mock_redis.ping.side_effect = Exception("Connection failed")
mock_redis_class.return_value = mock_redis
with pytest.raises(Exception, match="Connection failed"):
RedisConfig.get_redis()
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_get_learning_queue(self, mock_redis_class, mock_queue_class):
"""Test getting learning queue."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
queue = RedisConfig.get_learning_queue()
# Should create queue with correct parameters
mock_queue_class.assert_called_once_with(
"claude-os:learning",
connection=mock_redis
)
assert queue is mock_queue
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_get_prompt_queue(self, mock_redis_class, mock_queue_class):
"""Test getting prompt queue."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
queue = RedisConfig.get_prompt_queue()
# Should create queue with correct parameters
mock_queue_class.assert_called_once_with(
"claude-os:prompts",
connection=mock_redis
)
assert queue is mock_queue
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_get_ingest_queue(self, mock_redis_class, mock_queue_class):
"""Test getting ingest queue."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
queue = RedisConfig.get_ingest_queue()
# Should create queue with correct parameters
mock_queue_class.assert_called_once_with(
"claude-os:ingest",
connection=mock_redis
)
assert queue is mock_queue
@patch('app.core.redis_config.Redis')
def test_publish_conversation(self, mock_redis_class):
"""Test publishing conversation message."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
# Get redis to initialize it
RedisConfig.get_redis()
# Publish message
project_id = 123
role = "user"
text = "Test message"
RedisConfig.publish_conversation(project_id, role, text)
# Should publish to correct channel
mock_redis.publish.assert_called_once()
call_args = mock_redis.publish.call_args
assert call_args[0][0] == "claude-os:conversation:123"
# Check message format
message = json.loads(call_args[0][1])
assert message["role"] == role
assert message["text"] == text
assert "timestamp" in message
@patch('app.core.redis_config.Redis')
def test_subscribe_to_conversation(self, mock_redis_class):
"""Test subscribing to conversation messages."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_pubsub = MagicMock()
mock_redis.pubsub.return_value = mock_pubsub
# Get redis to initialize it
RedisConfig.get_redis()
# Subscribe to conversation
project_id = 123
pubsub = RedisConfig.subscribe_to_conversation(project_id)
# Should subscribe to correct channel
mock_redis.pubsub.assert_called_once()
mock_pubsub.subscribe.assert_called_once_with("claude-os:conversation:123")
assert pubsub is mock_pubsub
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_queue_learning_job(self, mock_redis_class, mock_queue_class):
"""Test queuing learning job."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.return_value = MagicMock(id="job_123")
# Queue job
project_id = 123
detection = {"id": "detection_123", "trigger": "switching"}
job_id = RedisConfig.queue_learning_job(project_id, detection)
# Should enqueue with correct parameters
mock_queue.enqueue.assert_called_once_with(
"app.core.learning_jobs.process_learning_detection",
project_id,
detection,
job_timeout="5m"
)
assert job_id == "job_123"
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_queue_prompt_job(self, mock_redis_class, mock_queue_class):
"""Test queuing prompt job."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.return_value = MagicMock(id="prompt_job_123")
# Queue prompt job
project_id = 123
detection = {"id": "detection_123", "trigger": "switching"}
job_id = RedisConfig.queue_prompt_job(project_id, detection)
# Should enqueue with correct parameters
mock_queue.enqueue.assert_called_once_with(
"app.core.learning_jobs.prompt_user_for_confirmation",
project_id,
detection,
job_timeout="10m"
)
assert job_id == "prompt_job_123"
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_queue_ingest_job(self, mock_redis_class, mock_queue_class):
"""Test queuing ingest job."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.return_value = MagicMock(id="ingest_job_123")
# Queue ingest job
project_id = 123
file_path = "/path/to/file"
job_id = RedisConfig.queue_ingest_job(project_id, file_path)
# Should enqueue with correct parameters
mock_queue.enqueue.assert_called_once_with(
"app.core.learning_jobs.ingest_to_mcp",
project_id,
file_path,
job_timeout="5m"
)
assert job_id == "ingest_job_123"
@patch('app.core.redis_config.Redis')
def test_set_confirmation(self, mock_redis_class):
"""Test setting user confirmation."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
# Get redis to initialize it
RedisConfig.get_redis()
# Set confirmation
project_id = 123
detection_id = "detection_123"
confirmed = True
RedisConfig.set_confirmation(project_id, detection_id, confirmed)
# Should set with TTL
mock_redis.setex.assert_called_once_with(
"claude_os:prompt:123:detection_123:confirmed",
600, # 10 minutes
"True"
)
@patch('app.core.redis_config.Redis')
def test_set_confirmation_false(self, mock_redis_class):
"""Test setting user confirmation to false."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
# Get redis to initialize it
RedisConfig.get_redis()
# Set confirmation to false
project_id = 123
detection_id = "detection_123"
confirmed = False
RedisConfig.set_confirmation(project_id, detection_id, confirmed)
# Should set with TTL
mock_redis.setex.assert_called_once_with(
"claude_os:prompt:123:detection_123:confirmed",
600, # 10 minutes
"False"
)
@patch('app.core.redis_config.Redis')
def test_get_confirmation_exists(self, mock_redis_class):
"""Test getting confirmation when it exists."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_redis.get.return_value = "True"
# Get redis to initialize it
RedisConfig.get_redis()
# Get confirmation
project_id = 123
detection_id = "detection_123"
result = RedisConfig.get_confirmation(project_id, detection_id)
# Should get from Redis
mock_redis.get.assert_called_once_with(
"claude_os:prompt:123:detection_123:confirmed"
)
assert result is True
@patch('app.core.redis_config.Redis')
def test_get_confirmation_not_exists(self, mock_redis_class):
"""Test getting confirmation when it doesn't exist."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_redis.get.return_value = None
# Get redis to initialize it
RedisConfig.get_redis()
# Get confirmation
project_id = 123
detection_id = "nonexistent_detection"
result = RedisConfig.get_confirmation(project_id, detection_id)
# Should get from Redis
mock_redis.get.assert_called_once_with(
"claude_os:prompt:123:nonexistent_detection:confirmed"
)
assert result is None
@patch('app.core.redis_config.Redis')
def test_get_confirmation_string_value(self, mock_redis_class):
"""Test getting confirmation when stored as string."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_redis.get.return_value = "true" # Lowercase string
# Get redis to initialize it
RedisConfig.get_redis()
# Get confirmation
project_id = 123
detection_id = "detection_123"
result = RedisConfig.get_confirmation(project_id, detection_id)
# Should handle string value
mock_redis.get.assert_called_once_with(
"claude_os:prompt:123:detection_123:confirmed"
)
assert result is True
@patch('app.core.redis_config.Redis')
def test_get_confirmation_false_string_value(self, mock_redis_class):
"""Test getting confirmation when stored as false string."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_redis.get.return_value = "false" # Lowercase string
# Get redis to initialize it
RedisConfig.get_redis()
# Get confirmation
project_id = 123
detection_id = "detection_123"
result = RedisConfig.get_confirmation(project_id, detection_id)
# Should handle string value
mock_redis.get.assert_called_once_with(
"claude_os:prompt:123:detection_123:confirmed"
)
assert result is False
@pytest.mark.integration
class TestRedisConfigIntegration:
"""Integration tests for Redis configuration."""
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_full_workflow(self, mock_redis_class, mock_queue_class):
"""Test full Redis workflow with all operations."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
# Configure get to return values for confirmations
mock_redis.get.side_effect = ["False", "True"]
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.return_value = MagicMock(id="job_123")
# Initialize redis
RedisConfig.get_redis()
# Test full workflow
project_id = 123
detection = {"id": "detection_123", "trigger": "switching"}
# 1. Publish conversation
RedisConfig.publish_conversation(project_id, "user", "Test message")
# 2. Queue learning job
job_id = RedisConfig.queue_learning_job(project_id, detection)
# 3. Set initial confirmation
RedisConfig.set_confirmation(project_id, "detection_123", False)
# 4. Get confirmation (should be False initially)
result = RedisConfig.get_confirmation(project_id, "detection_123")
assert result is False
# 5. Update confirmation
RedisConfig.set_confirmation(project_id, "detection_123", True)
# 6. Get confirmation (should be True now)
result = RedisConfig.get_confirmation(project_id, "detection_123")
assert result is True
# Verify all Redis calls
assert mock_redis.publish.call_count == 1
assert mock_queue.enqueue.call_count == 1
assert mock_redis.setex.call_count == 2
assert mock_redis.get.call_count == 2
@patch('app.core.redis_config.Redis')
def test_connection_error_handling(self, mock_redis_class):
"""Test Redis connection error handling."""
mock_redis = MagicMock()
mock_redis.ping.side_effect = Exception("Connection failed")
mock_redis_class.return_value = mock_redis
with pytest.raises(Exception, match="Connection failed"):
RedisConfig.get_redis()
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_queue_error_handling(self, mock_redis_class, mock_queue_class):
"""Test Redis queue error handling."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.side_effect = Exception("Queue error")
# Initialize redis
RedisConfig.get_redis()
# Should handle queue error gracefully
with pytest.raises(Exception, match="Queue error"):
RedisConfig.queue_learning_job(123, {"test": "data"})
@patch('app.core.redis_config.Redis')
def test_publish_error_handling(self, mock_redis_class):
"""Test Redis publish error handling."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_redis.publish.side_effect = Exception("Publish error")
# Initialize redis
RedisConfig.get_redis()
# Should handle publish error gracefully
with pytest.raises(Exception, match="Publish error"):
RedisConfig.publish_conversation(123, "user", "test")
@patch('app.core.redis_config.Redis')
def test_set_get_confirmation_error_handling(self, mock_redis_class):
"""Test Redis set/get confirmation error handling."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_redis.setex.side_effect = Exception("Set error")
mock_redis.get.side_effect = Exception("Get error")
# Initialize redis
RedisConfig.get_redis()
# Should handle set error gracefully
with pytest.raises(Exception, match="Set error"):
RedisConfig.set_confirmation(123, "detection_123", True)
# Should handle get error gracefully
with pytest.raises(Exception, match="Get error"):
RedisConfig.get_confirmation(123, "detection_123")
@patch('app.core.redis_config.Redis')
def test_subscribe_error_handling(self, mock_redis_class):
"""Test Redis subscribe error handling."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_redis.pubsub.side_effect = Exception("Pubsub error")
# Initialize redis
RedisConfig.get_redis()
# Should handle pubsub error gracefully
with pytest.raises(Exception, match="Pubsub error"):
RedisConfig.subscribe_to_conversation(123)
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_queue_job_timeout_configuration(self, mock_redis_class, mock_queue_class):
"""Test that queue jobs use correct timeout configuration."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.return_value = MagicMock(id="job_123")
# Initialize redis
RedisConfig.get_redis()
# Queue learning job
RedisConfig.queue_learning_job(123, {"test": "data"})
# Should use 5m timeout
mock_queue.enqueue.assert_called_once_with(
"app.core.learning_jobs.process_learning_detection",
123,
{"test": "data"},
job_timeout="5m"
)
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_queue_prompt_job_timeout_configuration(self, mock_redis_class, mock_queue_class):
"""Test that prompt queue jobs use correct timeout configuration."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.return_value = MagicMock(id="job_123")
# Initialize redis
RedisConfig.get_redis()
# Queue prompt job
RedisConfig.queue_prompt_job(123, {"test": "data"})
# Should use 10m timeout
mock_queue.enqueue.assert_called_once_with(
"app.core.learning_jobs.prompt_user_for_confirmation",
123,
{"test": "data"},
job_timeout="10m"
)
@patch('app.core.redis_config.Queue')
@patch('app.core.redis_config.Redis')
def test_queue_ingest_job_timeout_configuration(self, mock_redis_class, mock_queue_class):
"""Test that ingest queue jobs use correct timeout configuration."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_queue = MagicMock()
mock_queue_class.return_value = mock_queue
mock_queue.enqueue.return_value = MagicMock(id="job_123")
# Initialize redis
RedisConfig.get_redis()
# Queue ingest job
RedisConfig.queue_ingest_job(123, "/path/to/file")
# Should use 5m timeout
mock_queue.enqueue.assert_called_once_with(
"app.core.learning_jobs.ingest_to_mcp",
123,
"/path/to/file",
job_timeout="5m"
)
@patch('app.core.redis_config.Redis')
def test_set_confirmation_ttl_configuration(self, mock_redis_class):
"""Test that set confirmation uses correct TTL configuration."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
# Initialize redis
RedisConfig.get_redis()
# Set confirmation
RedisConfig.set_confirmation(123, "detection_123", True)
# Should use 600 seconds TTL (10 minutes)
mock_redis.setex.assert_called_once_with(
"claude_os:prompt:123:detection_123:confirmed",
600, # 10 minutes
"True"
)
@patch('app.core.redis_config.Redis')
def test_publish_conversation_message_format(self, mock_redis_class):
"""Test that publish conversation uses correct message format."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
# Initialize redis
RedisConfig.get_redis()
# Publish message
project_id = 123
role = "user"
text = "Test message"
RedisConfig.publish_conversation(project_id, role, text)
# Check message format
call_args = mock_redis.publish.call_args
message = json.loads(call_args[0][1])
assert message["role"] == role
assert message["text"] == text
assert "timestamp" in message
assert isinstance(message["timestamp"], str)
@patch('app.core.redis_config.Redis')
def test_subscribe_to_conversation_channel_format(self, mock_redis_class):
"""Test that subscribe uses correct channel format."""
mock_redis = MagicMock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
mock_pubsub = MagicMock()
mock_redis.pubsub.return_value = mock_pubsub
# Initialize redis
RedisConfig.get_redis()
# Subscribe to conversation
project_id = 123
pubsub = RedisConfig.subscribe_to_conversation(project_id)
# Should subscribe to correct channel
mock_redis.pubsub.assert_called_once()
mock_pubsub.subscribe.assert_called_once_with("claude-os:conversation:123")