test-search-code.js•17.4 kB
/**
 * Unit tests for search functionality using new streaming search API
 */
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);
// Test directory and files
const TEST_DIR = path.join(__dirname, 'search-test-files');
const TEST_FILE_1 = path.join(TEST_DIR, 'test1.js');
const TEST_FILE_2 = path.join(TEST_DIR, 'test2.ts');
const TEST_FILE_3 = path.join(TEST_DIR, 'hidden.txt');
const TEST_FILE_4 = path.join(TEST_DIR, 'subdir', 'nested.py');
// 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 to prepare test environment
 */
async function setup() {
  console.log(`${colors.blue}Setting up search code tests...${colors.reset}`);
  
  // Save original config
  const originalConfig = await configManager.getConfig();
  
  // Set allowed directories to include test directory
  await configManager.setValue('allowedDirectories', [TEST_DIR]);
  
  // Create test directory structure
  await fs.mkdir(TEST_DIR, { recursive: true });
  await fs.mkdir(path.join(TEST_DIR, 'subdir'), { recursive: true });
  
  // Create test files with various content
  await fs.writeFile(TEST_FILE_1, `// JavaScript test file
function searchFunction() {
  const pattern = 'test pattern';
  console.log('This is a test function');
  return pattern;
}
// Another function
function anotherFunction() {
  const result = searchFunction();
  return result;
}
`);
  await fs.writeFile(TEST_FILE_2, `// TypeScript test file
interface TestInterface {
  pattern: string;
  value: number;
}
class TestClass implements TestInterface {
  pattern: string = 'test pattern';
  value: number = 42;
  
  searchMethod(): string {
    return this.pattern;
  }
}
export { TestClass };
`);
  await fs.writeFile(TEST_FILE_3, `This is a hidden text file.
It contains some test content.
Pattern matching should work here too.
Multiple lines with different patterns.
`);
  await fs.writeFile(TEST_FILE_4, `# Python test file
import os
import sys
def search_function():
    pattern = "test pattern"
    print("This is a python function")
    return pattern
class TestClass:
    def __init__(self):
        self.pattern = "test pattern"
    
    def search_method(self):
        return self.pattern
`);
  console.log(`${colors.green}✓ Setup complete: Test files created${colors.reset}`);
  return originalConfig;
}
/**
 * Teardown function to clean up after tests
 */
async function teardown(originalConfig) {
  console.log(`${colors.blue}Cleaning up search code tests...${colors.reset}`);
  
  // 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
  }
  
  // Remove test directory and all files
  await fs.rm(TEST_DIR, { force: true, recursive: true });
  
  // Restore original config
  await configManager.updateConfig(originalConfig);
  
  console.log(`${colors.green}✓ Teardown complete: Test files removed and config restored${colors.reset}`);
}
/**
 * Assert function for test validation
 */
function assert(condition, message) {
  if (!condition) {
    throw new Error(`Assertion failed: ${message}`);
  }
}
/**
 * Test basic search functionality
 */
async function testBasicSearch() {
  console.log(`${colors.yellow}Testing basic search functionality...${colors.reset}`);
  
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'pattern',
    searchType: 'content'
  });
  
  assert(finalResult.content, 'Result should have content');
  assert(finalResult.content.length > 0, 'Content should not be empty');
  
  const text = finalResult.content[0].text;
  assert(text.includes('test1.js'), 'Should find matches in test1.js');
  assert(text.includes('test2.ts'), 'Should find matches in test2.ts');
  assert(text.includes('nested.py'), 'Should find matches in nested.py');
  
  console.log(`${colors.green}✓ Basic search test passed${colors.reset}`);
}
/**
 * Test case-sensitive search
 */
async function testCaseSensitiveSearch() {
  console.log(`${colors.yellow}Testing case-sensitive search...${colors.reset}`);
  
  // Search for 'Pattern' (capital P) with case sensitivity
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'Pattern',
    searchType: 'content',
    ignoreCase: false
  });
  
  const text = finalResult.content[0].text;
  // Should only find matches where 'Pattern' appears with capital P
  assert(text.includes('hidden.txt'), 'Should find Pattern in hidden.txt');
  
  console.log(`${colors.green}✓ Case-sensitive search test passed${colors.reset}`);
}
/**
 * Test case-insensitive search
 */
