Skip to main content
Glama

Rootly MCP server

Official
test_smart_utils.py18 kB
""" Unit tests for smart utilities functionality. Tests the text similarity analysis, solution extraction, and AI-powered incident analysis features. """ from unittest.mock import patch, MagicMock from rootly_mcp_server.smart_utils import ( TextSimilarityAnalyzer, SolutionExtractor, IncidentSimilarity ) class TestTextSimilarityAnalyzer: """Test the TextSimilarityAnalyzer class.""" def setup_method(self): """Set up test fixtures.""" self.analyzer = TextSimilarityAnalyzer() def test_preprocess_text(self): """Test text preprocessing functionality.""" # Test basic preprocessing result = self.analyzer.preprocess_text("Payment API returning 500 errors!") expected = "payment api returning 500 errors" assert result == expected # Test with special characters and multiple spaces result = self.analyzer.preprocess_text("Auth service is down - users can't login") expected = "auth service down users can login" assert result == expected # Test empty text result = self.analyzer.preprocess_text("") assert result == "" # Test None input result = self.analyzer.preprocess_text(None) assert result == "" def test_extract_services(self): """Test service name extraction from text.""" # Test hyphenated service names text = "payment-service is throwing errors" services = self.analyzer.extract_services(text) assert "payment" in services # Test API services text = "authapi connection timeout" services = self.analyzer.extract_services(text) assert "auth" in services # Test dotted service names text = "Error connecting to user.service" services = self.analyzer.extract_services(text) assert "user" in services # Test multiple services text = "payment-api and auth-service are both down" services = self.analyzer.extract_services(text) assert "payment" in services assert "auth" in services def test_extract_error_patterns(self): """Test error pattern extraction.""" # Test HTTP status codes text = "API returning 500 internal server error" patterns = self.analyzer.extract_error_patterns(text) assert "http-500" in patterns # Test database errors text = "Database connection timeout occurred" patterns = self.analyzer.extract_error_patterns(text) assert "database-error" in patterns # Test memory errors text = "Out of memory error in the application" patterns = self.analyzer.extract_error_patterns(text) assert "resource-error" in patterns # Test network errors text = "Network unreachable error" patterns = self.analyzer.extract_error_patterns(text) assert "network-error" in patterns def test_combine_incident_text(self): """Test incident text combination.""" incident = { "attributes": { "title": "Payment API Error", "summary": "Users cannot checkout", "description": "500 errors from payment service" } } result = self.analyzer._combine_incident_text(incident) assert "payment api error" in result assert "users cannot checkout" in result assert "500 errors from payment service" in result def test_combine_incident_text_backward_compatibility(self): """Test incident text combination with root-level fields.""" incident = { "title": "Auth Service Down", "summary": "Login failures", "description": "Cannot authenticate users" } result = self.analyzer._combine_incident_text(incident) assert "auth service down" in result assert "login failures" in result assert "cannot authenticate users" in result def test_calculate_resolution_time(self): """Test resolution time calculation.""" # Test with valid timestamps incident = { "attributes": { "created_at": "2024-01-01T10:00:00Z", "resolved_at": "2024-01-01T12:30:00Z" } } result = self.analyzer._calculate_resolution_time(incident) assert result == 2.5 # 2.5 hours # Test with missing timestamps incident = { "attributes": { "created_at": "2024-01-01T10:00:00Z" # No resolved_at } } result = self.analyzer._calculate_resolution_time(incident) assert result is None # Test with invalid timestamps incident = { "attributes": { "created_at": "invalid-date", "resolved_at": "2024-01-01T12:00:00Z" } } result = self.analyzer._calculate_resolution_time(incident) assert result is None @patch('rootly_mcp_server.smart_utils.ML_AVAILABLE', True) def test_calculate_similarity_with_ml(self): """Test similarity calculation with ML libraries available.""" incidents = [ { "id": "1", "attributes": { "title": "Payment API timeout", "summary": "Users cannot complete payments" } }, { "id": "2", "attributes": { "title": "Auth service error", "summary": "Login failures occurring" } } ] target_incident = { "id": "target", "attributes": { "title": "Payment service down", "summary": "Payment processing failures" } } # Mock the TF-IDF components - patch the sklearn module imports with patch('sklearn.feature_extraction.text.TfidfVectorizer') as mock_vectorizer, \ patch('sklearn.metrics.pairwise.cosine_similarity') as mock_similarity: mock_vectorizer_instance = MagicMock() mock_vectorizer.return_value = mock_vectorizer_instance # Mock TF-IDF matrix mock_tfidf_matrix = MagicMock() mock_vectorizer_instance.fit_transform.return_value = mock_tfidf_matrix # Mock similarity scores (payment incident should be more similar) import numpy as np mock_similarity.return_value = np.array([[0.8, 0.3]]) # High similarity to incident 1, low to incident 2 results = self.analyzer.calculate_similarity(incidents, target_incident) # Should return results sorted by similarity assert len(results) >= 1 assert isinstance(results[0], IncidentSimilarity) assert results[0].incident_id == "1" # Most similar incident assert results[0].similarity_score > 0.5 @patch('rootly_mcp_server.smart_utils.ML_AVAILABLE', False) def test_calculate_similarity_fallback(self): """Test similarity calculation fallback without ML libraries.""" incidents = [ { "id": "1", "attributes": { "title": "Payment API timeout error", "summary": "Users cannot complete payments due to timeout" } }, { "id": "2", "attributes": { "title": "Database connection error", "summary": "Cannot connect to user database" } } ] target_incident = { "id": "target", "attributes": { "title": "Payment API timeout", "summary": "Payment processing timeout errors" } } results = self.analyzer.calculate_similarity(incidents, target_incident) # Should use keyword-based similarity assert len(results) >= 1 assert isinstance(results[0], IncidentSimilarity) # Payment incident should be more similar due to matching keywords assert results[0].incident_id == "1" assert results[0].similarity_score > 0 def test_extract_common_keywords(self): """Test common keyword extraction.""" text1 = "payment api timeout error service" text2 = "payment service timeout connection error" common = self.analyzer._extract_common_keywords(text1, text2) assert "payment" in common assert "timeout" in common assert "error" in common assert "service" in common assert len(common) <= 8 # Should limit to top 8 (increased for fuzzy matching) class TestSolutionExtractor: """Test the SolutionExtractor class.""" def setup_method(self): """Set up test fixtures.""" self.extractor = SolutionExtractor() def test_extract_action_items(self): """Test action item extraction from resolution text.""" # Test restart actions text = "We restarted the payment service and the issue was resolved" actions = self.extractor._extract_action_items(text) assert any("restart" in action.lower() for action in actions) # Test clear actions text = "Cleared the Redis cache to fix the connection issue" actions = self.extractor._extract_action_items(text) assert any("clear" in action.lower() for action in actions) # Test update actions text = "Updated the database configuration and deployed the fix" actions = self.extractor._extract_action_items(text) assert any("update" in action.lower() for action in actions) # Test with no actions text = "The issue resolved itself after some time" actions = self.extractor._extract_action_items(text) # Should still extract some context assert isinstance(actions, list) def test_identify_common_patterns(self): """Test common pattern identification.""" similar_incidents = [ IncidentSimilarity( incident_id="1", title="Payment API error", similarity_score=0.8, matched_services=["payment", "auth"], matched_keywords=["timeout", "error"], resolution_time_hours=1.5 ), IncidentSimilarity( incident_id="2", title="Payment timeout", similarity_score=0.7, matched_services=["payment"], matched_keywords=["timeout", "connection"], resolution_time_hours=0.5 ) ] patterns = self.extractor._identify_common_patterns( ["timeout", "error", "timeout", "connection"], similar_incidents ) # Should identify common services assert any("payment" in pattern.lower() for pattern in patterns) # Should identify resolution time patterns (average is 1.0 hour, so in middle range) time_pattern_found = any("hour" in pattern.lower() for pattern in patterns) assert time_pattern_found, f"Expected time pattern in {patterns}" def test_extract_solutions(self): """Test complete solution extraction.""" similar_incidents = [ IncidentSimilarity( incident_id="123", title="Payment API timeout", similarity_score=0.85, matched_services=["payment"], matched_keywords=["timeout", "api"], resolution_summary="Restarted payment service and cleared cache", resolution_time_hours=1.0 ) ] result = self.extractor.extract_solutions(similar_incidents) assert "solutions" in result assert "common_patterns" in result assert "average_resolution_time" in result assert "total_similar_incidents" in result # Check solution structure solutions = result["solutions"] assert len(solutions) > 0 solution = solutions[0] assert "incident_id" in solution assert "title" in solution assert "similarity" in solution assert "resolution_summary" in solution # Check average resolution time assert result["average_resolution_time"] == 1.0 assert result["total_similar_incidents"] == 1 def test_extract_solutions_empty_input(self): """Test solution extraction with empty input.""" result = self.extractor.extract_solutions([]) assert result["solutions"] == [] assert result["common_patterns"] == [] assert result["average_resolution_time"] is None assert result["total_similar_incidents"] == 0 class TestIncidentSimilarity: """Test the IncidentSimilarity dataclass.""" def test_incident_similarity_creation(self): """Test creating an IncidentSimilarity object.""" similarity = IncidentSimilarity( incident_id="123", title="Test Incident", similarity_score=0.85, matched_services=["api", "db"], matched_keywords=["error", "timeout"], resolution_summary="Fixed by restart", resolution_time_hours=2.0 ) assert similarity.incident_id == "123" assert similarity.title == "Test Incident" assert similarity.similarity_score == 0.85 assert similarity.matched_services == ["api", "db"] assert similarity.matched_keywords == ["error", "timeout"] assert similarity.resolution_summary == "Fixed by restart" assert similarity.resolution_time_hours == 2.0 def test_incident_similarity_defaults(self): """Test IncidentSimilarity with default values.""" similarity = IncidentSimilarity( incident_id="123", title="Test Incident", similarity_score=0.85, matched_services=[], matched_keywords=[] ) assert similarity.resolution_summary == "" assert similarity.resolution_time_hours is None class TestIntegrationScenarios: """Test realistic end-to-end scenarios.""" def setup_method(self): """Set up test fixtures.""" self.analyzer = TextSimilarityAnalyzer() self.extractor = SolutionExtractor() def test_payment_api_scenario(self): """Test a realistic payment API incident scenario.""" # Historical incidents historical_incidents = [ { "id": "100", "attributes": { "title": "Payment API timeout errors", "summary": "Users cannot complete payments due to API timeouts", "created_at": "2024-01-01T10:00:00Z", "resolved_at": "2024-01-01T11:30:00Z" } }, { "id": "101", "attributes": { "title": "Database connection issues", "summary": "Cannot connect to user database", "created_at": "2024-01-02T14:00:00Z", "resolved_at": "2024-01-02T18:00:00Z" } } ] # Current incident target_incident = { "id": "200", "attributes": { "title": "Payment service returning 500 errors", "summary": "Users getting errors during checkout payment processing" } } # Calculate similarities similar_incidents = self.analyzer.calculate_similarity(historical_incidents, target_incident) # Should find the payment incident as more similar assert len(similar_incidents) > 0 most_similar = similar_incidents[0] assert most_similar.incident_id == "100" # Payment incident should be most similar assert most_similar.similarity_score > 0 # Extract solutions solutions = self.extractor.extract_solutions(similar_incidents[:1]) assert len(solutions["solutions"]) > 0 assert solutions["average_resolution_time"] == 1.5 # 1.5 hours resolution time @patch('rootly_mcp_server.smart_utils.ML_AVAILABLE', False) def test_fallback_scenario(self): """Test that fallback works when ML libraries aren't available.""" historical_incidents = [ { "id": "300", "attributes": { "title": "Auth service connection timeout", "summary": "Authentication failures due to connection timeout" } } ] target_incident = { "id": "400", "attributes": { "title": "Auth API timeout errors", "summary": "User login timeout errors" } } # Should work with keyword-based fallback similar_incidents = self.analyzer.calculate_similarity(historical_incidents, target_incident) assert len(similar_incidents) > 0 assert similar_incidents[0].incident_id == "300" assert similar_incidents[0].similarity_score > 0 # Should have matched keywords assert "auth" in similar_incidents[0].matched_keywords or "timeout" in similar_incidents[0].matched_keywords

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Rootly-AI-Labs/Rootly-MCP-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server