Skip to main content
Glama

Claude Desktop Commander MCP

test-search-code-edge-cases.js18.4 kB
/** * Additional comprehensive tests for search functionality using new streaming API * These tests cover edge cases and advanced scenarios */ import path from 'path'; import fs from 'fs/promises'; import { fileURLToPath } from 'url'; import { handleStartSearch, handleGetMoreSearchResults, handleStopSearch } from '../dist/handlers/search-handlers.js'; import { configManager } from '../dist/config-manager.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const EDGE_CASE_TEST_DIR = path.join(__dirname, 'search-edge-case-tests'); // Colors for console output const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m' }; /** * Helper function to wait for search completion and get all results */ async function searchAndWaitForCompletion(searchArgs, timeout = 10000) { const result = await handleStartSearch(searchArgs); // Extract session ID from result const sessionIdMatch = result.content[0].text.match(/Started .+ session: (.+)/); if (!sessionIdMatch) { throw new Error('Could not extract session ID from search result'); } const sessionId = sessionIdMatch[1]; try { // Wait for completion by polling const startTime = Date.now(); while (Date.now() - startTime < timeout) { const moreResults = await handleGetMoreSearchResults({ sessionId }); if (moreResults.content[0].text.includes('✅ Search completed')) { return { initialResult: result, finalResult: moreResults, sessionId }; } if (moreResults.content[0].text.includes('❌ ERROR')) { throw new Error(`Search failed: ${moreResults.content[0].text}`); } // Wait a bit before polling again await new Promise(resolve => setTimeout(resolve, 100)); } throw new Error('Search timed out'); } finally { // Always stop the search session to prevent hanging try { await handleStopSearch({ sessionId }); } catch (e) { // Ignore errors when stopping - session might already be completed } } } /** * Setup function for edge case tests */ async function setupEdgeCases() { console.log(`${colors.blue}Setting up edge case tests...${colors.reset}`); const originalConfig = await configManager.getConfig(); await configManager.setValue('allowedDirectories', [EDGE_CASE_TEST_DIR]); await fs.mkdir(EDGE_CASE_TEST_DIR, { recursive: true }); // Create files with edge cases // Empty file await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'empty.txt'), ''); // File with only whitespace await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'whitespace.txt'), ' \n\t\n \n'); // File with very long lines const longLine = 'a'.repeat(10000) + 'pattern' + 'b'.repeat(10000); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'long-lines.txt'), longLine); // File with special characters await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'special-chars.txt'), 'Special chars: @#$%^&*(){}[]|\\:";\'<>?,./\nUnicode: 😀🎉🔍\nPattern with special chars: test@pattern'); // File with binary content (should be handled gracefully) const binaryData = Buffer.from([0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD]); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'binary.bin'), binaryData); // Large file (for performance testing) const largeContent = 'This is line with pattern\n'.repeat(1000); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'large.txt'), largeContent); // File with regex special characters in content await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'regex-chars.txt'), 'Content with regex chars: .+*?^${}()|[]\\\nPattern: test.pattern\nAnother: test*pattern'); return originalConfig; } /** * Teardown function for edge case tests */ async function teardownEdgeCases(originalConfig) { // Clean up any remaining search sessions try { const { handleListSearches, handleStopSearch } = await import('../dist/handlers/search-handlers.js'); const sessionsResult = await handleListSearches(); if (sessionsResult.content && sessionsResult.content[0] && sessionsResult.content[0].text) { const sessionsText = sessionsResult.content[0].text; if (!sessionsText.includes('No active searches')) { // Extract session IDs and stop them const sessionMatches = sessionsText.match(/Session: (\S+)/g); if (sessionMatches) { for (const match of sessionMatches) { const sessionId = match.replace('Session: ', ''); try { await handleStopSearch({ sessionId }); } catch (e) { // Ignore errors - session might already be stopped } } } } } } catch (e) { // Ignore errors in cleanup } await fs.rm(EDGE_CASE_TEST_DIR, { force: true, recursive: true }); await configManager.updateConfig(originalConfig); } /** * Assert function */ function assert(condition, message) { if (!condition) { throw new Error(`Assertion failed: ${message}`); } } /** * Test empty and whitespace files */ async function testEmptyFiles() { console.log(`${colors.yellow}Testing empty and whitespace files...${colors.reset}`); const { finalResult } = await searchAndWaitForCompletion({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content' }); const text = finalResult.content[0].text; // Should not find matches in empty files, but should handle gracefully const isValidResponse = !text.includes('empty.txt') && !text.includes('whitespace.txt') || text.includes('No matches'); assert(isValidResponse, 'Should not find matches in empty files'); console.log(`${colors.green}✓ Empty files test passed${colors.reset}`); } /** * Test very long lines */ async function testLongLines() { console.log(`${colors.yellow}Testing very long lines...${colors.reset}`); const { finalResult } = await searchAndWaitForCompletion({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content' }); const text = finalResult.content[0].text; assert(text.includes('long-lines.txt'), 'Should find pattern in files with very long lines'); console.log(`${colors.green}✓ Long lines test passed${colors.reset}`); } /** * Test special characters and Unicode */ async function testSpecialCharacters() { console.log(`${colors.yellow}Testing special characters and Unicode...${colors.reset}`); const { finalResult } = await searchAndWaitForCompletion({ path: EDGE_CASE_TEST_DIR, pattern: 'test@pattern', searchType: 'content' }); const text = finalResult.content[0].text; assert(text.includes('special-chars.txt'), 'Should find patterns with special characters'); console.log(`${colors.green}✓ Special characters test passed${colors.reset}`); } /** * Test binary files handling */ async function testBinaryFiles() { console.log(`${colors.yellow}Testing binary files handling...${colors.reset}`); const { finalResult } = await searchAndWaitForCompletion({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content' }); const text = finalResult.content[0].text; // Binary files should either be ignored or handled gracefully // Should not crash the search assert(typeof text === 'string', 'Should return string result even with binary files present'); console.log(`${colors.green}✓ Binary files test passed${colors.reset}`); } /** * Test large file performance */ async function testLargeFiles() { console.log(`${colors.yellow}Testing large file performance...${colors.reset}`); const startTime = Date.now(); const { finalResult } = await searchAndWaitForCompletion({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content', maxResults: 10 // Limit results for performance }); const endTime = Date.now(); const duration = endTime - startTime; const text = finalResult.content[0].text; assert(text.includes('large.txt'), 'Should find matches in large files'); // Performance check - should complete within reasonable time (10 seconds) assert(duration < 10000, `Search should complete within 10 seconds, took ${duration}ms`); console.log(`${colors.green}✓ Large files test passed (${duration}ms)${colors.reset}`); } /** * Test concurrent searches */ async function testConcurrentSearches() { console.log(`${colors.yellow}Testing concurrent searches...${colors.reset}`); // Start multiple searches concurrently const promises = [ handleStartSearch({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content' }), handleStartSearch({ path: EDGE_CASE_TEST_DIR, pattern: 'test', searchType: 'content' }), handleStartSearch({ path: EDGE_CASE_TEST_DIR, pattern: 'chars', searchType: 'content' }) ]; const results = await Promise.all(promises); // All searches should complete successfully results.forEach((result, index) => { assert(result.content, `Search ${index + 1} should have content`); assert(result.content.length > 0, `Search ${index + 1} should not be empty`); }); console.log(`${colors.green}✓ Concurrent searches test passed${colors.reset}`); } /** * Test search with very short timeout */ async function testVeryShortTimeout() { console.log(`${colors.yellow}Testing very short timeout...${colors.reset}`); const result = await handleStartSearch({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content', timeout_ms: 1 // Extremely short timeout }); assert(result.content, 'Should handle timeout gracefully'); const text = result.content[0].text; // Should either return results or handle timeout gracefully const hasValidResponse = text.includes('session:') || text.includes('Error') || text.includes('timeout'); assert(hasValidResponse, 'Should handle very short timeout appropriately'); console.log(`${colors.green}✓ Very short timeout test passed${colors.reset}`); } /** * Test invalid file patterns */ async function testInvalidFilePatterns() { console.log(`${colors.yellow}Testing invalid file patterns...${colors.reset}`); // Test with invalid glob pattern const result = await handleStartSearch({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content', filePattern: '***invalid***' }); // Should handle gracefully assert(result.content, 'Should handle invalid file patterns gracefully'); console.log(`${colors.green}✓ Invalid file patterns test passed${colors.reset}`); } /** * Test zero max results */ async function testZeroMaxResults() { console.log(`${colors.yellow}Testing zero max results...${colors.reset}`); const result = await handleStartSearch({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content', maxResults: 0 }); const text = result.content[0].text; // Should return appropriate response assert(typeof text === 'string', 'Should return string result'); console.log(`${colors.green}✓ Zero max results test passed${colors.reset}`); } /** * Test extremely large context lines */ async function testLargeContextLines() { console.log(`${colors.yellow}Testing large context lines...${colors.reset}`); const result = await handleStartSearch({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content', contextLines: 1000 // Very large context }); assert(result.content, 'Should handle large context lines'); const text = result.content[0].text; assert(typeof text === 'string', 'Should return string result'); console.log(`${colors.green}✓ Large context lines test passed${colors.reset}`); } /** * Test path traversal security */ async function testPathTraversalSecurity() { console.log(`${colors.yellow}Testing path traversal security...${colors.reset}`); // Test with path traversal attempts try { const result = await handleStartSearch({ path: EDGE_CASE_TEST_DIR + '/../../../etc', pattern: 'pattern', searchType: 'content' }); // If it doesn't throw, it should handle gracefully assert(result.content, 'Should handle path traversal attempts gracefully'); const text = result.content[0].text; const isSecure = text.includes('not allowed') || text.includes('Error') || text.includes('permission'); assert(isSecure, 'Should handle path traversal securely'); } catch (error) { // It's acceptable to throw an error for security violations const isSecurityError = error.message.includes('not allowed') || error.message.includes('permission'); assert(isSecurityError, 'Should reject unauthorized path access'); } console.log(`${colors.green}✓ Path traversal security test passed${colors.reset}`); } /** * Test memory usage with many small files */ async function testManySmallFiles() { console.log(`${colors.yellow}Testing many small files...${colors.reset}`); // Create subdirectory with many small files const manyFilesDir = path.join(EDGE_CASE_TEST_DIR, 'many-files'); await fs.mkdir(manyFilesDir, { recursive: true }); try { // Create 100 small files const promises = []; for (let i = 0; i < 100; i++) { promises.push(fs.writeFile( path.join(manyFilesDir, `file${i}.txt`), `This is file ${i} with pattern ${i}` )); } await Promise.all(promises); const { finalResult } = await searchAndWaitForCompletion({ path: manyFilesDir, pattern: 'pattern', searchType: 'content', maxResults: 50 }); const text = finalResult.content[0].text; assert(text.includes('pattern') || text.includes('No matches'), 'Should handle many small files'); console.log(`${colors.green}✓ Many small files test passed${colors.reset}`); } finally { // Clean up many files await fs.rm(manyFilesDir, { force: true, recursive: true }); } } /** * Test filePattern with multiple values, including whitespace and empty tokens */ async function testFilePatternWithMultipleValues() { console.log(`${colors.yellow}Testing filePattern with multiple values...${colors.reset}`); // Create test files await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'file1.ts'), 'export const myTsVar = "patternTs";'); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'file2.js'), 'const myJsVar = "patternJs";'); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'file3.py'), 'my_py_var = "patternPy"'); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'file4.java'), 'String myJavaVar = "patternJava";'); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'file5.go'), 'var myGoVar = "patternGo"'); await fs.writeFile(path.join(EDGE_CASE_TEST_DIR, 'file6.txt'), 'This is a text file.'); // Test with valid multiple patterns let { finalResult } = await searchAndWaitForCompletion({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content', filePattern: '*.ts|*.js|*.py' }); let text = finalResult.content[0].text; assert(text.includes('file1.ts'), 'Should find match in file1.ts'); assert(text.includes('file2.js'), 'Should find match in file2.js'); assert(text.includes('file3.py'), 'Should find match in file3.py'); assert(!text.includes('file4.java'), 'Should not find match in file4.java'); assert(!text.includes('file5.go'), 'Should not find match in file5.go'); // Test with patterns including whitespace ({ finalResult } = await searchAndWaitForCompletion({ path: EDGE_CASE_TEST_DIR, pattern: 'pattern', searchType: 'content', filePattern: ' *.ts | *.js ' })); text = finalResult.content[0].text; assert(text.includes('file1.ts'), 'Should find match with whitespace-padded patterns (file1.ts)'); assert(text.includes('file2.js'), 'Should find match with whitespace-padded patterns (file2.js)'); assert(!text.includes('file3.py'), 'Should not find match with whitespace-padded patterns (file3.py)'); console.log(`${colors.green}✓ FilePattern with multiple values test passed${colors.reset}`); } /** * Main test runner for edge cases */ export async function testSearchCodeEdgeCases() { console.log(`${colors.blue}Starting search functionality edge case tests...${colors.reset}`); let originalConfig; try { // Setup originalConfig = await setupEdgeCases(); // Run all edge case tests await testEmptyFiles(); await testLongLines(); await testSpecialCharacters(); await testBinaryFiles(); await testLargeFiles(); await testConcurrentSearches(); await testVeryShortTimeout(); await testInvalidFilePatterns(); await testZeroMaxResults(); await testLargeContextLines(); await testPathTraversalSecurity(); await testManySmallFiles(); await testFilePatternWithMultipleValues(); console.log(`${colors.green}✅ All search functionality edge case tests passed!${colors.reset}`); return true; } catch (error) { console.error(`${colors.red}❌ Edge case test failed: ${error.message}${colors.reset}`); console.error(error.stack); throw error; } finally { // Cleanup if (originalConfig) { await teardownEdgeCases(originalConfig); } // Force cleanup of search manager to ensure process can exit try { const { searchManager, stopSearchManagerCleanup } = await import('../dist/search-manager.js'); // Terminate all active sessions const activeSessions = searchManager.listSearchSessions(); for (const session of activeSessions) { searchManager.terminateSearch(session.id); } // Stop the cleanup interval stopSearchManagerCleanup(); // Clear the sessions map searchManager.sessions?.clear?.(); } catch (e) { // Ignore import errors } } } // Export for use in test runners export default testSearchCodeEdgeCases; // Run tests if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { testSearchCodeEdgeCases().then(() => { console.log('Edge case tests completed successfully.'); process.exit(0); }).catch(error => { console.error('Edge case test execution failed:', error); process.exit(1); }); }

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/wonderwhy-er/DesktopCommanderMCP'

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