test-literal-search.js•14.6 kB
/**
 * Test for literal search functionality - testing regex vs literal string matching
 */
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 for literal search tests
const LITERAL_SEARCH_TEST_DIR = path.join(__dirname, 'literal-search-test-files');
// 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 literal search test environment
 */
async function setup() {
  console.log(`${colors.blue}Setting up literal search tests...${colors.reset}`);
  
  // Save original config
  const originalConfig = await configManager.getConfig();
  
  // Set allowed directories to include test directory
  await configManager.setValue('allowedDirectories', [LITERAL_SEARCH_TEST_DIR]);
  
  // Create test directory
  await fs.mkdir(LITERAL_SEARCH_TEST_DIR, { recursive: true });
  
  // Create test file with patterns that contain regex special characters
  await fs.writeFile(path.join(LITERAL_SEARCH_TEST_DIR, 'code-patterns.js'), `// JavaScript file with common code patterns
// These patterns contain regex special characters that should be matched literally
// Function calls with parentheses
toast.error("test");
console.log("hello world");
alert("message");
// Array access with brackets
array[0]
data[index]
items[key]
// Object method calls with dots
obj.method()
user.getName()
config.getValue()
// Template literals with backticks
const msg = \`Hello \${name}\`;
const query = \`SELECT * FROM users WHERE id = \${id}\`;
// Regex special characters
const pattern = ".*";
const wildcard = "test*";
const question = "value?";
const plus = "count++";
const caret = "^start";
const dollar = "end$";
const pipe = "a|b";
const backslash = "path\\\\to\\\\file";
// Complex patterns
if (condition && obj.method()) {
  toast.error("Error occurred");
}
function validateEmail(email) {
  return email.includes("@") && email.includes(".");
}
`);
  // Create another file with similar but different patterns
  await fs.writeFile(path.join(LITERAL_SEARCH_TEST_DIR, 'similar-patterns.ts'), `// TypeScript file with similar patterns
interface Config {
  getValue(): string;
}
class Logger {
  static error(message: string): void {
    console.error(\`[ERROR] \${message}\`);
  }
  
  static log(message: string): void {
    console.log(message);
  }
}
// These should NOT match when searching for exact patterns
toast.errorHandler("test"); // Similar but different
console.logout("hello world"); // Similar but different
array.slice(0, 1); // Similar but different
`);
  
  console.log(`${colors.green}✓ Setup complete: Literal search test files created${colors.reset}`);
  return originalConfig;
}
/**
 * Teardown function to clean up after tests
 */
