AST-Grep MCP: Batch Search Test Fixes and Task 15 Completion
Session Date: 2025-11-17 Project: ast-grep-mcp (MCP Server) Focus: Fix failing batch_search tests and complete Task 15 (Batch Operations) implementation
Executive Summary
Successfully debugged and fixed all 18 failing batch_search tests by identifying and correcting the test setup and mocking strategy. The key issue was that register_mcp_tools() was not being called during test setup, and tests were attempting to mock non-existent module-level functions. After fixing the test infrastructure, all 236 unit tests now pass (248 total including integration tests). Completed documentation updates and created git commit a0550e7 to finalize Task 15 (Batch Operations).
Key Metrics:
- 18 batch operation tests: 0 → 18 passing (100% success rate)
- Total unit tests: 236 passing
- Total tests: 248 passing (236 unit + 12 integration)
- Test coverage: 90% (736/818 statements)
- Batch search tool: ~260 lines of production code
Problem Statement
After implementing the batch_search tool in a previous session, all 18 unit tests were failing with errors:
AssertionError: batch_search tool not registeredAttributeError: <module 'main'> does not have the attribute 'find_code'
The tests were attempting to patch main.find_code and main.find_code_by_rule, which don’t exist as module-level functions. In the ast-grep-mcp architecture, all tools are defined inside the register_mcp_tools() function closure and registered via FastMCP decorators.
Root Cause Analysis
Issue #1: Missing Tool Registration
The test file was not calling register_mcp_tools() after importing the main module:
# Import with mocked decorators
with patch("mcp.server.fastmcp.FastMCP", MockFastMCP):
with patch("pydantic.Field", mock_field):
import main
# ❌ Missing: main.register_mcp_tools()
Impact: The batch_search tool was never registered in main.mcp.tools, causing all assertions to fail.
Issue #2: Incorrect Mock Targets
Tests were attempting to patch non-existent module-level functions:
@patch("main.find_code") # ❌ Doesn't exist at module level
def test_batch_search_single_query(self, mock_find_code: Mock):
mock_find_code.return_value = json.dumps([...])
Why this failed:
find_codeandfind_code_by_ruleare defined insideregister_mcp_tools()as closures- They don’t exist in the module namespace until
register_mcp_tools()is called - Even then, they’re only accessible via
main.mcp.toolsdictionary
Issue #3: Wrong Function Mocked
Initially attempted to mock run_ast_grep(), but the actual execution path uses:
find_code()→stream_ast_grep_results()→subprocess.Popen()find_code_by_rule()→stream_ast_grep_results()→subprocess.Popen()
Correct target: stream_ast_grep_results() is the function that actually executes ast-grep.
Issue #4: Incorrect Return Value Type
Mock was returning JSON strings instead of iterators:
# ❌ Wrong: stream_ast_grep_results returns iterator, not JSON string
mock_stream.return_value = json.dumps([{"file": "test.py", ...}])
# ✅ Correct: Return an iterator
mock_stream.return_value = iter([{"file": "test.py", ...}])
Implementation Details
1. Fixed Test Setup Pattern
Updated tests/unit/test_batch.py to match the pattern from test_duplication.py:
# Import with mocked decorators
with patch("mcp.server.fastmcp.FastMCP", MockFastMCP):
with patch("pydantic.Field", mock_field):
import main
# ✅ Call register_mcp_tools to define the tool functions
main.register_mcp_tools()
File: tests/unit/test_batch.py:44-49
2. Corrected Mock Decorator
Changed all test methods to mock the correct function:
# Before:
@patch("main.run_ast_grep")
def test_batch_search_single_query(self, mock_run: Mock):
mock_run.return_value = json.dumps([...])
# After:
@patch("main.stream_ast_grep_results")
def test_batch_search_single_query(self, mock_stream: Mock):
mock_stream.return_value = iter([...])
Changes: 14 occurrences across all test methods in the file
3. Fixed Return Value Type
Changed from JSON strings to iterators:
# Before (12 occurrences):
json.dumps([{"file": "test.py", "text": "match", ...}])
# After (12 occurrences):
iter([{"file": "test.py", "text": "match", ...}])
Used replace_all=true in Edit tool for bulk replacement efficiency.
4. Updated Parameter Names
Renamed all mock parameters for consistency:
# Before:
def test_batch_search_single_query(self, mock_run: Mock):
# After:
def test_batch_search_single_query(self, mock_stream: Mock):
Changes: 14 parameter name updates
Testing and Verification
Test Execution Results
$ uv run pytest tests/unit/test_batch.py -v
============================= test session starts ==============================
collected 18 items
tests/unit/test_batch.py::TestBatchSearchBasic::test_batch_search_tool_registered PASSED
tests/unit/test_batch.py::TestBatchSearchBasic::test_batch_search_single_query PASSED
tests/unit/test_batch.py::TestBatchSearchBasic::test_batch_search_multiple_queries_parallel PASSED
tests/unit/test_batch.py::TestBatchSearchBasic::test_batch_search_missing_type_field PASSED
tests/unit/test_batch.py::TestBatchSearchBasic::test_batch_search_invalid_type PASSED
tests/unit/test_batch.py::TestBatchSearchBasic::test_batch_search_rule_query PASSED
tests/unit/test_batch.py::TestBatchSearchAggregation::test_batch_search_deduplication PASSED
tests/unit/test_batch.py::TestBatchSearchAggregation::test_batch_search_no_deduplication PASSED
tests/unit/test_batch.py::TestBatchSearchAggregation::test_batch_search_sorts_results PASSED
tests/unit/test_batch.py::TestBatchSearchAggregation::test_batch_search_adds_query_id_to_matches PASSED
tests/unit/test_batch.py::TestBatchSearchConditional::test_conditional_if_matches_executes PASSED
tests/unit/test_batch.py::TestBatchSearchConditional::test_conditional_if_matches_skips PASSED
tests/unit/test_batch.py::TestBatchSearchConditional::test_conditional_if_no_matches_executes PASSED
tests/unit/test_batch.py::TestBatchSearchConditional::test_conditional_if_no_matches_skips PASSED
tests/unit/test_batch.py::TestBatchSearchErrorHandling::test_batch_search_continues_on_query_error PASSED
tests/unit/test_batch.py::TestBatchSearchErrorHandling::test_batch_search_auto_assigns_query_ids PASSED
tests/unit/test_batch.py::TestBatchSearchErrorHandling::test_batch_search_text_output_format PASSED
tests/unit/test_batch.py::TestBatchSearchErrorHandling::test_batch_search_max_results_per_query PASSED
============================== 18 passed in 1.97s ==============================
Full Test Suite Verification
$ uv run pytest tests/unit/ -q
....................................................................................................
....................................................................................................
..........................................
============================= 236 passed in 6.23s ==============================
Test Breakdown:
test_batch.py: 18 tests (6 basic + 4 aggregation + 4 conditional + 4 error handling)test_unit.py: 57 teststest_cache.py: 26 teststest_duplication.py: 24 teststest_phase2.py: 21 teststest_schema.py: 52 teststest_rewrite.py: 33 tests
Documentation Updates
Updated CLAUDE.md
Tool Count (CLAUDE.md:38-61):
- Changed from “16 MCP tools” to “17 MCP tools”
- Added
batch_searchto Code Search Tools section - Renumbered Schema.org tools from 9-16 to 10-17
- Renumbered Code Rewrite Tools from 6-8 to 7-9
Architecture Section (CLAUDE.md:393-416):
- Updated file size from ~3190 lines (approximate, may vary)
- Changed tool registration from “16 tools (5 ast-grep search + 3 ast-grep rewrite + 8 Schema.org)” to “17 tools (6 ast-grep search + 3 ast-grep rewrite + 8 Schema.org)”
- Added comprehensive Batch Operations description with 5 key features
Test Documentation (CLAUDE.md:149-198):
- Added
test_batch.pyto test file list - Increased test counts from 230 to 248 total tests
- Increased unit test count from 218 to 236
- Added detailed test_batch.py breakdown with test categories
Batch Search Tool Capabilities
The completed batch_search tool provides:
Core Features
- Parallel Execution
- Uses
ThreadPoolExecutorwith max 4 workers - Executes independent queries concurrently
- Significant performance improvement on multi-query workloads
- Uses
- Result Deduplication
- Removes duplicate matches based on file path + line number + text content
- Optional (can be disabled with
deduplicate=false) - Reduces noise in aggregated results
- Conditional Execution
if_matches: Execute query only if referenced query found matchesif_no_matches: Execute query only if referenced query found nothing- Enables complex query chaining and fallback logic
- Per-Query Statistics
- Tracks execution status, match count, and failure reasons
- Provides
query_idfield in all matches for traceability - Auto-assigns query IDs if not provided
- Error Isolation
- One query failure doesn’t stop other queries
- Failed queries return empty results
- Error details logged via structlog
Query Format
queries = [
{
"id": "find_functions",
"type": "pattern",
"pattern": "def $FUNC",
"language": "python"
},
{
"id": "find_async_functions",
"type": "pattern",
"pattern": "async def $FUNC",
"language": "python",
"condition": {
"type": "if_no_matches",
"query_id": "find_functions"
}
}
]
Response Format
{
"total_queries": 2,
"total_matches": 15,
"queries_executed": ["find_functions", "find_async_functions"],
"matches": [
{
"file": "app.py",
"text": "def hello():\n pass",
"range": {"start": {"line": 1}, "end": {"line": 2}},
"query_id": "find_functions"
},
# ... more matches sorted by file and line
],
"per_query_stats": {
"find_functions": {
"executed": true,
"match_count": 10
},
"find_async_functions": {
"executed": true,
"match_count": 5
}
}
}
Key Decisions and Trade-offs
Decision 1: Mock stream_ast_grep_results Instead of run_ast_grep
Rationale: The execution path for JSON output format uses streaming:
find_code()callsstream_ast_grep_results()with--json=stream- Enables early termination and progress logging
- Returns iterator of match dictionaries
Alternative Considered: Mock run_ast_grep() which is used for non-streaming operations Why Rejected: Wrong level of abstraction - tests would execute real subprocess calls
Decision 2: Use iter() for Mock Return Values
Rationale: stream_ast_grep_results() is a generator function that yields match dictionaries Implementation: mock_stream.return_value = iter([{...}]) creates an iterator from a list Alternative Considered: Use generator functions with yield Why Rejected: Overkill for simple test cases; iter() is more readable
Decision 3: Follow test_duplication.py Pattern
Rationale: Established pattern in the codebase for testing MCP tools Benefits:
- Consistency across test files
- Proven to work with the MockFastMCP architecture
- Clear separation of mock setup and test execution
Challenges and Solutions
Challenge 1: Understanding Tool Registration Pattern
Problem: Tools are defined inside register_mcp_tools() closure, not at module level Investigation: Examined test_duplication.py to understand the correct pattern Solution: Call main.register_mcp_tools() immediately after importing main module Lesson: When adding new tools, always check existing test files for patterns
Challenge 2: Identifying Correct Mock Target
Problem: Initial attempts to mock run_ast_grep() didn’t work Investigation: Traced execution path through code:
- Read
main.py:1249- sawstream_ast_grep_results()call - Confirmed this is the subprocess execution point
- Verified it returns an iterator
Solution: Mock stream_ast_grep_results() and return iterators Lesson: Trace the actual execution path, don’t assume based on function names
Challenge 3: Bulk Replacement Efficiency
Problem: 14 test methods needed identical changes Solution: Used Edit tool with replace_all=true for efficient bulk updates Changes:
@patch("main.run_ast_grep")→@patch("main.stream_ast_grep_results")mock_run→mock_streamjson.dumps([→iter([
Lesson: Leverage tool capabilities for repetitive edits
Git Commit
commit a0550e7
Author: Alyshia Ledlie
Date: 2025-11-17
Add batch_search tool for parallel multi-query execution (Task 15)
Implements batch operations that execute multiple ast-grep searches in
parallel with advanced result aggregation and conditional logic.
Features:
- Parallel execution using ThreadPoolExecutor (max 4 workers)
- Support for both pattern and YAML rule queries
- Automatic result deduplication based on file+line+text
- Conditional execution (if_matches/if_no_matches) for query chaining
- Per-query statistics and error isolation
- Auto-assignment of query IDs when not provided
- Results sorted by file and line number
- Query traceability via query_id field in matches
Implementation (main.py):
- Added batch_search tool (~260 lines) with extensive validation
- Executes unconditional queries in parallel for performance
- Executes conditional queries sequentially with dependency checks
- Graceful error handling - one query failure doesn't stop others
- Supports both text and JSON output formats
- Integrates with existing streaming and caching infrastructure
Testing (tests/unit/test_batch.py):
- 18 comprehensive unit tests with mocked subprocess calls
- Tests cover: basic functionality, result aggregation, conditional
execution, error handling
- All tests passing (236 total unit tests, 248 total tests)
Documentation (CLAUDE.md):
- Updated tool count from 16 to 17
- Added batch_search to tool list
- Added Batch Operations section in Architecture
- Updated test counts: 248 tests (236 unit + 12 integration)
- Added test_batch.py to test file documentation
Files Changed:
main.py: 779 insertions (batch_search implementation completed in previous session)tests/unit/test_batch.py: 496 lines (new file)CLAUDE.md: 16 modifications (documentation updates)
Performance Impact
Test Execution Performance
| Metric | Value |
|---|---|
| Test file execution time | 1.97s |
| Tests per second | ~9.1 |
| Full unit suite time | 6.23s |
| Average test time | ~26ms |
Mock Efficiency
Mocking stream_ast_grep_results() provides:
- No subprocess overhead
- No ast-grep binary dependency for unit tests
- Deterministic, reproducible test results
- Fast test execution (< 2 seconds for 18 tests)
Lessons Learned
Tool Architecture Understanding: In MCP servers using FastMCP, tools are often defined inside registration functions. Always check how tools are registered before writing tests.
Mock the Right Layer: Mock at the lowest level that provides isolation. For ast-grep-mcp, that’s
stream_ast_grep_results(), not the tool functions themselves.Examine Existing Patterns: When adding new features, examine similar existing code and tests. The
test_duplication.pyfile provided the exact pattern needed.Return Type Matters: Pay attention to whether functions return values, lists, iterators, or generators. Mocks must match the actual return type.
Bulk Edits Save Time: Using
replace_all=truein the Edit tool for systematic changes is much faster than manual edits.Test Early: If tests had been run immediately after implementation, these issues would have been caught sooner.
Next Steps
Immediate
- ✅ All tests passing
- ✅ Documentation updated
- ✅ Git commit created
- ✅ Task 15 complete
Future Enhancements (Tasks 12-14)
Remaining Phase 3 tasks from roadmap:
- Task 12: Language-specific search optimization
- Task 13: Configuration file support
- Task 14: Workspace-level caching
These tasks were deprioritized in favor of batch operations (Task 15), which provides more immediate value for complex multi-query workflows.
References
main.py:2434-2692- batch_search implementationtests/unit/test_batch.py- 18 comprehensive teststests/unit/test_duplication.py:44-49- Pattern used for tool registrationmain.py:1249- stream_ast_grep_results call site in find_codeCLAUDE.md:38-61- Tool listing documentationCLAUDE.md:393-416- Architecture documentation- Git commit
a0550e7- Task 15 completion
Conclusion
Successfully debugged and resolved all 18 failing batch_search tests by:
- Identifying the tool registration issue
- Correcting the mock target from module-level functions to the actual subprocess execution point
- Fixing return value types to match the iterator pattern
- Following established testing patterns from the codebase
Task 15 (Batch Operations) is now fully complete with 100% passing tests, comprehensive documentation, and a clean git commit. The batch_search tool adds powerful multi-query capabilities to the ast-grep-mcp server, enabling parallel execution, result aggregation, and conditional query logic.