tree-ast-grep-mcp
Version:
Simple, direct ast-grep wrapper for AI coding agents. Zero abstractions, maximum performance.
432 lines (364 loc) • 15.3 kB
JavaScript
/**
* Advanced Error Handling Unit Tests
* Comprehensive error scenario testing
*/
import { AstGrepBinaryManager } from '../../build/core/binary-manager.js';
import { WorkspaceManager } from '../../build/core/workspace-manager.js';
import { SearchTool } from '../../build/tools/search.js';
import { ReplaceTool } from '../../build/tools/replace.js';
import { ScanTool } from '../../build/tools/scan.js';
import { BinaryError, ValidationError, ExecutionError } from '../../build/types/errors.js';
import { TestSuite, TestAssert } from '../utils/test-helpers.js';
export default async function runAdvancedErrorHandlingTests() {
const suite = new TestSuite('Advanced Error Handling Unit Tests');
// =============================================================================
// BINARY MANAGER ERROR SCENARIOS
// =============================================================================
suite.test('should handle binary initialization failures gracefully', async () => {
const manager = new AstGrepBinaryManager({
customBinaryPath: '/non/existent/path/ast-grep'
});
try {
await manager.initialize();
TestAssert.assertTrue(false, 'Should have thrown error for invalid binary path');
} catch (error) {
TestAssert.assertTrue(error instanceof BinaryError);
TestAssert.assertContains(error.message.toLowerCase(), 'binary');
}
});
suite.test('should handle corrupted binary scenarios', async () => {
const manager = new AstGrepBinaryManager({
customBinaryPath: process.execPath // Use Node.js executable as fake ast-grep
});
try {
await manager.initialize();
// Try to execute ast-grep command with Node.js
await manager.executeAstGrep(['--version']);
TestAssert.assertTrue(false, 'Should have failed with corrupted binary');
} catch (error) {
// Should handle invalid binary gracefully
TestAssert.assertTrue(error instanceof Error);
}
});
suite.test('should handle system resource exhaustion', async () => {
const manager = new AstGrepBinaryManager({ useSystem: true });
try {
await manager.initialize();
// Try to exhaust resources with multiple concurrent operations
const operations = Array(100).fill().map(() =>
manager.executeAstGrep(['--version'], { timeout: 1 })
);
const results = await Promise.allSettled(operations);
// Some should succeed, some may fail due to resource limits
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
TestAssert.assertTrue(succeeded + failed === 100);
// Failed operations should have proper error types
results.filter(r => r.status === 'rejected').forEach(result => {
TestAssert.assertTrue(result.reason instanceof Error);
});
} catch (error) {
if (error.message.includes('ast-grep not found')) {
console.log(' ⚠️ Skipping resource test - ast-grep not available');
return;
}
throw error;
}
});
// =============================================================================
// WORKSPACE MANAGER ERROR SCENARIOS
// =============================================================================
suite.test('should handle invalid workspace configurations', async () => {
const invalidPaths = [
'/non/existent/path',
'',
null,
undefined,
'../../../../../../etc/passwd',
'C:\\Windows\\System32\\config\\SAM'
];
for (const invalidPath of invalidPaths) {
try {
const manager = new WorkspaceManager(invalidPath);
const root = manager.getWorkspaceRoot();
// Should either handle gracefully or provide a safe fallback
TestAssert.assertTrue(typeof root === 'string');
TestAssert.assertTrue(root.length > 0);
} catch (error) {
// Security or validation errors are acceptable
TestAssert.assertTrue(error instanceof Error);
}
}
});
suite.test('should handle path validation edge cases', async () => {
const manager = new WorkspaceManager();
const edgeCases = [
'',
' ',
'\n\t\r',
null,
undefined,
'../../../etc/passwd',
'file:///etc/passwd',
'http://example.com/malicious',
'C:\\Windows\\System32\\drivers\\etc\\hosts',
'\\\\server\\share\\file',
'/dev/null',
'/proc/self/mem'
];
for (const testCase of edgeCases) {
try {
const result = manager.validatePath(testCase);
// Should always return an object with validation result
TestAssert.assertTrue(typeof result === 'object');
TestAssert.assertTrue('valid' in result);
TestAssert.assertTrue(typeof result.valid === 'boolean');
if (!result.valid) {
TestAssert.assertTrue('error' in result);
}
} catch (error) {
// Throwing is also acceptable for dangerous paths
TestAssert.assertTrue(error instanceof Error);
}
}
});
suite.test('should handle filesystem permission errors', async () => {
const manager = new WorkspaceManager();
// Test paths that might have permission issues
const restrictedPaths = [
'/root',
'/etc/shadow',
'C:\\Windows\\System32',
'/System/Library'
];
for (const restrictedPath of restrictedPaths) {
const result = manager.validatePath(restrictedPath);
TestAssert.assertTrue(typeof result === 'object');
TestAssert.assertTrue('valid' in result);
// Either valid (if accessible) or invalid with proper error
if (!result.valid) {
TestAssert.assertTrue('error' in result);
TestAssert.assertTrue(typeof result.error === 'string');
}
}
});
// =============================================================================
// TOOL ERROR SCENARIOS
// =============================================================================
suite.test('should handle malformed search parameters', async () => {
const binaryManager = new AstGrepBinaryManager({ useSystem: true });
const workspaceManager = new WorkspaceManager();
const searchTool = new SearchTool(binaryManager, workspaceManager);
const malformedParams = [
{ pattern: null },
{ pattern: undefined },
{ pattern: '' },
{ pattern: 123 },
{ pattern: {} },
{ pattern: [] },
{ pattern: 'console.log($ARG)', language: null },
{ pattern: 'console.log($ARG)', language: 123 },
{ pattern: 'console.log($ARG)', code: null, language: 'javascript' },
{ pattern: 'console.log($ARG)', paths: 'not-an-array' },
{ pattern: 'console.log($ARG)', context: 'not-a-number' },
{ pattern: 'console.log($ARG)', maxMatches: -1 },
{ pattern: 'console.log($ARG)', timeoutMs: 'invalid' }
];
for (const params of malformedParams) {
try {
await searchTool.execute(params);
// If it doesn't throw, check that it returns valid structure
TestAssert.assertTrue(false, `Should have thrown for params: ${JSON.stringify(params)}`);
} catch (error) {
TestAssert.assertTrue(error instanceof ValidationError || error instanceof ExecutionError);
}
}
});
suite.test('should handle malformed replacement parameters', async () => {
const binaryManager = new AstGrepBinaryManager({ useSystem: true });
const workspaceManager = new WorkspaceManager();
const replaceTool = new ReplaceTool(binaryManager, workspaceManager);
const malformedParams = [
{ pattern: 'console.log($ARG)' }, // Missing replacement
{ replacement: 'logger.info($ARG)' }, // Missing pattern
{ pattern: '', replacement: 'logger.info($ARG)' },
{ pattern: null, replacement: 'logger.info($ARG)' },
{ pattern: 'console.log($ARG)', replacement: null },
{ pattern: 'console.log($ARG)', replacement: '', language: 'javascript' },
{ pattern: 123, replacement: 'logger.info($ARG)' },
{ pattern: 'console.log($ARG)', replacement: 123 }
];
for (const params of malformedParams) {
try {
await replaceTool.execute(params);
TestAssert.assertTrue(false, `Should have thrown for params: ${JSON.stringify(params)}`);
} catch (error) {
TestAssert.assertTrue(error instanceof ValidationError || error instanceof ExecutionError);
}
}
});
suite.test('should handle malformed scan parameters', async () => {
const binaryManager = new AstGrepBinaryManager({ useSystem: true });
const workspaceManager = new WorkspaceManager();
const scanTool = new ScanTool(binaryManager, workspaceManager);
const malformedParams = [
{}, // Missing rule and ruleFile
{ rule: '' },
{ rule: null },
{ rule: 123 },
{ rule: {} },
{ ruleFile: '/non/existent/file.yml' },
{ rule: 'invalid yaml }{{}' },
{ rule: 'rule:\n pattern: console.log\n but-invalid-yaml: }{' }
];
for (const params of malformedParams) {
try {
await scanTool.execute(params);
TestAssert.assertTrue(false, `Should have thrown for params: ${JSON.stringify(params)}`);
} catch (error) {
TestAssert.assertTrue(error instanceof ValidationError || error instanceof ExecutionError);
}
}
});
// =============================================================================
// MEMORY AND RESOURCE EXHAUSTION TESTS
// =============================================================================
suite.test('should handle memory pressure gracefully', async () => {
const binaryManager = new AstGrepBinaryManager({ useSystem: true });
const workspaceManager = new WorkspaceManager();
const searchTool = new SearchTool(binaryManager, workspaceManager);
try {
await binaryManager.initialize();
// Create very large code string
const hugeCode = 'console.log("test");\n'.repeat(100000);
const params = {
pattern: 'console.log($ARG)',
language: 'javascript',
code: hugeCode,
timeoutMs: 5000
};
const result = await searchTool.execute(params);
// Should either complete or timeout gracefully
TestAssert.assertTrue(typeof result === 'object');
TestAssert.assertTrue('matches' in result);
} catch (error) {
// Memory errors, timeouts, or missing binary are acceptable
TestAssert.assertTrue(error instanceof Error);
TestAssert.assertTrue(
error.message.includes('timeout') ||
error.message.includes('memory') ||
error.message.includes('ast-grep not found') ||
error instanceof ExecutionError
);
}
});
// =============================================================================
// CONCURRENT ACCESS AND RACE CONDITIONS
// =============================================================================
suite.test('should handle concurrent tool operations safely', async () => {
const binaryManager = new AstGrepBinaryManager({ useSystem: true });
const workspaceManager = new WorkspaceManager();
const searchTool = new SearchTool(binaryManager, workspaceManager);
try {
await binaryManager.initialize();
// Launch multiple concurrent operations
const operations = [];
for (let i = 0; i < 10; i++) {
operations.push(
searchTool.execute({
pattern: 'console.log($ARG)',
language: 'javascript',
code: `console.log("Concurrent test ${i}");`
})
);
}
const results = await Promise.allSettled(operations);
// Check that all operations completed (successfully or with proper errors)
TestAssert.assertEqual(results.length, 10);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
TestAssert.assertTrue(typeof result.value === 'object');
} else {
TestAssert.assertTrue(result.reason instanceof Error);
}
});
} catch (error) {
if (error.message.includes('ast-grep not found')) {
console.log(' ⚠️ Skipping concurrent test - ast-grep not available');
return;
}
throw error;
}
});
// =============================================================================
// ERROR RECOVERY AND CLEANUP TESTS
// =============================================================================
suite.test('should clean up resources after errors', async () => {
const binaryManager = new AstGrepBinaryManager({ useSystem: true });
try {
await binaryManager.initialize();
// Simulate operations that might fail
const failingOperations = [
() => binaryManager.executeAstGrep(['--invalid-flag']),
() => binaryManager.executeAstGrep(['run', '--pattern']), // Missing pattern
() => binaryManager.executeAstGrep([]), // Empty args
];
for (const operation of failingOperations) {
try {
await operation();
TestAssert.assertTrue(false, 'Operation should have failed');
} catch (error) {
// Should be proper error type
TestAssert.assertTrue(error instanceof Error);
// After error, binary manager should still be usable
try {
await binaryManager.executeAstGrep(['--version']);
TestAssert.assertTrue(true, 'Binary manager recovered after error');
} catch (recoveryError) {
// If ast-grep is not available, that's also acceptable
if (!recoveryError.message.includes('ast-grep not found')) {
throw recoveryError;
}
}
}
}
} catch (error) {
if (error.message.includes('ast-grep not found')) {
console.log(' ⚠️ Skipping cleanup test - ast-grep not available');
return;
}
throw error;
}
});
// =============================================================================
// ERROR MESSAGE QUALITY TESTS
// =============================================================================
suite.test('should provide helpful error messages', async () => {
const binaryManager = new AstGrepBinaryManager({ useSystem: true });
const workspaceManager = new WorkspaceManager();
const searchTool = new SearchTool(binaryManager, workspaceManager);
const testCases = [
{
params: { pattern: '' },
expectedContent: ['pattern', 'required']
},
{
params: { pattern: 'console.log($ARG)', code: 'test' },
expectedContent: ['language', 'required']
}
];
for (const testCase of testCases) {
try {
await searchTool.execute(testCase.params);
TestAssert.assertTrue(false, 'Should have thrown validation error');
} catch (error) {
TestAssert.assertTrue(error instanceof ValidationError);
// Check that error message contains expected content
testCase.expectedContent.forEach(content => {
TestAssert.assertContains(error.message.toLowerCase(), content.toLowerCase());
});
}
}
});
return suite.run();
}