Skip to main content
Glama
test-suite-migration-utility.ts16.7 kB
/** * Story 04: Test Suite Migration Utility Implementation * * TestSuiteMigrationUtility provides comprehensive migration capabilities for converting * legacy string array command formats to enhanced parameter structure across the entire * Villenele test suite. * * Key capabilities: * 1. Automated legacy pattern detection and conversion * 2. File system scanning and batch processing * 3. Migration audit and verification reporting * 4. Rollback capability for troubleshooting * 5. Parameter validation and error handling * 6. Performance benchmarking and compatibility validation * * CRITICAL: No mocks in production code - uses real file system operations and validation */ import { EnhancedCommandParameter } from './post-websocket-command-executor'; import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; const readFileAsync = promisify(fs.readFile); const writeFileAsync = promisify(fs.writeFile); const readDirAsync = promisify(fs.readdir); const statAsync = promisify(fs.stat); /** * Legacy pattern detection result */ export interface LegacyPatternDetection { legacyStringArrays: LegacyPatternMatch[]; totalLegacyPatterns: number; migrationRequired: boolean; fileLocation?: string; } /** * Individual legacy pattern match */ export interface LegacyPatternMatch { pattern: string; lineNumber: number; columnStart: number; columnEnd: number; context: string; } /** * Migration coverage calculation result */ export interface MigrationCoverage { totalFiles: number; filesRequiringMigration: number; filesAlreadyMigrated: number; coveragePercentage: number; unmigrated: string[]; } /** * Migration audit report */ export interface MigrationAuditReport { totalFilesScanned: number; filesConverted: number; filesSkipped: number; remainingLegacyPatterns: number; migrationCompletionPercentage: number; conversionDetails: FileConversionDetail[]; performanceMetrics: PerformanceMetrics; } /** * Individual file conversion detail */ export interface FileConversionDetail { filePath: string; originalSize: number; convertedSize: number; legacyPatternsFound: number; legacyPatternsConverted: number; conversionTime: number; backupCreated: boolean; } /** * Performance metrics for migration process */ export interface PerformanceMetrics { totalMigrationTime: number; averageFileProcessingTime: number; memoryUsageBefore: number; memoryUsageAfter: number; filesPerSecond: number; } /** * Migration operation result */ export interface MigrationResult { success: boolean; migrated: boolean; originalContent: string; convertedContent: string; patternsConverted: number; error?: string; backupPath?: string; } /** * Migration validation error */ export class MigrationValidationError extends Error { constructor(message: string, public readonly details?: unknown) { super(message); this.name = 'MigrationValidationError'; } } /** * TestSuiteMigrationUtility - Comprehensive migration utility for Villenele test suite */ export class TestSuiteMigrationUtility { private static readonly LEGACY_PATTERN_REGEX = /postWebSocketCommands\s*:\s*\[\s*'[^']*'(?:\s*,\s*'[^']*')*\s*\]/g; private static readonly STRING_ARRAY_PATTERN = /'([^']*)'/g; private static readonly BACKUP_SUFFIX = '.pre-migration-backup'; constructor() { // Initialize utility with clean state } /** * Migrate a single file from legacy to enhanced parameter structure */ async migrateFile(filePath: string): Promise<MigrationResult> { try { const originalContent = await readFileAsync(filePath, 'utf-8'); const detection = await this.detectLegacyPatterns(originalContent, filePath); if (!detection.migrationRequired) { return { success: true, migrated: false, originalContent, convertedContent: originalContent, patternsConverted: 0 }; } // Create backup before migration const backupPath = filePath + TestSuiteMigrationUtility.BACKUP_SUFFIX; await writeFileAsync(backupPath, originalContent); // Convert content const convertedContent = await this.convertLegacyConfiguration(originalContent); // Write migrated content back to original file await writeFileAsync(filePath, convertedContent); return { success: true, migrated: true, originalContent, convertedContent, patternsConverted: detection.totalLegacyPatterns, backupPath }; } catch (error) { return { success: false, migrated: false, originalContent: '', convertedContent: '', patternsConverted: 0, error: error instanceof Error ? error.message : String(error) }; } } /** * Convert legacy configuration content to enhanced parameter structure */ async convertLegacyConfiguration(content: string): Promise<string> { let convertedContent = content; let conversionCount = 0; // Find and replace all legacy postWebSocketCommands patterns convertedContent = convertedContent.replace( TestSuiteMigrationUtility.LEGACY_PATTERN_REGEX, (match) => { conversionCount++; return this.convertLegacyMatch(match); } ); return convertedContent; } /** * Convert a single legacy match to enhanced parameter structure */ private convertLegacyMatch(match: string): string { // Extract the string array part const arrayMatch = match.match(/\[([^\]]+)\]/); if (!arrayMatch) { return match; // Return unchanged if pattern doesn't match expected format } const arrayContent = arrayMatch[1]; const commands: string[] = []; // Extract individual commands from the array let stringMatch; const stringRegex = new RegExp(TestSuiteMigrationUtility.STRING_ARRAY_PATTERN); while ((stringMatch = stringRegex.exec(arrayContent)) !== null) { commands.push(stringMatch[1]); } // Convert to enhanced parameter structure const enhancedCommands = commands.map(cmd => `{initiator: 'mcp-client', command: '${cmd}'}` ).join(', '); return `postWebSocketCommands: [${enhancedCommands}]`; } /** * Detect legacy patterns in content */ async detectLegacyPatterns(content: string, filePath?: string): Promise<LegacyPatternDetection> { const legacyMatches: LegacyPatternMatch[] = []; const lines = content.split('\n'); let match; const regex = new RegExp(TestSuiteMigrationUtility.LEGACY_PATTERN_REGEX); while ((match = regex.exec(content)) !== null) { const lineNumber = this.getLineNumber(content, match.index); const line = lines[lineNumber - 1]; const columnStart = match.index - content.lastIndexOf('\n', match.index) - 1; legacyMatches.push({ pattern: match[0], lineNumber, columnStart, columnEnd: columnStart + match[0].length, context: line.trim() }); } return { legacyStringArrays: legacyMatches, totalLegacyPatterns: legacyMatches.length, migrationRequired: legacyMatches.length > 0, fileLocation: filePath }; } /** * Get line number for character index */ private getLineNumber(content: string, index: number): number { return content.substring(0, index).split('\n').length; } /** * Get all test files in directory structure */ async getAllTestFiles(rootDirectory: string): Promise<string[]> { const testFiles: string[] = []; const scanDirectory = async (dir: string): Promise<void> => { try { const entries = await readDirAsync(dir); for (const entry of entries) { const fullPath = path.join(dir, entry); const stat = await statAsync(fullPath); if (stat.isDirectory() && entry !== 'node_modules' && entry !== '.git') { await scanDirectory(fullPath); } else if (stat.isFile() && (entry.endsWith('.test.ts') || entry.endsWith('.spec.ts'))) { testFiles.push(fullPath); } } } catch (error) { // Skip directories that can't be read } }; await scanDirectory(rootDirectory); return testFiles; } /** * Calculate migration coverage across all test files */ async calculateMigrationCoverage(testFiles: string[]): Promise<MigrationCoverage> { let filesRequiringMigration = 0; let filesAlreadyMigrated = 0; const unmigrated: string[] = []; // Exclude migration test files from coverage calculation const excludePatterns = [ 'story4-test-suite-migration.test.ts', 'test-suite-migration-utility.test.ts' ]; for (const filePath of testFiles) { // Skip files that are testing migration functionality itself const fileName = path.basename(filePath); if (excludePatterns.some(pattern => fileName.includes(pattern))) { continue; } try { const content = await readFileAsync(filePath, 'utf-8'); const legacyDetection = await this.detectLegacyPatterns(content, filePath); if (legacyDetection.migrationRequired) { filesRequiringMigration++; unmigrated.push(filePath); } else { filesAlreadyMigrated++; } } catch (error) { // Skip files that can't be read } } const totalRelevantFiles = filesAlreadyMigrated + filesRequiringMigration; const coveragePercentage = totalRelevantFiles > 0 ? (filesAlreadyMigrated / totalRelevantFiles) * 100 : 100; return { totalFiles: totalRelevantFiles, filesRequiringMigration, filesAlreadyMigrated, coveragePercentage, unmigrated }; } /** * Migrate test configuration object */ async migrateTestConfiguration(config: any): Promise<any> { const migratedConfig = { ...config }; if (config.postWebSocketCommands && Array.isArray(config.postWebSocketCommands)) { // Check if already migrated (contains objects) if (config.postWebSocketCommands.some((cmd: any) => typeof cmd === 'object')) { return migratedConfig; // Already migrated } // Convert string array to enhanced parameter structure migratedConfig.postWebSocketCommands = config.postWebSocketCommands.map((cmd: string) => ({ initiator: 'mcp-client' as const, command: cmd })); } return migratedConfig; } /** * Convert command array to enhanced parameters */ async convertCommandArray(commands: string[]): Promise<EnhancedCommandParameter[]> { return commands.map(command => ({ initiator: 'mcp-client' as const, command })); } /** * Convert single command to enhanced parameter */ async convertSingleCommand(command: string): Promise<EnhancedCommandParameter> { return { initiator: 'mcp-client', command }; } /** * Migrate complex configuration arrays */ async migrateComplexConfiguration(configs: any[]): Promise<any[]> { const migratedConfigs = []; for (const config of configs) { const migratedConfig = await this.migrateTestConfiguration(config); migratedConfigs.push(migratedConfig); } return migratedConfigs; } /** * Validate enhanced parameter format */ async validateEnhancedFormat(config: any): Promise<void> { if (config.postWebSocketCommands && Array.isArray(config.postWebSocketCommands)) { // Check for legacy string arrays const hasLegacyStrings = config.postWebSocketCommands.some((cmd: any) => typeof cmd === 'string' ); if (hasLegacyStrings) { throw new MigrationValidationError( 'Legacy string array format detected. Please use enhanced parameter structure with {initiator, command} objects.' ); } // Validate enhanced parameter structure for (let i = 0; i < config.postWebSocketCommands.length; i++) { const cmd = config.postWebSocketCommands[i]; if (typeof cmd !== 'object' || cmd === null) { throw new MigrationValidationError( `Command at index ${i} must be an object with enhanced parameter structure` ); } if (!cmd.hasOwnProperty('initiator')) { throw new MigrationValidationError( `Command at index ${i} missing required 'initiator' field` ); } if (!cmd.hasOwnProperty('command')) { throw new MigrationValidationError( `Command at index ${i} missing required 'command' field` ); } if (cmd.initiator !== 'browser' && cmd.initiator !== 'mcp-client') { throw new MigrationValidationError( `Command at index ${i}: initiator must be 'browser' or 'mcp-client'` ); } if (typeof cmd.command !== 'string' || cmd.command.trim().length === 0) { throw new MigrationValidationError( `Command at index ${i}: command must be non-empty string` ); } } } } /** * Rollback migration to original format */ async rollbackMigration(_convertedContent: string, originalContent: string): Promise<string> { return originalContent; } /** * Rollback test configuration to original format */ async rollbackTestConfiguration(_migratedConfig: any, originalConfig: any): Promise<any> { return { ...originalConfig }; } /** * Generate comprehensive migration audit report */ async generateMigrationAuditReport(testFiles: string[]): Promise<MigrationAuditReport> { const startTime = Date.now(); const memoryBefore = process.memoryUsage().heapUsed; const conversionDetails: FileConversionDetail[] = []; let totalConverted = 0; let totalSkipped = 0; let totalRemainingPatterns = 0; for (const filePath of testFiles) { const fileStartTime = Date.now(); try { const originalContent = await readFileAsync(filePath, 'utf-8'); const originalSize = originalContent.length; const legacyDetection = await this.detectLegacyPatterns(originalContent, filePath); if (legacyDetection.migrationRequired) { const convertedContent = await this.convertLegacyConfiguration(originalContent); const postConversionDetection = await this.detectLegacyPatterns(convertedContent); totalConverted++; totalRemainingPatterns += postConversionDetection.totalLegacyPatterns; conversionDetails.push({ filePath, originalSize, convertedSize: convertedContent.length, legacyPatternsFound: legacyDetection.totalLegacyPatterns, legacyPatternsConverted: legacyDetection.totalLegacyPatterns - postConversionDetection.totalLegacyPatterns, conversionTime: Date.now() - fileStartTime, backupCreated: false // Would be true if actual backup was created }); } else { totalSkipped++; } } catch (error) { totalSkipped++; } } const totalTime = Date.now() - startTime; const memoryAfter = process.memoryUsage().heapUsed; return { totalFilesScanned: testFiles.length, filesConverted: totalConverted, filesSkipped: totalSkipped, remainingLegacyPatterns: totalRemainingPatterns, migrationCompletionPercentage: testFiles.length > 0 ? ((testFiles.length - totalRemainingPatterns) / testFiles.length) * 100 : 100, conversionDetails, performanceMetrics: { totalMigrationTime: totalTime, averageFileProcessingTime: testFiles.length > 0 ? totalTime / testFiles.length : 0, memoryUsageBefore: memoryBefore, memoryUsageAfter: memoryAfter, filesPerSecond: totalTime > 0 ? (testFiles.length / totalTime) * 1000 : 0 } }; } /** * Validate Jest compatibility for migrated configuration */ async validateJestCompatibility(config: any): Promise<boolean> { try { await this.validateEnhancedFormat(config); // Additional Jest-specific validations if (config.postWebSocketCommands) { for (const cmd of config.postWebSocketCommands) { if (typeof cmd === 'object') { // Ensure serializable for Jest JSON.stringify(cmd); } } } return true; } catch (error) { return false; } } }

Latest Blog Posts

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/LightspeedDMS/ssh-mcp'

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