async function teardown(originalConfig) {
  console.log(`${colors.blue}Cleaning up literal search tests...${colors.reset}`);
  
  // Clean up test directory
  await fs.rm(LITERAL_SEARCH_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}`);
  }
}
/**
 * Count occurrences of a pattern in text
 */
function countOccurrences(text, pattern) {
  return (text.match(new RegExp(pattern, 'g')) || []).length;
}
/**
 * Test that literal search finds exact matches for patterns with special characters
 * This test should FAIL initially since literalSearch parameter doesn't exist yet
 */
async function testLiteralSearchExactMatches() {
  console.log(`${colors.yellow}Testing literal search for exact matches...${colors.reset}`);
  
  // Test 1: Search for exact function call with quotes and parentheses
  const { finalResult: result1 } = await searchAndWaitForCompletion({
    path: LITERAL_SEARCH_TEST_DIR,
    pattern: 'toast.error("test")',
    searchType: 'content',
    literalSearch: true  // This parameter should be added
  });
  
  const text1 = result1.content[0].text;
  
  // Should find exactly 2 occurrences (one in each file: the exact match and in comment)
  const exactMatches = countOccurrences(text1, 'toast\\.error\\("test"\\)');
  assert(exactMatches >= 1, `Should find exact matches for 'toast.error("test")', found: ${exactMatches}`);
  
  // Should NOT find the similar but different pattern
  assert(!text1.includes('toast.errorHandler'), 'Should not match similar but different patterns');
  
  console.log(`${colors.green}✓ Literal search exact matches test passed${colors.reset}`);
}
/**
 * Test that regex search works differently than literal search
 */
async function testRegexVsLiteralDifference() {
  console.log(`${colors.yellow}Testing difference between regex and literal search...${colors.reset}`);
  
  // Test with regex (default behavior) - should interpret dots as wildcard
  const { finalResult: regexResult } = await searchAndWaitForCompletion({
    path: LITERAL_SEARCH_TEST_DIR,
    pattern: 'console.log',  // Dot should match any character in regex mode
    searchType: 'content',
    literalSearch: false  // Explicit regex mode
  });
  
  // Test with literal search - should match exact dots
  const { finalResult: literalResult } = await searchAndWaitForCompletion({
    path: LITERAL_SEARCH_TEST_DIR,
    pattern: 'console.log',  // Dot should match literal dot only
    searchType: 'content', 
    literalSearch: true
  });
  
  const regexText = regexResult.content[0].text;
  const literalText = literalResult.content[0].text;
  
  // Both should find console.log, but regex might find more due to dot wildcard behavior
  assert(regexText.includes('console.log'), 'Regex search should find console.log');
  assert(literalText.includes('console.log'), 'Literal search should find console.log');
  
  console.log(`${colors.green}✓ Regex vs literal difference test passed${colors.reset}`);
}
/**
 * Test literal search with various special characters
 */
async function testSpecialCharactersLiteralSearch() {
  console.log(`${colors.yellow}Testing literal search with various special characters...${colors.reset}`);
  
  const testPatterns = [
    'array[0]',           // Brackets
    'obj.method()',       // Dots and parentheses
    'count++',            // Plus signs
    'value?',             // Question mark
    'pattern.*',          // Dot and asterisk
    '^start',             // Caret
    'end$',               // Dollar sign
    'a|b',                // Pipe
    'path\\\\to\\\\file'  // Backslashes
  ];
  
  for (const pattern of testPatterns) {
    console.log(`  Testing pattern: ${pattern}`);
    
    const { finalResult } = await searchAndWaitForCompletion({
      path: LITERAL_SEARCH_TEST_DIR,
      pattern: pattern,
      searchType: 'content',
      literalSearch: true
    });
    
    const text = finalResult.content[0].text;
    
    // Should find the pattern or indicate no matches (both are valid for literal search)
    const hasResults = text.includes(pattern) || text.includes('No matches found') || text.includes('Total results found: 0');
    assert(hasResults, `Should handle literal search for pattern '${pattern}' (found results or no matches message)`);
  }
  
  console.log(`${colors.green}✓ Special characters literal search test passed${colors.reset}`);
}
/**
 * Test that literalSearch parameter defaults to false (maintains backward compatibility)
 */
async function testLiteralSearchDefault() {
  console.log(`${colors.yellow}Testing that literalSearch defaults to false...${colors.reset}`);
  
  // Search without specifying literalSearch - should default to regex behavior
  const { finalResult } = await searchAndWaitForCompletion({
    path: LITERAL_SEARCH_TEST_DIR,
    pattern: 'console.log',
    searchType: 'content'
    // literalSearch not specified - should default to false
  });
  
  const text = finalResult.content[0].text;
  
  // Should work (either find matches or no matches, but not error)
  const isValidResult = text.includes('console.log') || text.includes('No matches found') || text.includes('Total results found');
  assert(isValidResult, 'Should handle search with default literalSearch behavior');
  
  console.log(`${colors.green}✓ Literal search default behavior test passed${colors.reset}`);
}
/**
 * Test the specific failing case that motivated this fix
 */
async function testOriginalFailingCase() {
  console.log(`${colors.yellow}Testing the original failing case that motivated this fix...${colors.reset}`);
  
  // This was the original failing search: toast.error("test")
  const { finalResult } = await searchAndWaitForCompletion({
    path: LITERAL_SEARCH_TEST_DIR,
    pattern: 'toast.error("test")',
    searchType: 'content',
    literalSearch: true
  });
  
  const text = finalResult.content[0].text;
  
  // Should find the exact match
  assert(text.includes('toast.error("test")') || text.includes('code-patterns.js'), 
    'Should find the exact pattern that was originally failing');
    
  // Verify it contains the file where we know the pattern exists
  assert(text.includes('code-patterns.js'), 'Should find matches in code-patterns.js file');
  
  console.log(`${colors.green}✓ Original failing case test passed${colors.reset}`);
}
/**
 * Test that demonstrates regex mode fails while literal mode succeeds
 * This is the key test that shows the problem we solved
 */
async function testRegexFailureLiteralSuccess() {
  console.log(`${colors.yellow}Testing that regex mode fails where literal mode succeeds...${colors.reset}`);
  
  // Test with regex mode (default) - should fail to find exact pattern due to regex interpretation
  const { finalResult: regexResult } = await searchAndWaitForCompletion({
    path: LITERAL_SEARCH_TEST_DIR,
    pattern: 'toast.error("test")',  // This pattern has regex special chars
    searchType: 'content',
    literalSearch: false  // Use regex mode (default)
  });
  
  // Test with literal mode - should succeed in finding exact pattern  
  const { finalResult: literalResult } = await searchAndWaitForCompletion({
    path: LITERAL_SEARCH_TEST_DIR,
    pattern: 'toast.error("test")',  // Same pattern
    searchType: 'content',
    literalSearch: true   // Use literal mode
  });
  
  const regexText = regexResult.content[0].text;
  const literalText = literalResult.content[0].text;
  
  // Regex mode should find few/no matches due to special character interpretation
  const regexMatches = (regexText.match(/toast\.error\("test"\)/g) || []).length;
  
  // Literal mode should find the exact matches
  const literalMatches = (literalText.match(/toast\.error\("test"\)/g) || []).length;
  
  console.log(`  Regex mode found: ${regexMatches} matches`);
  console.log(`  Literal mode found: ${literalMatches} matches`);
  
  // The key assertion: literal should find more matches than regex
  assert(literalMatches > regexMatches, 
    `Literal search should find more matches (${literalMatches}) than regex search (${regexMatches}) for patterns with special characters`);
  
  // Literal should find at least one match
  assert(literalMatches >= 1, 'Literal search should find at least one exact match');
  
  console.log(`${colors.green}✓ Regex failure vs literal success test passed${colors.reset}`);
}
/**
 * Main test runner function for literal search tests
 */
export async function testLiteralSearch() {
  console.log(`${colors.blue}Starting literal search functionality tests...${colors.reset}`);
  
  let originalConfig;
  
  try {
    // Setup
    originalConfig = await setup();
    
    // Run all literal search tests
    await testLiteralSearchExactMatches();
    await testRegexVsLiteralDifference(); 
    await testSpecialCharactersLiteralSearch();
    await testLiteralSearchDefault();
    await testOriginalFailingCase();
    await testRegexFailureLiteralSuccess(); // NEW: Critical test showing the problem we solved
    
    console.log(`${colors.green}✅ All literal search tests passed!${colors.reset}`);
    return true;
    
  } catch (error) {
    console.error(`${colors.red}❌ Literal search test failed: ${error.message}${colors.reset}`);
    console.error(error.stack);
    throw error;
  } finally {
    // Cleanup
    if (originalConfig) {
      await teardown(originalConfig);
    }
  }
}
// Export for use in run-all-tests.js
export default testLiteralSearch;
// Run tests if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
  testLiteralSearch().then(() => {
    console.log('Literal search tests completed successfully.');
    process.exit(0);
  }).catch(error => {
    console.error('Literal search test execution failed:', error);
    process.exit(1);
  });
}