Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
qa-direct-test.jsโ€ข14.3 kB
#!/usr/bin/env node /** * Direct MCP SDK QA Test Runner for DollhouseMCP * * Tests all MCP tools directly via the SDK without the Inspector * Addresses Issue #629 - Comprehensive QA Testing Process */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { writeFileSync, mkdirSync } from 'fs'; import { spawn } from 'child_process'; import { discoverAvailableToolsDirect, validateToolExists, calculateAccurateSuccessRate, createTestResult, logTestResult, isCI, ensureDirectoryExists } from './qa-utils.js'; import { CONFIG, isCI as configIsCI } from '../test-config.js'; import { TestDataCleanup } from './qa-cleanup-manager.js'; import { QAMetricsCollector } from './qa-metrics-collector.js'; class DirectMCPTestRunner { constructor() { this.results = []; this.startTime = new Date(); this.client = null; this.transport = null; this.availableTools = []; // Initialize as empty array to prevent race conditions this.isCI = isCI(); // Initialize cleanup manager with unique test run ID this.testCleanup = new TestDataCleanup(`QA_DIRECT_TEST_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`); // Initialize metrics collector this.metricsCollector = new QAMetricsCollector(`QA_DIRECT_${Date.now()}`); if (this.isCI) { console.log('๐Ÿค– Running in CI environment'); console.log(`๐Ÿ“ TEST_PERSONAS_DIR: ${process.env.TEST_PERSONAS_DIR}`); } } async connect() { console.log('๐Ÿ”— Connecting to MCP server...'); this.transport = new StdioClientTransport({ command: "./node_modules/.bin/tsx", args: ["src/index.ts"], cwd: process.cwd() }); this.client = new Client({ name: "qa-test-client", version: "1.0.0" }, { capabilities: {} }); await this.client.connect(this.transport); console.log('โœ… Connected to MCP server'); } async discoverAvailableTools() { const toolDiscoveryStartTime = Date.now(); this.availableTools = await discoverAvailableToolsDirect(this.client); const toolDiscoveryEndTime = Date.now(); this.metricsCollector.recordToolDiscovery(toolDiscoveryStartTime, toolDiscoveryEndTime, this.availableTools.length); return this.availableTools; } validateToolExists(toolName) { return validateToolExists(toolName, this.availableTools); } async callTool(toolName, args = {}) { const startTime = Date.now(); let success = false; let error = null; let result = null; let skipped = false; try { // Check if tool exists before calling if (!this.validateToolExists(toolName)) { skipped = true; error = 'Tool not available'; return createTestResult(toolName, args, startTime, false, null, error, true); } // Set server connection timeout const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool call timed out after ${CONFIG.timeouts.server_connection/1000}s`)), CONFIG.timeouts.server_connection) ); result = await Promise.race([ this.client.callTool({ name: toolName, arguments: args }), timeoutPromise ]); success = true; return createTestResult(toolName, args, startTime, true, result.content); } catch (err) { success = false; error = err.message; return createTestResult(toolName, args, startTime, false, null, error); } finally { const endTime = Date.now(); this.metricsCollector.recordTestExecution(toolName, args, startTime, endTime, success, error, skipped); } } async testElementListing() { console.log('\n๐Ÿ” Testing Element Listing...'); const elementTypes = ['personas', 'skills', 'templates', 'agents']; for (const type of elementTypes) { const result = await this.callTool('list_elements', { type }); this.results.push(result); if (result.success) { // Try to count items from the result const text = result.result?.[0]?.text || ''; const count = text.match(/Available \w+ \((\d+)\):/)?.[1] || 'unknown'; console.log(` โœ… ${type}: ${count} items (${result.duration}ms)`); } else { console.log(` โŒ ${type}: ${result.error} (${result.duration}ms)`); } } } async testCollectionBrowsing() { console.log('\n๐Ÿช Testing Collection Browsing...'); const tests = [ { name: 'Browse All Elements', tool: 'browse_collection', params: {} }, { name: 'Browse Personas', tool: 'browse_collection', params: { type: 'personas' } }, { name: 'Search Creative', tool: 'search_collection', params: { query: 'creative' } } ]; for (const test of tests) { const result = await this.callTool(test.tool, test.params); this.results.push(result); if (result.skipped) { console.log(` โš ๏ธ ${test.name}: Skipped - ${result.error} (${result.duration}ms)`); } else if (result.success) { console.log(` โœ… ${test.name}: Success (${result.duration}ms)`); } else { console.log(` โŒ ${test.name}: ${result.error} (${result.duration}ms)`); } } } async testUserIdentity() { console.log('\n๐Ÿ‘ค Testing User Identity...'); // Get current identity let result = await this.callTool('get_user_identity'); this.results.push(result); console.log(` โœ… Get Identity: ${result.success ? 'Success' : result.error} (${result.duration}ms)`); // Set test identity with QA_TEST_ prefix const testUsername = 'QA_TEST_USER_qa-direct-test-user'; result = await this.callTool('set_user_identity', { username: testUsername }); this.results.push(result); console.log(` โœ… Set Identity: ${result.success ? 'Success' : result.error} (${result.duration}ms)`); // Track test user identity for cleanup if (result.success) { this.testCleanup.trackArtifact('persona', testUsername, null, { type: 'test_user_identity', created_by: 'qa-direct-test' }); } // Verify identity was set result = await this.callTool('get_user_identity'); this.results.push(result); console.log(` โœ… Verify Identity: ${result.success ? 'Success' : result.error} (${result.duration}ms)`); } async testElementOperations() { console.log('\n๐ŸŽญ Testing Element Operations (Personas)...'); // Get active elements first (new tool) let result = await this.callTool('get_active_elements', { type: 'personas' }); this.results.push(result); if (result.skipped) { console.log(` โš ๏ธ Get Active Elements (initial): Skipped - ${result.error} (${result.duration}ms)`); } else { console.log(` โœ… Get Active Elements (initial): ${result.success ? 'Success' : result.error} (${result.duration}ms)`); } // Try to activate Creative Writer (new tool) result = await this.callTool('activate_element', { name: 'Creative Writer', type: 'personas' }); this.results.push(result); if (result.skipped) { console.log(` โš ๏ธ Activate Creative Writer: Skipped - ${result.error} (${result.duration}ms)`); } else { console.log(` โœ… Activate Creative Writer: ${result.success ? 'Success' : result.error} (${result.duration}ms)`); } // Get active elements again (new tool) result = await this.callTool('get_active_elements', { type: 'personas' }); this.results.push(result); if (result.skipped) { console.log(` โš ๏ธ Get Active Elements (after activation): Skipped - ${result.error} (${result.duration}ms)`); } else { console.log(` โœ… Get Active Elements (after activation): ${result.success ? 'Success' : result.error} (${result.duration}ms)`); } // Deactivate element (new tool) result = await this.callTool('deactivate_element', { name: 'Creative Writer', type: 'personas' }); this.results.push(result); if (result.skipped) { console.log(` โš ๏ธ Deactivate Element: Skipped - ${result.error} (${result.duration}ms)`); } else { console.log(` โœ… Deactivate Element: ${result.success ? 'Success' : result.error} (${result.duration}ms)`); } } async testErrorHandling() { console.log('\nโš ๏ธ Testing Error Handling...'); // Test with invalid parameters const tests = [ { tool: 'list_elements', params: { type: 'invalid_type' } }, { tool: 'activate_element', params: { name: 'NonExistentElement', type: 'personas' } } ]; for (const test of tests) { const result = await this.callTool(test.tool, test.params); this.results.push(result); if (!result.success) { console.log(` โœ… Expected error for ${test.tool}: ${result.error} (${result.duration}ms)`); } else { console.log(` โš ๏ธ Expected error but got success for ${test.tool} (${result.duration}ms)`); } } } calculateAccurateSuccessRate(results) { return calculateAccurateSuccessRate(results); } generateReport() { const endTime = new Date(); const duration = endTime - this.startTime; const stats = this.calculateAccurateSuccessRate(this.results); const totalTests = this.results.length; const report = { timestamp: endTime.toISOString(), duration: `${duration}ms`, tool_discovery: { available_tools_count: this.availableTools.length, available_tools: this.availableTools }, summary: { total_tests: totalTests, executed_tests: stats.total, skipped_tests: stats.skipped, successful_tests: stats.successful, failed_tests: stats.total - stats.successful, success_rate: `${stats.percentage}%`, success_rate_note: "Based only on executed tests (excludes skipped)" }, test_details: this.results.map(r => ({ tool: r.tool, success: r.success, skipped: r.skipped || false, duration: `${r.duration}ms`, params: r.params, error: r.error || null })), full_results: this.results }; // Ensure directory exists mkdirSync('docs/QA', { recursive: true }); const filename = `qa-direct-test-results-${new Date().toISOString().slice(0, 19).replaceAll(/[:.]/g, '-')}.json`; const filepath = `docs/QA/${filename}`; // Track test result file for cleanup this.testCleanup.trackArtifact('result', filename, filepath, { type: 'test_results', created_by: 'qa-direct-test' }); writeFileSync(filepath, JSON.stringify(report, null, 2)); console.log(`\n๐Ÿ“Š Test Summary:`); console.log(` Available Tools: ${this.availableTools.length}`); console.log(` Total Tests: ${totalTests}`); console.log(` Executed Tests: ${stats.total}`); console.log(` Skipped Tests: ${stats.skipped}`); console.log(` Successful: ${stats.successful}`); console.log(` Failed: ${stats.total - stats.successful}`); console.log(` Success Rate: ${stats.percentage}% (based on executed tests only)`); console.log(` Duration: ${report.duration}`); console.log(` Report: docs/QA/${filename}`); return report; } async performCleanup() { console.log('\n๐Ÿงน Performing direct test cleanup...'); try { const cleanupResults = await this.testCleanup.cleanupAll(); console.log(`โœ… Direct test cleanup completed: ${cleanupResults.cleaned} items cleaned, ${cleanupResults.failed} failed`); } catch (error) { console.warn(`โš ๏ธ Direct test cleanup failed: ${error.message}`); } } async disconnect() { if (this.client && this.transport) { await this.client.close(); console.log('๐Ÿ”Œ Disconnected from MCP server'); } } async runFullTestSuite() { console.log('๐Ÿš€ Starting Direct MCP QA Test Suite...'); console.log(`๐Ÿงน Test cleanup ID: ${this.testCleanup.testRunId}`); console.log(`๐Ÿ“Š Metrics collector ID: ${this.metricsCollector.testRunId}`); // Start metrics collection this.metricsCollector.startCollection(); let report = null; try { await this.connect(); await this.discoverAvailableTools(); // Ensure availableTools is properly initialized before validation if (!Array.isArray(this.availableTools)) { this.availableTools = []; } await this.testElementListing(); await this.testCollectionBrowsing(); await this.testUserIdentity(); await this.testElementOperations(); await this.testErrorHandling(); report = this.generateReport(); // End metrics collection and generate metrics report this.metricsCollector.endCollection(); const metricsReport = this.metricsCollector.generateReport(); if (metricsReport.filepath) { console.log(`๐Ÿ“Š Direct test metrics saved to: ${metricsReport.filepath}`); } return report; } catch (error) { console.error('โŒ Test suite failed:', error.message); // End metrics collection even on failure this.metricsCollector.endCollection(); const metricsReport = this.metricsCollector.generateReport(); if (metricsReport.filepath) { console.log(`๐Ÿ“Š Partial direct test metrics saved: ${metricsReport.filepath}`); } return null; } finally { // CRITICAL: Always attempt cleanup and disconnection try { await this.performCleanup(); } catch (cleanupError) { console.error(`โŒ CRITICAL: Direct test cleanup failed: ${cleanupError.message}`); } try { await this.disconnect(); } catch (disconnectError) { console.error(`โš ๏ธ Disconnect error: ${disconnectError.message}`); } } } } // Run if called directly if (import.meta.url === `file://${process.argv[1]}`) { const runner = new DirectMCPTestRunner(); runner.runFullTestSuite().then(report => { process.exit(report && report.summary.success_rate !== '0.0%' ? 0 : 1); }); } export { DirectMCPTestRunner };

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/DollhouseMCP/DollhouseMCP'

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