Skip to main content
Glama
test_patterns.py•15.6 kB
""" Unit tests for pattern analysis functionality. Tests the analyze_patterns MCP tool with mocked LLM responses. """ import pytest from unittest.mock import Mock, AsyncMock, patch from pathlib import Path # Mock implementation (replace with actual import when refactored) async def analyze_patterns_mock( pattern_type: str, max_examples: int = 3, local_llm=None, semantic_search_func=None ) -> str: """Mock pattern analysis for testing.""" if not local_llm: return "Error: Ollama not available. Is it running?" try: # Simulate semantic search to find examples if not semantic_search_func: search_result = f"Found examples for {pattern_type}" else: search_result = await semantic_search_func( f"{pattern_type} patterns", max_results=max_examples * 2 ) if "No results" in search_result or "not indexed" in search_result: return search_result # Simulate LLM analysis prompt = f"""Analyze code patterns in this workspace. Pattern Type: {pattern_type} Search Results: {search_result[:2000]} Provide concise summary (max 200 words): 1. Pattern characteristics 2. 1-2 code examples (short) 3. When to use 4. Common pitfalls Be concise - this summary goes to another LLM.""" response = await local_llm.ainvoke(prompt) summary = response.content[:800] return summary except Exception as e: return f"Error: {str(e)}" @pytest.mark.unit @pytest.mark.asyncio class TestPatternAnalysis: """Test suite for pattern analysis.""" async def test_analyze_no_llm(self): """Test analysis fails gracefully when LLM unavailable.""" result = await analyze_patterns_mock( "error_handling", local_llm=None ) assert "Error: Ollama not available" in result async def test_analyze_with_llm(self, mock_ollama_llm): """Test successful pattern analysis with mocked LLM.""" # Mock search function that returns results async def mock_search(query, max_results): return """Found 2 matches for 'error_handling patterns': 1. src/main.py (score: 0.92) def handle_error(error): print(f"Error: {error}") 2. src/utils.py (score: 0.85) try: func() except Exception as e: handle_error(e) """ result = await analyze_patterns_mock( "error_handling", max_examples=3, local_llm=mock_ollama_llm, semantic_search_func=mock_search ) assert "Mocked LLM response" in result assert len(result) <= 800 # Should truncate to 800 chars async def test_analyze_no_search_results(self, mock_ollama_llm): """Test analysis when search returns no results.""" async def mock_search_empty(query, max_results): return "No results found for 'nonexistent patterns'" result = await analyze_patterns_mock( "nonexistent", local_llm=mock_ollama_llm, semantic_search_func=mock_search_empty ) assert "No results found" in result async def test_analyze_workspace_not_indexed(self, mock_ollama_llm): """Test analysis when workspace not indexed.""" async def mock_search_not_indexed(query, max_results): return "Workspace not indexed yet. Run: index_workspace()" result = await analyze_patterns_mock( "async", local_llm=mock_ollama_llm, semantic_search_func=mock_search_not_indexed ) assert "not indexed" in result.lower() async def test_analyze_max_examples_parameter(self, mock_ollama_llm): """Test max_examples parameter affects search.""" call_count = 0 captured_max_results = None async def mock_search_tracker(query, max_results): nonlocal call_count, captured_max_results call_count += 1 captured_max_results = max_results return "Found 1 match" await analyze_patterns_mock( "async", max_examples=5, local_llm=mock_ollama_llm, semantic_search_func=mock_search_tracker ) # Should request max_examples * 2 results from search assert captured_max_results == 10 async def test_analyze_truncates_long_response(self, mock_ollama_llm): """Test that LLM responses are truncated to 800 chars.""" # Create mock LLM with very long response long_response = Mock() long_response.content = "x" * 2000 # 2000 chars mock_ollama_llm.ainvoke = AsyncMock(return_value=long_response) async def mock_search(query, max_results): return "Found results" result = await analyze_patterns_mock( "test", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) assert len(result) == 800 async def test_analyze_truncates_long_search_results(self, mock_ollama_llm): """Test that search results are truncated to 2000 chars in prompt.""" very_long_search = "x" * 5000 # Very long search result async def mock_search_long(query, max_results): return very_long_search # Capture the prompt sent to LLM captured_prompt = None async def capture_prompt(prompt): nonlocal captured_prompt captured_prompt = prompt response = Mock() response.content = "Analysis result" return response mock_ollama_llm.ainvoke = AsyncMock(side_effect=capture_prompt) await analyze_patterns_mock( "test", local_llm=mock_ollama_llm, semantic_search_func=mock_search_long ) # Search results in prompt should be truncated to 2000 chars # The full 5000-char search should NOT be in prompt assert very_long_search not in captured_prompt # But the truncated version (2000 chars) should be assert very_long_search[:2000] in captured_prompt async def test_analyze_exception_handling(self, mock_ollama_llm): """Test error handling when analysis fails.""" # Mock LLM that raises exception mock_ollama_llm.ainvoke = AsyncMock( side_effect=Exception("LLM connection failed") ) async def mock_search(query, max_results): return "Found results" result = await analyze_patterns_mock( "test", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) assert "Error" in result assert "LLM connection failed" in result @pytest.mark.unit @pytest.mark.asyncio class TestPatternTypes: """Test analysis of different pattern types.""" @pytest.mark.parametrize("pattern_type,expected_in_query", [ ("error_handling", "error_handling patterns"), ("async", "async patterns"), ("retry_logic", "retry_logic patterns"), ("authentication", "authentication patterns"), ("caching", "caching patterns"), ]) async def test_analyze_various_patterns( self, mock_ollama_llm, pattern_type, expected_in_query ): """Parametrized test for different pattern types.""" captured_query = None async def mock_search_capture(query, max_results): nonlocal captured_query captured_query = query return "Found results" await analyze_patterns_mock( pattern_type, local_llm=mock_ollama_llm, semantic_search_func=mock_search_capture ) assert expected_in_query in captured_query async def test_analyze_special_characters_in_pattern(self, mock_ollama_llm): """Test pattern type with special characters.""" async def mock_search(query, max_results): return "Found results" result = await analyze_patterns_mock( "error/exception handling", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) # Should handle without crashing assert isinstance(result, str) async def test_analyze_empty_pattern_type(self, mock_ollama_llm): """Test with empty pattern type.""" async def mock_search(query, max_results): return "Found results" result = await analyze_patterns_mock( "", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) # Should still work (query will be " patterns") assert isinstance(result, str) @pytest.mark.unit @pytest.mark.asyncio class TestPatternAnalysisPrompt: """Test the prompt construction for LLM.""" async def test_prompt_includes_pattern_type(self, mock_ollama_llm): """Test that prompt includes requested pattern type.""" captured_prompt = None async def capture_prompt(prompt): nonlocal captured_prompt captured_prompt = prompt response = Mock() response.content = "Analysis" return response mock_ollama_llm.ainvoke = AsyncMock(side_effect=capture_prompt) async def mock_search(query, max_results): return "Results" await analyze_patterns_mock( "error_handling", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) assert "Pattern Type: error_handling" in captured_prompt async def test_prompt_includes_search_results(self, mock_ollama_llm): """Test that prompt includes search results.""" captured_prompt = None async def capture_prompt(prompt): nonlocal captured_prompt captured_prompt = prompt response = Mock() response.content = "Analysis" return response mock_ollama_llm.ainvoke = AsyncMock(side_effect=capture_prompt) async def mock_search(query, max_results): return "UNIQUE_SEARCH_MARKER_12345" await analyze_patterns_mock( "test", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) assert "UNIQUE_SEARCH_MARKER_12345" in captured_prompt async def test_prompt_requests_concise_summary(self, mock_ollama_llm): """Test that prompt asks for concise summary.""" captured_prompt = None async def capture_prompt(prompt): nonlocal captured_prompt captured_prompt = prompt response = Mock() response.content = "Analysis" return response mock_ollama_llm.ainvoke = AsyncMock(side_effect=capture_prompt) async def mock_search(query, max_results): return "Results" await analyze_patterns_mock( "test", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) assert "concise" in captured_prompt.lower() assert "200 words" in captured_prompt or "max 200" in captured_prompt async def test_prompt_includes_required_sections(self, mock_ollama_llm): """Test prompt requests all required sections.""" captured_prompt = None async def capture_prompt(prompt): nonlocal captured_prompt captured_prompt = prompt response = Mock() response.content = "Analysis" return response mock_ollama_llm.ainvoke = AsyncMock(side_effect=capture_prompt) async def mock_search(query, max_results): return "Results" await analyze_patterns_mock( "test", local_llm=mock_ollama_llm, semantic_search_func=mock_search ) # Should request these sections assert "characteristics" in captured_prompt.lower() assert "examples" in captured_prompt.lower() assert "when to use" in captured_prompt.lower() assert "pitfalls" in captured_prompt.lower() @pytest.mark.unit @pytest.mark.asyncio class TestPatternAnalysisIntegration: """Integration-style tests for pattern analysis (still using mocks).""" async def test_full_analysis_workflow(self, mock_ollama_llm): """Test complete analysis workflow end-to-end.""" # Simulate realistic search results async def realistic_search(query, max_results): return """Found 3 matches for 'error_handling patterns': 1. src/main.py (score: 0.92) def handle_error(error): logger.error(f"Error occurred: {error}") raise CustomException(error) 2. src/utils.py (score: 0.88) try: risky_operation() except ValueError as e: handle_error(e) except Exception as e: log_and_exit(e) 3. src/api/routes.py (score: 0.85) @app.errorhandler(404) def not_found(error): return {"error": "Not found"}, 404 """ # Simulate realistic LLM response realistic_response = Mock() realistic_response.content = """Error Handling Patterns: 1. Characteristics: Consistent logger.error() usage, custom exceptions, decorator-based handlers 2. Example: try/except with specific exception types (ValueError), fallback to generic Exception 3. When to use: API endpoints (404 handler), risky operations, centralized error handling 4. Pitfalls: Avoid bare except clauses, always log before raising, don't silently swallow errors""" mock_ollama_llm.ainvoke = AsyncMock(return_value=realistic_response) result = await analyze_patterns_mock( "error_handling", max_examples=3, local_llm=mock_ollama_llm, semantic_search_func=realistic_search ) # Verify result contains expected analysis assert "Error Handling Patterns" in result or "error handling" in result.lower() assert len(result) <= 800 # Truncated properly async def test_analysis_with_default_search(self, mock_ollama_llm): """Test analysis when no search function provided (uses default).""" result = await analyze_patterns_mock( "testing_patterns", local_llm=mock_ollama_llm, semantic_search_func=None # No search function ) # Should still produce result using fallback assert isinstance(result, str) assert "Mocked LLM response" in result async def test_analysis_handles_search_exception(self, mock_ollama_llm): """Test graceful handling when search function raises.""" async def mock_search_error(query, max_results): raise ConnectionError("Network unavailable") result = await analyze_patterns_mock( "test", local_llm=mock_ollama_llm, semantic_search_func=mock_search_error ) assert "Error" in result assert "Network unavailable" in result

Latest Blog Posts

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/mjdevaccount/AIStack-MCP'

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