Files
zooqueenandHanzo Dev b144973bfc fix(lint): resolve all ruff + black errors across all packages
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310,
S306, S110, TID251, UP042, F401) and run black on 526 files.

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-21 23:32:32 -08:00

636 lines
21 KiB
Python

"""Comprehensive test suite for search functionality."""
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from hanzo_tools.filesystem.search_tool import SearchResult, SearchTool, SearchType
from hanzo_mcp.tools.common.permissions import PermissionManager
from tests.test_utils import create_permission_manager
class TestSearchTool:
"""Test suite for the SearchTool."""
@pytest.fixture
def permission_manager(self):
"""Create a permission manager for testing."""
pm = PermissionManager()
pm.add_allowed_path("/tmp")
pm.add_allowed_path(".")
return pm
@pytest.fixture
def mock_project_manager(self):
"""Create a mock project manager."""
mock_pm = MagicMock()
mock_pm.search_all_projects = AsyncMock(return_value={})
mock_pm.get_project_for_path = MagicMock(return_value=None)
mock_pm._get_global_store = MagicMock()
mock_pm.projects = {}
return mock_pm
@pytest.fixture
def search_tool(self, permission_manager, mock_project_manager):
"""Create a search tool instance."""
return SearchTool(permission_manager, mock_project_manager)
@pytest.fixture
def mock_context(self):
"""Create a mock MCP context."""
ctx = MagicMock()
ctx.meta = MagicMock()
return ctx
@pytest.fixture
def test_files(self):
"""Create temporary test files."""
with tempfile.TemporaryDirectory() as tmpdir:
test_dir = Path(tmpdir)
# Create test Python file
python_file = test_dir / "test_module.py"
python_file.write_text("""
def hello_world():
'''Say hello to the world.'''
print("Hello, world!")
return "greeting"
class TestClass:
'''A test class for demonstration.'''
def test_method(self):
'''Test method with error handling.'''
try:
result = hello_world()
return result
except Exception as e:
print(f"Error occurred: {e}")
raise
def complex_function(data, options=None):
'''Complex function for testing search.'''
if options is None:
options = {}
# Process data with error handling
processed = []
for item in data:
try:
processed.append(item.upper())
except AttributeError:
processed.append(str(item))
return processed
""")
# Create test JavaScript file
js_file = test_dir / "test_script.js"
js_file.write_text("""
function helloWorld() {
console.log("Hello, world!");
return "greeting";
}
class TestClass {
constructor() {
this.name = "test";
}
testMethod() {
try {
return helloWorld();
} catch (error) {
console.error("Error occurred:", error);
throw error;
}
}
}
function complexFunction(data, options = {}) {
const processed = data.map(item => {
try {
return item.toUpperCase();
} catch (error) {
return String(item);
}
});
return processed;
}
""")
# Create test documentation file
md_file = test_dir / "README.md"
md_file.write_text("""
# Test Project
This is a test project for demonstrating search capabilities.
## Features
- Hello world functionality
- Error handling
- Complex data processing
- Multi-language support
## Usage
```python
from test_module import hello_world
result = hello_world()
```
## Error Handling
The project includes comprehensive error handling throughout.
""")
yield {
"dir": test_dir,
"python": python_file,
"javascript": js_file,
"markdown": md_file,
}
def test_search_intent_detection(self, tool_helper, search_tool):
"""Test the search intent detection logic."""
# Regex pattern should disable vector search
use_vector, use_ast, use_symbol = search_tool._detect_search_intent(".*error")
assert not use_vector
assert use_ast
assert use_symbol
# Function name should enable symbol and AST
use_vector, use_ast, use_symbol = search_tool._detect_search_intent(
"hello_world"
)
assert use_vector # Could be semantic
assert use_ast
assert use_symbol
# Natural language should enable vector search
use_vector, use_ast, use_symbol = search_tool._detect_search_intent(
"error handling functionality"
)
assert use_vector
assert use_ast
assert use_symbol
@pytest.mark.asyncio
async def test_grep_search(
self, tool_helper, search_tool, test_files, mock_context
):
"""Test grep search functionality."""
tool_ctx = MagicMock()
tool_ctx.info = AsyncMock()
tool_ctx.error = AsyncMock()
tool_ctx.mcp_context = mock_context
with patch.object(search_tool, "create_tool_context", return_value=tool_ctx):
with patch.object(search_tool.grep_tool, "call") as mock_grep:
mock_grep.return_value = """Found 2 matches in 1 file:
test_module.py:3: def hello_world():
test_module.py:15: result = hello_world()"""
results = await search_tool._run_grep_search(
"hello_world", str(test_files["dir"]), "*", tool_ctx, 10
)
assert len(results) == 2
assert all(r.search_type == SearchType.GREP for r in results)
assert results[0].file_path == "test_module.py"
assert results[0].line_number == 3
assert "def hello_world():" in results[0].content
@pytest.mark.asyncio
async def test_ast_search(self, tool_helper, search_tool, test_files, mock_context):
"""Test AST search functionality."""
tool_ctx = MagicMock()
tool_ctx.info = AsyncMock()
tool_ctx.error = AsyncMock()
tool_ctx.mcp_context = mock_context
with patch.object(search_tool, "create_tool_context", return_value=tool_ctx):
with patch.object(search_tool.grep_ast_tool, "call") as mock_ast:
mock_ast.return_value = f"""
{test_files["python"]}:
3: def hello_world():
4: '''Say hello to the world.'''
5: print("Hello, world!")
6: return "greeting"
"""
results = await search_tool._run_ast_search(
"hello_world", str(test_files["dir"]), "*", tool_ctx, 10
)
assert len(results) > 0
assert all(r.search_type == SearchType.AST for r in results)
@pytest.mark.asyncio
async def test_symbol_search(
self, tool_helper, search_tool, test_files, mock_context
):
"""Test symbol search functionality."""
tool_ctx = MagicMock()
tool_ctx.info = AsyncMock()
tool_ctx.error = AsyncMock()
with patch.object(search_tool, "create_tool_context", return_value=tool_ctx):
# Mock the AST analyzer
with patch.object(search_tool.ast_analyzer, "analyze_file") as mock_analyze:
from hanzo_mcp.tools.vector.ast_analyzer import FileAST, Symbol
# Create mock symbols
symbol = Symbol(
name="hello_world",
type="function",
file_path=str(test_files["python"]),
line_start=3,
line_end=6,
column_start=0,
column_end=20,
scope="global",
signature="hello_world()",
)
mock_ast = FileAST(
file_path=str(test_files["python"]),
file_hash="test_hash",
language="python",
symbols=[symbol],
ast_nodes=[],
imports=[],
exports=[],
dependencies=[],
)
mock_analyze.return_value = mock_ast
results = await search_tool._run_symbol_search(
"hello_world", str(test_files["dir"]), tool_ctx, 10
)
assert len(results) > 0
assert all(r.search_type == SearchType.SYMBOL for r in results)
assert results[0].symbol_info is not None
assert results[0].symbol_info.name == "hello_world"
@pytest.mark.asyncio
async def test_vector_search(
self, tool_helper, search_tool, test_files, mock_context
):
"""Test vector search functionality."""
tool_ctx = MagicMock()
tool_ctx.info = AsyncMock()
tool_ctx.error = AsyncMock()
tool_ctx.mcp_context = mock_context
# Mock vector tool
search_tool.vector_tool = MagicMock()
search_tool.vector_tool.call = AsyncMock()
search_tool.vector_tool.call.return_value = """Found 1 results for query: 'error handling'
Result 1 (Score: 85.5%) - Project: test
test_module.py [Chunk 0]
Content:
Test method with error handling.
try:
result = hello_world()
return result
except Exception as e:
print(f"Error occurred: {e}")"""
with patch.object(search_tool, "create_tool_context", return_value=tool_ctx):
results = await search_tool._run_vector_search(
"error handling", str(test_files["dir"]), tool_ctx, 10
)
assert len(results) > 0
assert all(r.search_type == SearchType.VECTOR for r in results)
@pytest.mark.asyncio
async def test_full_search(
self, tool_helper, search_tool, test_files, mock_context
):
"""Test complete search functionality."""
with patch.object(search_tool, "validate_path") as mock_validate:
mock_validate.return_value = MagicMock(is_error=False)
with patch.object(search_tool, "check_path_allowed") as mock_allowed:
mock_allowed.return_value = (True, None)
with patch.object(search_tool, "check_path_exists") as mock_exists:
mock_exists.return_value = (True, None)
with patch.object(
search_tool, "create_tool_context"
) as mock_tool_ctx:
tool_ctx = MagicMock()
tool_ctx.info = AsyncMock()
tool_ctx.error = AsyncMock()
tool_ctx.mcp_context = mock_context
mock_tool_ctx.return_value = tool_ctx
# Mock all search methods
with patch.object(search_tool, "_run_grep_search") as mock_grep:
with patch.object(
search_tool, "_run_ast_search"
) as mock_ast:
with patch.object(
search_tool, "_run_symbol_search"
) as mock_symbol:
# Setup mock results
grep_result = SearchResult(
file_path="test.py",
line_number=1,
content="def hello_world():",
search_type=SearchType.GREP,
score=1.0,
)
ast_result = SearchResult(
file_path="test.py",
line_number=1,
content="def hello_world():",
search_type=SearchType.AST,
score=0.9,
context="function definition",
)
symbol_result = SearchResult(
file_path="test.py",
line_number=1,
content="function hello_world",
search_type=SearchType.SYMBOL,
score=0.95,
)
mock_grep.return_value = [grep_result]
mock_ast.return_value = [ast_result]
mock_symbol.return_value = [symbol_result]
# Execute search
result = await search_tool.call(
mock_context,
pattern="hello_world",
path=str(test_files["dir"]),
max_results=10,
)
tool_helper.assert_in_result(
"Unified Search Results", result
)
tool_helper.assert_in_result("hello_world", result)
tool_helper.assert_in_result("Found", result)
def test_result_combination_and_ranking(self, tool_helper, search_tool):
"""Test result combination and ranking logic."""
# Create test results
grep_result = SearchResult(
file_path="test.py",
line_number=1,
content="def hello_world():",
search_type=SearchType.GREP,
score=1.0,
)
ast_result = SearchResult(
file_path="test.py",
line_number=1,
content="def hello_world():",
search_type=SearchType.AST,
score=0.9,
context="function definition",
)
symbol_result = SearchResult(
file_path="test.py",
line_number=1,
content="function hello_world",
search_type=SearchType.SYMBOL,
score=0.95,
)
vector_result = SearchResult(
file_path="other.py",
line_number=5,
content="call hello_world function",
search_type=SearchType.VECTOR,
score=0.8,
)
results_by_type = {
SearchType.GREP: [grep_result],
SearchType.AST: [ast_result],
SearchType.SYMBOL: [symbol_result],
SearchType.VECTOR: [vector_result],
}
combined = search_tool._combine_and_rank_results(results_by_type)
# Should have 2 unique results (3 duplicates merged into 1)
assert len(combined) == 2
# Should be sorted by score (symbol score should win for duplicates)
assert combined[0].score == 0.95 # Symbol result wins
assert combined[1].score == 0.8 # Vector result
def test_search_result_serialization(self):
"""Test SearchResult and UnifiedSearchResults serialization."""
from hanzo_mcp.tools.vector.ast_analyzer import Symbol
symbol = Symbol(
name="test_func",
type="function",
file_path="test.py",
line_start=1,
line_end=5,
column_start=0,
column_end=20,
scope="global",
)
result = SearchResult(
file_path="test.py",
line_number=1,
content="def test_func():",
search_type=SearchType.GREP,
score=1.0,
context="function definition",
symbol_info=symbol,
)
# Test serialization
result_dict = result.to_dict()
assert result_dict["file_path"] == "test.py"
assert result_dict["search_type"] == "grep"
assert result_dict["symbol_info"]["name"] == "test_func"
# Test UnifiedSearchResults
unified_results = UnifiedSearchResults(
query="test",
total_results=1,
results_by_type={SearchType.GREP: [result]},
combined_results=[result],
search_time_ms=100.5,
)
unified_dict = unified_results.to_dict()
assert unified_dict["query"] == "test"
assert unified_dict["total_results"] == 1
assert unified_dict["search_time_ms"] == 100.5
class TestUnifiedSearchIntegration:
"""Integration tests for search with real file operations."""
@pytest.fixture
def real_test_environment(self):
"""Create a real test environment with actual files."""
with tempfile.TemporaryDirectory() as tmpdir:
test_dir = Path(tmpdir)
# Create a small Python project
(test_dir / "__init__.py").touch()
main_file = test_dir / "main.py"
main_file.write_text("""
#!/usr/bin/env python3
'''Main module for testing search.'''
import logging
from typing import List, Optional
logger = logging.getLogger(__name__)
class DataProcessor:
'''Processes data with error handling.'''
def __init__(self, config: Optional[dict] = None):
self.config = config or {}
logger.info("DataProcessor initialized")
def process_items(self, items: List[str]) -> List[str]:
'''Process a list of items with error handling.'''
processed = []
for item in items:
try:
result = self._process_single_item(item)
processed.append(result)
except ValueError as e:
logger.error(f"Error processing item {item}: {e}")
continue
except Exception as e:
logger.critical(f"Unexpected error: {e}")
raise
return processed
def _process_single_item(self, item: str) -> str:
'''Process a single item.'''
if not item.strip():
raise ValueError("Empty item")
return item.upper().strip()
def main():
'''Main function with comprehensive error handling.'''
try:
processor = DataProcessor()
test_data = ["hello", "world", "", "test"]
results = processor.process_items(test_data)
print(f"Processed {len(results)} items successfully")
return results
except Exception as e:
logger.error(f"Main execution failed: {e}")
return []
if __name__ == "__main__":
main()
""")
utils_file = test_dir / "utils.py"
utils_file.write_text("""
'''Utility functions for the test project.'''
import re
from typing import Union, Optional
def validate_input(data: Union[str, int, float]) -> bool:
'''Validate input data with error handling.'''
try:
if isinstance(data, str):
return bool(data.strip())
elif isinstance(data, (int, float)):
return not (data is None or data != data) # Check for NaN
else:
return False
except Exception:
return False
def format_error_message(error: Exception, context: Optional[str] = None) -> str:
'''Format error messages consistently.'''
base_msg = f"{type(error).__name__}: {str(error)}"
if context:
return f"[{context}] {base_msg}"
return base_msg
def extract_function_names(code: str) -> List[str]:
'''Extract function names from Python code.'''
pattern = r'def\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\('
matches = re.findall(pattern, code)
return matches
""")
yield {"dir": test_dir, "main": main_file, "utils": utils_file}
@pytest.mark.asyncio
async def test_real_search(self, tool_helper, real_test_environment):
"""Test search on real files."""
permission_manager = create_permission_manager(
[str(real_test_environment["dir"])]
)
# Test without vector search (no project manager)
unified_tool = SearchTool(permission_manager, None)
# Mock context
ctx = MagicMock()
# Test search for "error handling"
with patch.object(unified_tool, "validate_path") as mock_validate:
mock_validate.return_value = MagicMock(is_error=False)
with patch.object(unified_tool, "check_path_allowed") as mock_allowed:
mock_allowed.return_value = (True, None)
with patch.object(unified_tool, "check_path_exists") as mock_exists:
mock_exists.return_value = (True, None)
result = await unified_tool.call(
ctx,
pattern="error.handling",
path=str(real_test_environment["dir"]),
enable_vector=False, # Disable vector search
max_results=20,
)
# Should find multiple instances of error handling
tool_helper.assert_in_result("Unified Search Results", result)
assert "error" in result.lower()
if __name__ == "__main__":
# Run tests
pytest.main([__file__, "-v"])