Files
extension/test/comprehensive-test.js
Hanzo Dev 0b6f26d0bf feat: Add comprehensive MCP tools and multi-platform build support
- Implement feature parity between Python and TypeScript MCP versions
- Add comprehensive test coverage for all orthogonal tools
- Consolidate tools to follow single-tool-multiple-actions pattern
- Add bash, git-search, critic, and mode tools
- Create multi-platform build system for VS Code, Claude Desktop, and DXT
- Add test files for filesystem, search, shell, process, web-fetch, mode, and git-search tools
- Fix TypeScript compilation issues and update build scripts
- Update package.json with hanzo-ai publisher ID
- Successfully build all distribution formats:
  - VS Code Extension (.vsix)
  - Claude Desktop MCP package
  - Claude Code DXT file
  - Standalone MCP server
2025-07-04 20:13:16 -04:00

541 lines
18 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Comprehensive test suite for Hanzo Extension
* Tests all tools and features
*/
const path = require('path');
const fs = require('fs').promises;
const { spawn } = require('child_process');
const Module = require('module');
// Hook require to provide vscode mock
const originalRequire = Module.prototype.require;
Module.prototype.require = function(id) {
if (id === 'vscode') {
return require('../scripts/vscode-mock');
}
return originalRequire.apply(this, arguments);
};
// Test utilities
const assert = (condition, message) => {
if (!condition) {
throw new Error(`Assertion failed: ${message}`);
}
};
const testDir = path.join(__dirname, 'test-workspace');
// Load tools
const { MCPTools } = require('../out/mcp/tools');
class TestRunner {
constructor() {
this.results = {
passed: 0,
failed: 0,
errors: []
};
this.context = null;
this.tools = null;
}
async setup() {
console.log('🔧 Setting up test environment...\n');
// Create test directory
await fs.mkdir(testDir, { recursive: true });
process.chdir(testDir);
// Create mock context
this.context = {
globalState: {
_store: new Map(),
get(key, defaultValue) {
return this._store.get(key) ?? defaultValue;
},
update(key, value) {
this._store.set(key, value);
return Promise.resolve();
}
},
workspaceState: {
get: () => undefined,
update: () => Promise.resolve()
},
extensionPath: path.dirname(__dirname),
subscriptions: [],
asAbsolutePath: (p) => path.join(path.dirname(__dirname), p)
};
// Initialize tools
this.tools = new MCPTools(this.context);
await this.tools.initialize();
console.log(`✅ Initialized ${this.tools.getAllTools().length} tools\n`);
}
async runTest(name, testFn) {
process.stdout.write(`Testing ${name}... `);
try {
await testFn();
this.results.passed++;
console.log('✅');
} catch (error) {
this.results.failed++;
this.results.errors.push({ test: name, error: error.message });
console.log('❌');
console.error(` Error: ${error.message}`);
}
}
async testFileOperations() {
console.log('\n📁 Testing File Operations\n');
// Test write
await this.runTest('write tool', async () => {
const writeTool = this.tools.getTool('write');
assert(writeTool, 'Write tool not found');
const result = await writeTool.handler({
path: 'test-file.txt',
content: 'Hello, World!'
});
assert(result.includes('successfully'), 'Write failed');
});
// Test read
await this.runTest('read tool', async () => {
const readTool = this.tools.getTool('read');
assert(readTool, 'Read tool not found');
const result = await readTool.handler({
path: 'test-file.txt'
});
assert(result.includes('Hello, World!'), 'Read failed');
});
// Test edit
await this.runTest('edit tool', async () => {
const editTool = this.tools.getTool('edit');
assert(editTool, 'Edit tool not found');
const result = await editTool.handler({
path: 'test-file.txt',
old_text: 'World',
new_text: 'Hanzo'
});
assert(result.includes('successfully'), 'Edit failed');
});
// Test multi_edit
await this.runTest('multi_edit tool', async () => {
const multiEditTool = this.tools.getTool('multi_edit');
assert(multiEditTool, 'Multi-edit tool not found');
await this.tools.getTool('write').handler({
path: 'multi-edit-test.txt',
content: 'Line 1\nLine 2\nLine 3'
});
const result = await multiEditTool.handler({
path: 'multi-edit-test.txt',
edits: [
{ old_text: 'Line 1', new_text: 'First Line' },
{ old_text: 'Line 3', new_text: 'Third Line' }
]
});
assert(result.includes('successfully'), 'Multi-edit failed');
});
// Test directory_tree
await this.runTest('directory_tree tool', async () => {
const treeTool = this.tools.getTool('directory_tree');
assert(treeTool, 'Directory tree tool not found');
// Create some test structure
await fs.mkdir('subdir', { recursive: true });
await fs.writeFile('subdir/file.txt', 'test');
const result = await treeTool.handler({
path: '.'
});
assert(result.includes('subdir'), 'Directory tree failed');
});
// Test find_files
await this.runTest('find_files tool', async () => {
const findTool = this.tools.getTool('find_files');
assert(findTool, 'Find files tool not found');
const result = await findTool.handler({
pattern: '*.txt'
});
assert(result.includes('test-file.txt'), 'Find files failed');
});
}
async testSearchOperations() {
console.log('\n🔍 Testing Search Operations\n');
// Create test content
await this.tools.getTool('write').handler({
path: 'search-test.js',
content: `
function testFunction() {
console.log('This is a test');
return 42;
}
class TestClass {
constructor() {
this.value = 'test';
}
}
`
});
// Test grep
await this.runTest('grep tool', async () => {
const grepTool = this.tools.getTool('grep');
assert(grepTool, 'Grep tool not found');
const result = await grepTool.handler({
pattern: 'test',
path: '.'
});
assert(result.includes('testFunction'), 'Grep failed');
});
// Test search
await this.runTest('search tool', async () => {
const searchTool = this.tools.getTool('search');
assert(searchTool, 'Search tool not found');
const result = await searchTool.handler({
query: 'TestClass',
type: 'all'
});
assert(typeof result === 'string', 'Search failed');
});
// Test unified_search
await this.runTest('unified_search tool', async () => {
const unifiedSearchTool = this.tools.getTool('unified_search');
assert(unifiedSearchTool, 'Unified search tool not found');
const result = await unifiedSearchTool.handler({
query: 'test',
include: ['grep', 'filename']
});
assert(result.includes('test'), 'Unified search failed');
});
}
async testShellOperations() {
console.log('\n🖥️ Testing Shell Operations\n');
// Test run_command
await this.runTest('run_command tool', async () => {
const runTool = this.tools.getTool('run_command');
assert(runTool, 'Run command tool not found');
const result = await runTool.handler({
command: 'echo "Hello from shell"'
});
assert(result.includes('Hello from shell'), 'Run command failed');
});
// Test process management
await this.runTest('process tool', async () => {
const processTool = this.tools.getTool('process');
if (!processTool) {
console.log(' (Process tool not enabled, skipping)');
return;
}
// Start a process
const startResult = await processTool.handler({
action: 'run',
command: 'sleep 2',
name: 'test-sleep'
});
assert(startResult.includes('Started'), 'Process start failed');
// List processes
const listResult = await processTool.handler({
action: 'list'
});
assert(listResult.includes('test-sleep'), 'Process list failed');
});
}
async testDevelopmentTools() {
console.log('\n🛠️ Testing Development Tools\n');
// Test todo operations
await this.runTest('todo_write tool', async () => {
const todoWriteTool = this.tools.getTool('todo_write');
assert(todoWriteTool, 'Todo write tool not found');
const result = await todoWriteTool.handler({
todos: [{
id: 'test-1',
content: 'Test todo item',
status: 'pending',
priority: 'high'
}]
});
assert(result.includes('updated'), 'Todo write failed');
});
await this.runTest('todo_read tool', async () => {
const todoReadTool = this.tools.getTool('todo_read');
assert(todoReadTool, 'Todo read tool not found');
const result = await todoReadTool.handler({});
assert(result.includes('Test todo item'), 'Todo read failed');
});
// Test think tool
await this.runTest('think tool', async () => {
const thinkTool = this.tools.getTool('think');
assert(thinkTool, 'Think tool not found');
const result = await thinkTool.handler({
thought: 'Testing the think tool',
category: 'analysis'
});
assert(result.includes('recorded'), 'Think tool failed');
});
// Test critic tool
await this.runTest('critic tool', async () => {
const criticTool = this.tools.getTool('critic');
if (!criticTool) {
console.log(' (Critic tool not enabled, skipping)');
return;
}
const result = await criticTool.handler({
code: 'function bad() { eval("dangerous"); }',
aspect: 'security'
});
assert(result.includes('Security'), 'Critic tool failed');
});
}
async testConfigurationTools() {
console.log('\n⚙️ Testing Configuration Tools\n');
// Test palette
await this.runTest('palette tool', async () => {
const paletteTool = this.tools.getTool('palette');
if (!paletteTool) {
console.log(' (Palette tool not enabled, skipping)');
return;
}
const result = await paletteTool.handler({
action: 'list'
});
assert(result.includes('minimal'), 'Palette list failed');
});
// Test rules
await this.runTest('rules tool', async () => {
const rulesTool = this.tools.getTool('rules');
if (!rulesTool) {
console.log(' (Rules tool not enabled, skipping)');
return;
}
// Create a test rules file
await fs.writeFile('.cursorrules', 'Test rules content');
const result = await rulesTool.handler({
format: 'list'
});
assert(result.includes('cursorrules'), 'Rules tool failed');
});
// Test config
await this.runTest('config tool', async () => {
const configTool = this.tools.getTool('config');
if (!configTool) {
console.log(' (Config tool not enabled, skipping)');
return;
}
// Set a config value
const setResult = await configTool.handler({
action: 'set',
key: 'test.value',
value: 'test123'
});
assert(setResult.includes('Set'), 'Config set failed');
// Get the config value
const getResult = await configTool.handler({
action: 'get',
key: 'test.value'
});
assert(getResult.includes('test123'), 'Config get failed');
});
}
async testWebTools() {
console.log('\n🌐 Testing Web Tools\n');
await this.runTest('web_fetch tool', async () => {
const webFetchTool = this.tools.getTool('web_fetch');
assert(webFetchTool, 'Web fetch tool not found');
// Test with a simple URL (will fail in offline mode but that's ok)
try {
const result = await webFetchTool.handler({
url: 'https://example.com',
format: 'metadata'
});
assert(typeof result === 'string', 'Web fetch returned invalid type');
} catch (error) {
// Expected in test environment without network
assert(true, 'Web fetch handled error correctly');
}
});
}
async testBatchOperations() {
console.log('\n🔄 Testing Batch Operations\n');
await this.runTest('batch tool', async () => {
const batchTool = this.tools.getTool('batch');
assert(batchTool, 'Batch tool not found');
const result = await batchTool.handler({
operations: [
{
tool: 'write',
args: {
path: 'batch-test-1.txt',
content: 'Batch file 1'
}
},
{
tool: 'write',
args: {
path: 'batch-test-2.txt',
content: 'Batch file 2'
}
}
]
});
assert(result.includes('completed'), 'Batch operation failed');
// Verify files were created
const files = await fs.readdir('.');
assert(files.includes('batch-test-1.txt'), 'Batch file 1 not created');
assert(files.includes('batch-test-2.txt'), 'Batch file 2 not created');
});
}
async testMCPServer() {
console.log('\n🔌 Testing MCP Server\n');
await this.runTest('MCP server build', async () => {
const buildScript = path.join(path.dirname(__dirname), 'scripts', 'build-mcp-standalone.js');
const exists = await fs.access(buildScript).then(() => true).catch(() => false);
assert(exists, 'Build script not found');
});
await this.runTest('MCP server binary', async () => {
const serverPath = path.join(path.dirname(__dirname), 'out', 'mcp-server-standalone.js');
const exists = await fs.access(serverPath).then(() => true).catch(() => false);
assert(exists, 'MCP server not built');
if (exists) {
// Test server can start
const proc = spawn('node', [serverPath, '--version'], {
timeout: 5000
});
const output = await new Promise((resolve) => {
let data = '';
proc.stdout.on('data', chunk => data += chunk);
proc.stderr.on('data', chunk => data += chunk);
proc.on('close', () => resolve(data));
});
assert(output.includes('Hanzo MCP Server'), 'MCP server version check failed');
}
});
}
async cleanup() {
console.log('\n🧹 Cleaning up test environment...');
// Go back to original directory
process.chdir(path.dirname(testDir));
// Remove test directory
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch (error) {
console.error('Failed to clean up test directory:', error.message);
}
}
async run() {
console.log('🧪 Hanzo Extension Comprehensive Test Suite\n');
console.log('=' .repeat(50) + '\n');
try {
await this.setup();
// Run all test categories
await this.testFileOperations();
await this.testSearchOperations();
await this.testShellOperations();
await this.testDevelopmentTools();
await this.testConfigurationTools();
await this.testWebTools();
await this.testBatchOperations();
await this.testMCPServer();
} catch (error) {
console.error('\n💥 Fatal error:', error);
this.results.failed++;
} finally {
await this.cleanup();
}
// Print summary
console.log('\n' + '=' .repeat(50));
console.log('\n📊 Test Summary:\n');
console.log(`✅ Passed: ${this.results.passed}`);
console.log(`❌ Failed: ${this.results.failed}`);
console.log(`📈 Total: ${this.results.passed + this.results.failed}`);
console.log(`🎯 Success Rate: ${Math.round((this.results.passed / (this.results.passed + this.results.failed)) * 100)}%`);
if (this.results.errors.length > 0) {
console.log('\n❌ Failed Tests:');
this.results.errors.forEach(({ test, error }) => {
console.log(` - ${test}: ${error}`);
});
}
if (this.results.failed === 0) {
console.log('\n🎉 All tests passed! The extension is ready for production.');
} else {
console.log('\n⚠️ Some tests failed. Please review the errors above.');
}
process.exit(this.results.failed > 0 ? 1 : 0);
}
}
// Run tests
const runner = new TestRunner();
runner.run().catch(console.error);