async function testCaseInsensitiveSearch() {
  console.log(`${colors.yellow}Testing case-insensitive search...${colors.reset}`);
  
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'PATTERN',
    searchType: 'content',
    ignoreCase: true
  });
  
  const text = finalResult.content[0].text;
  assert(text.includes('test1.js'), 'Should find pattern in test1.js');
  assert(text.includes('test2.ts'), 'Should find pattern in test2.ts');
  assert(text.includes('nested.py'), 'Should find pattern in nested.py');
  
  console.log(`${colors.green}✓ Case-insensitive search test passed${colors.reset}`);
}
/**
 * Test file pattern filtering
 */
async function testFilePatternFiltering() {
  console.log(`${colors.yellow}Testing file pattern filtering...${colors.reset}`);
  
  // Search only in TypeScript files
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'pattern',
    searchType: 'content',
    filePattern: '*.ts'
  });
  
  const text = finalResult.content[0].text;
  assert(text.includes('test2.ts'), 'Should find matches in TypeScript files');
  assert(!text.includes('test1.js'), 'Should not include JavaScript files');
  assert(!text.includes('nested.py'), 'Should not include Python files');
  
  console.log(`${colors.green}✓ File pattern filtering test passed${colors.reset}`);
}
/**
 * Test maximum results limiting
 */
async function testMaxResults() {
  console.log(`${colors.yellow}Testing maximum results limiting...${colors.reset}`);
  
  // Test that the maxResults parameter is accepted and doesn't cause errors
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'function', // This pattern should appear multiple times
    searchType: 'content',
    maxResults: 5 // Small limit
  });
  
  assert(finalResult.content, 'Should have content');
  assert(finalResult.content.length > 0, 'Content should not be empty');
  
  const text = finalResult.content[0].text;
  
  // Verify we get some results
  assert(text.length > 0, 'Should have some results');
  
  // Should have results but respect the limit
  const hasResults = text.includes('function') || text.includes('No matches found');
  assert(hasResults, 'Should have function results or no matches');
  
  console.log(`${colors.green}✓ Max results limiting test passed${colors.reset}`);
}
/**
 * Test context lines functionality
 */
async function testContextLines() {
  console.log(`${colors.yellow}Testing context lines functionality...${colors.reset}`);
  
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'searchFunction',
    searchType: 'content',
    contextLines: 1
  });
  
  const text = finalResult.content[0].text;
  // With context lines, we should see lines before and after the match
  assert(text.length > 0, 'Should have context around matches');
  
  console.log(`${colors.green}✓ Context lines test passed${colors.reset}`);
}
/**
 * Test hidden files inclusion
 */
async function testIncludeHidden() {
  console.log(`${colors.yellow}Testing hidden files inclusion...${colors.reset}`);
  
  // First, create a hidden file (starts with dot)
  const hiddenFile = path.join(TEST_DIR, '.hidden-file.txt');
  await fs.writeFile(hiddenFile, 'This is hidden content with pattern');
  
  try {
    const { finalResult } = await searchAndWaitForCompletion({
      path: TEST_DIR,
      pattern: 'hidden content',
      searchType: 'content',
      includeHidden: true
    });
    
    const text = finalResult.content[0].text;
    const hasHiddenResults = text.includes('.hidden-file.txt') || text.includes('No matches found');
    assert(hasHiddenResults, 'Should handle hidden files when includeHidden is true');
    
    console.log(`${colors.green}✓ Include hidden files test passed${colors.reset}`);
  } finally {
    // Clean up hidden file
    await fs.rm(hiddenFile, { force: true });
  }
}
/**
 * Test timeout functionality
 */
async function testTimeout() {
  console.log(`${colors.yellow}Testing timeout functionality...${colors.reset}`);
  
  // Use a reasonable timeout
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'pattern',
    searchType: 'content',
    timeout_ms: 5000 // 5 seconds should be plenty
  });
  
  assert(finalResult.content, 'Result should have content even with timeout');
  assert(finalResult.content.length > 0, 'Content should not be empty');
  
  const text = finalResult.content[0].text;
  // Should have results or indicate completion
  const hasValidResult = text.includes('pattern') || text.includes('No matches found') || text.includes('completed');
  assert(hasValidResult, 'Should handle timeout gracefully');
  
  console.log(`${colors.green}✓ Timeout test passed${colors.reset}`);
}
/**
 * Test no matches found scenario
 */
async function testNoMatches() {
  console.log(`${colors.yellow}Testing no matches found scenario...${colors.reset}`);
  
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: 'this-pattern-definitely-does-not-exist-anywhere',
    searchType: 'content'
  });
  
  assert(finalResult.content, 'Result should have content');
  assert(finalResult.content.length > 0, 'Content should not be empty');
  
  const text = finalResult.content[0].text;
  assert(text.includes('No matches') || text.includes('Total results found: 0'), 'Should return no matches message');
  
  console.log(`${colors.green}✓ No matches test passed${colors.reset}`);
}
/**
 * Test invalid path handling
 */
async function testInvalidPath() {
  console.log(`${colors.yellow}Testing invalid path handling...${colors.reset}`);
  
  try {
    const result = await handleStartSearch({
      path: '/nonexistent/path/that/does/not/exist',
      pattern: 'pattern',
      searchType: 'content'
    });
    
    // Should handle gracefully
    assert(result.content, 'Result should have content');
    const text = result.content[0].text;
    const isValidResponse = text.includes('Error') || text.includes('session:') || text.includes('not allowed');
    assert(isValidResponse, 'Should handle invalid path gracefully');
    
    console.log(`${colors.green}✓ Invalid path test passed${colors.reset}`);
  } catch (error) {
    // It's also acceptable for the function to throw an error for invalid paths
    console.log(`${colors.green}✓ Invalid path test passed (threw error as expected)${colors.reset}`);
  }
}
/**
 * Test schema validation with invalid arguments
 */
async function testInvalidArguments() {
  console.log(`${colors.yellow}Testing invalid arguments handling...${colors.reset}`);
  
  // Test missing required path
  try {
    const result = await handleStartSearch({
      pattern: 'test'
      // Missing path
    });
    const text = result.content[0].text;
    assert(text.includes('Invalid arguments'), 'Should validate path is required');
  } catch (error) {
    // Also acceptable to throw
    assert(error.message.includes('path') || error.message.includes('required'), 'Should validate path is required');
  }
  
  // Test missing required pattern
  try {
    const result = await handleStartSearch({
      path: TEST_DIR
      // Missing pattern
    });
    const text = result.content[0].text;
    assert(text.includes('Invalid arguments'), 'Should validate pattern is required');
  } catch (error) {
    // Also acceptable to throw
    assert(error.message.includes('pattern') || error.message.includes('required'), 'Should validate pattern is required');
  }
  
  console.log(`${colors.green}✓ Invalid arguments test passed${colors.reset}`);
}
/**
 * Test file search functionality
 */
async function testFileSearch() {
  console.log(`${colors.yellow}Testing file search functionality...${colors.reset}`);
  
  const { finalResult } = await searchAndWaitForCompletion({
    path: TEST_DIR,
    pattern: '*.js',
    searchType: 'files'
  });
  
  const text = finalResult.content[0].text;
  assert(text.includes('test1.js'), 'Should find JavaScript files');
  
  console.log(`${colors.green}✓ File search test passed${colors.reset}`);
}
/**
 * Main test runner function
 */
export async function testSearchCode() {
  console.log(`${colors.blue}Starting search functionality tests...${colors.reset}`);
  
  let originalConfig;
  
  try {
    // Setup
    originalConfig = await setup();
    
    // Run all tests
    await testBasicSearch();
    await testCaseSensitiveSearch();
    await testCaseInsensitiveSearch();
    await testFilePatternFiltering();
    await testMaxResults();
    await testContextLines();
    await testIncludeHidden();
    await testTimeout();
    await testNoMatches();
    await testInvalidPath();
    await testInvalidArguments();
    await testFileSearch();
    
    console.log(`${colors.green}✅ All search functionality tests passed!${colors.reset}`);
    return true;
    
  } catch (error) {
    console.error(`${colors.red}❌ Test failed: ${error.message}${colors.reset}`);
    console.error(error.stack);
    throw error;
  } finally {
    // Cleanup
    if (originalConfig) {
      await teardown(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 run-all-tests.js
export default testSearchCode;
// Run tests if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
  testSearchCode().then(() => {
    console.log('Search tests completed successfully.');
    process.exit(0);
  }).catch(error => {
    console.error('Test execution failed:', error);
    process.exit(1);
  });
}