veza/fixtures/tools/cli/index.ts
2025-12-03 22:56:50 +01:00

718 lines
No EOL
24 KiB
TypeScript

#!/usr/bin/env node
import { Command } from 'commander'
import chalk from 'chalk'
import ora from 'ora'
import inquirer from 'inquirer'
import { loadConfig, setGlobalConfig } from '../../core/config'
import { DataRelationManager } from '../../core/utils/data-relations'
import { WebFixtures } from '../../services/web'
import { ChatServerFixtures } from '../../services/chat-server'
import { StreamServerFixtures } from '../../services/stream-server'
import { NewUserOnboardingScenario } from '../../scenarios/user-journey/new-user-onboarding'
import { HighLoadScenario } from '../../scenarios/performance/high-load'
import { CrossServiceCommunicationScenario } from '../../scenarios/integration/cross-service-communication'
/**
* Veza Fixtures CLI Tool
*
* Comprehensive command-line interface for managing fixtures across the entire Veza Platform
*/
const program = new Command()
// CLI Configuration
program
.name('veza-fixtures')
.description('🎵 Veza Platform Fixtures Management CLI')
.version('1.0.0')
/**
* Generate Command - Create fixture data
*/
program
.command('generate')
.alias('gen')
.description('Generate fixture data for Veza Platform')
.option('-e, --env <environment>', 'Target environment', 'development')
.option('-u, --users <count>', 'Number of users to generate', '50')
.option('-t, --tracks <count>', 'Number of tracks to generate', '200')
.option('-p, --playlists <count>', 'Number of playlists to generate', '30')
.option('-c, --conversations <count>', 'Number of conversations to generate', '20')
.option('-s, --seed <seed>', 'Random seed for reproducible data')
.option('--dry-run', 'Show what would be generated without creating data')
.option('--format <format>', 'Output format (json|sql|csv)', 'json')
.option('--output <path>', 'Output directory path', './fixtures/exports')
.action(async (options) => {
const spinner = ora('🎯 Initializing fixture generation...').start()
try {
// Load configuration
const config = loadConfig(options.env)
if (options.seed) {
config.seed = options.seed
}
setGlobalConfig(config)
spinner.text = '📊 Generating comprehensive dataset...'
// Generate complete dataset with relationships
const dataset = DataRelationManager.generateCompleteDataset()
spinner.succeed(`✅ Generated complete dataset:`)
console.log(chalk.green(` 👥 Users: ${dataset.users.length}`))
console.log(chalk.green(` 🎵 Tracks: ${dataset.tracks.length}`))
console.log(chalk.green(` 📋 Playlists: ${dataset.playlists.length}`))
console.log(chalk.green(` 💬 Conversations: ${dataset.conversations.length}`))
console.log(chalk.green(` 📝 Messages: ${dataset.messages.length}`))
// Validate data relationships
const validation = DataRelationManager.validateRelations()
if (validation.isValid) {
console.log(chalk.green('✅ Data validation passed'))
} else {
console.log(chalk.yellow('⚠️ Data validation warnings:'))
validation.errors.forEach(error => console.log(chalk.red(`${error}`)))
validation.warnings.forEach(warning => console.log(chalk.yellow(` ⚠️ ${warning}`)))
}
// Export data if not dry run
if (!options.dryRun) {
await exportData(dataset, options.format, options.output)
} else {
console.log(chalk.blue('🔍 Dry run - no data exported'))
}
// Show statistics
const stats = DataRelationManager.getStatistics()
console.log(chalk.blue('\n📊 Generation Statistics:'))
console.log(` Average tracks per artist: ${stats.relationships.avgTracksPerArtist}`)
console.log(` Average playlists per user: ${stats.relationships.avgPlaylistsPerUser}`)
console.log(` Average messages per conversation: ${stats.relationships.avgMessagesPerConversation}`)
} catch (error) {
spinner.fail(`❌ Generation failed: ${error}`)
process.exit(1)
}
})
/**
* Seed Command - Populate services with fixture data
*/
program
.command('seed')
.description('Seed services with fixture data')
.option('-e, --env <environment>', 'Target environment', 'development')
.option('-s, --service <services...>', 'Services to seed (web, chat, stream, all)', ['all'])
.option('--scenario <scenario>', 'Use specific scenario data')
.option('--clean', 'Clean existing data before seeding')
.option('--validate', 'Validate data after seeding')
.action(async (options) => {
const spinner = ora('🌱 Initializing seeding process...').start()
try {
// Load configuration
const config = loadConfig(options.env)
setGlobalConfig(config)
// Determine services to seed
const servicesToSeed = options.service.includes('all') ?
['web', 'chat', 'stream'] : options.service
console.log(chalk.blue(`🎯 Seeding services: ${servicesToSeed.join(', ')}`))
// Generate or load scenario data
let dataset
if (options.scenario) {
dataset = await loadScenarioData(options.scenario, spinner)
} else {
spinner.text = '📊 Generating base dataset...'
dataset = DataRelationManager.generateCompleteDataset()
}
// Seed each service
for (const service of servicesToSeed) {
await seedService(service, dataset, options, spinner)
}
// Validate if requested
if (options.validate) {
spinner.text = '🔍 Validating seeded data...'
const validation = DataRelationManager.validateRelations()
if (!validation.isValid) {
spinner.fail('❌ Validation failed')
validation.errors.forEach(error => console.error(`${error}`))
process.exit(1)
}
console.log('✅ Validation passed')
}
spinner.succeed('✅ Seeding completed successfully!')
} catch (error) {
spinner.fail(`❌ Seeding failed: ${error}`)
process.exit(1)
}
})
/**
* Clean Command - Remove fixture data
*/
program
.command('clean')
.description('Clean fixture data from services')
.option('-e, --env <environment>', 'Target environment', 'development')
.option('-s, --service <services...>', 'Services to clean (web, chat, stream, all)', ['all'])
.option('--force', 'Skip confirmation prompt')
.action(async (options) => {
const servicesToClean = options.service.includes('all') ?
['web', 'chat', 'stream'] : options.service
// Confirmation prompt
if (!options.force) {
const { confirmed } = await inquirer.prompt([{
type: 'confirm',
name: 'confirmed',
message: `Are you sure you want to clean fixture data from: ${servicesToClean.join(', ')}?`,
default: false
}])
if (!confirmed) {
console.log(chalk.yellow('🚫 Operation cancelled'))
return
}
}
const spinner = ora('🧹 Cleaning fixture data...').start()
try {
// Load configuration
const config = loadConfig(options.env)
setGlobalConfig(config)
// Clean each service
for (const service of servicesToClean) {
await cleanService(service, spinner)
}
// Clear in-memory data
DataRelationManager.clearAll()
spinner.succeed('✅ Cleanup completed successfully!')
} catch (error) {
spinner.fail(`❌ Cleanup failed: ${error}`)
process.exit(1)
}
})
/**
* Validate Command - Validate fixture data integrity
*/
program
.command('validate')
.alias('check')
.description('Validate fixture data integrity and relationships')
.option('-e, --env <environment>', 'Target environment', 'development')
.option('-s, --service <services...>', 'Services to validate (web, chat, stream, all)', ['all'])
.option('--check-relations', 'Check data relationships')
.option('--check-performance', 'Check performance metrics')
.option('--report <path>', 'Generate validation report')
.action(async (options) => {
const spinner = ora('🔍 Starting validation...').start()
try {
// Load configuration
const config = loadConfig(options.env)
setGlobalConfig(config)
const servicesToValidate = options.service.includes('all') ?
['web', 'chat', 'stream'] : options.service
const validationResults: ValidationResult[] = []
// Validate data relationships
if (options.checkRelations) {
spinner.text = '🔗 Validating data relationships...'
const relationValidation = DataRelationManager.validateRelations()
validationResults.push({
type: 'relationships',
service: 'data-manager',
passed: relationValidation.isValid,
errors: relationValidation.errors,
warnings: relationValidation.warnings
})
}
// Validate each service
for (const service of servicesToValidate) {
const result = await validateService(service, spinner)
validationResults.push(result)
}
// Performance checks
if (options.checkPerformance) {
const perfResults = await performanceChecks(servicesToValidate, spinner)
validationResults.push(...perfResults)
}
// Display results
displayValidationResults(validationResults)
// Generate report if requested
if (options.report) {
await generateValidationReport(validationResults, options.report)
}
const hasErrors = validationResults.some(r => !r.passed)
if (hasErrors) {
spinner.fail('❌ Validation completed with errors')
process.exit(1)
} else {
spinner.succeed('✅ Validation passed successfully!')
}
} catch (error) {
spinner.fail(`❌ Validation failed: ${error}`)
process.exit(1)
}
})
/**
* Scenario Command - Work with test scenarios
*/
program
.command('scenario')
.description('Manage test scenarios')
.option('-l, --list', 'List available scenarios')
.option('-r, --run <scenario>', 'Run a specific scenario')
.option('-c, --create <name>', 'Create a new scenario')
.option('--interactive', 'Interactive scenario builder')
.action(async (options) => {
if (options.list) {
listAvailableScenarios()
} else if (options.run) {
await runScenario(options.run)
} else if (options.create) {
await createScenario(options.create, options.interactive)
} else {
console.log(chalk.yellow('Please specify an action. Use --help for more information.'))
}
})
/**
* Status Command - Show current fixture status
*/
program
.command('status')
.alias('info')
.description('Show current fixture status and statistics')
.option('-e, --env <environment>', 'Target environment', 'development')
.option('-v, --verbose', 'Show detailed information')
.action(async (options) => {
const spinner = ora('📊 Gathering status information...').start()
try {
// Load configuration
const config = loadConfig(options.env)
setGlobalConfig(config)
spinner.stop()
// Display configuration
console.log(chalk.blue('🔧 Configuration:'))
console.log(` Environment: ${chalk.green(config.environment)}`)
console.log(` Seed: ${chalk.green(config.seed)}`)
console.log(` Locale: ${chalk.green(config.locale)}`)
// Display generation settings
console.log(chalk.blue('\n📊 Generation Settings:'))
console.log(` Users: ${chalk.green(config.generation.users.count)}`)
console.log(` Tracks: ${chalk.green(config.generation.audio.trackCount)}`)
console.log(` Conversations: ${chalk.green(config.generation.conversations.directCount + config.generation.conversations.groupCount)}`)
// Check service health
console.log(chalk.blue('\n🏥 Service Health:'))
await checkServiceHealth(options.verbose)
// Show in-memory statistics
const stats = DataRelationManager.getStatistics()
if (stats.users > 0) {
console.log(chalk.blue('\n💾 In-Memory Data:'))
console.log(` Users: ${chalk.green(stats.users)}`)
console.log(` Tracks: ${chalk.green(stats.tracks)}`)
console.log(` Playlists: ${chalk.green(stats.playlists)}`)
console.log(` Conversations: ${chalk.green(stats.conversations)}`)
console.log(` Messages: ${chalk.green(stats.messages)}`)
}
} catch (error) {
spinner.fail(`❌ Status check failed: ${error}`)
process.exit(1)
}
})
/**
* Helper Functions
*/
async function exportData(dataset: any, format: string, outputPath: string): Promise<void> {
const fs = await import('fs/promises')
const path = await import('path')
// Ensure output directory exists
await fs.mkdir(outputPath, { recursive: true })
switch (format) {
case 'json':
await fs.writeFile(
path.join(outputPath, 'fixtures-export.json'),
JSON.stringify(dataset, null, 2)
)
break
case 'sql':
// Generate SQL inserts (simplified example)
const sqlContent = generateSQLInserts(dataset)
await fs.writeFile(
path.join(outputPath, 'fixtures-export.sql'),
sqlContent
)
break
case 'csv':
// Generate CSV files for each entity type
await generateCSVFiles(dataset, outputPath)
break
default:
throw new Error(`Unsupported export format: ${format}`)
}
console.log(chalk.green(`📁 Data exported to: ${outputPath}`))
}
async function seedService(service: string, dataset: any, options: any, spinner: any): Promise<void> {
spinner.text = `🌱 Seeding ${service} service...`
switch (service) {
case 'web':
await WebFixtures.initialize()
break
case 'chat':
await ChatServerFixtures.initialize()
await ChatServerFixtures.seedDatabase()
await ChatServerFixtures.seedRedis()
break
case 'stream':
await StreamServerFixtures.initialize()
await StreamServerFixtures.seedStreamingData()
break
default:
throw new Error(`Unknown service: ${service}`)
}
console.log(chalk.green(`${service} seeded successfully`))
}
async function cleanService(service: string, spinner: any): Promise<void> {
spinner.text = `🧹 Cleaning ${service} service...`
switch (service) {
case 'web':
WebFixtures.reset()
break
case 'chat':
await ChatServerFixtures.cleanup()
break
case 'stream':
await StreamServerFixtures.cleanup()
break
default:
throw new Error(`Unknown service: ${service}`)
}
console.log(chalk.green(`${service} cleaned successfully`))
}
async function validateService(service: string, spinner: any): Promise<ValidationResult> {
spinner.text = `🔍 Validating ${service} service...`
switch (service) {
case 'web':
// Validate web fixtures
return {
type: 'service',
service: 'web',
passed: true,
errors: [],
warnings: []
}
case 'chat':
const chatHealth = await ChatServerFixtures.healthCheck()
return {
type: 'service',
service: 'chat',
passed: chatHealth.database && chatHealth.redis && chatHealth.dataConsistency,
errors: [
...(chatHealth.database ? [] : ['Database connection failed']),
...(chatHealth.redis ? [] : ['Redis connection failed']),
...(chatHealth.dataConsistency ? [] : ['Data consistency check failed'])
],
warnings: []
}
case 'stream':
const streamHealth = await StreamServerFixtures.healthCheck()
return {
type: 'service',
service: 'stream',
passed: streamHealth.redis,
errors: streamHealth.redis ? [] : ['Redis connection failed'],
warnings: streamHealth.sessionsActive === 0 ? ['No active streaming sessions'] : []
}
default:
throw new Error(`Unknown service: ${service}`)
}
}
async function loadScenarioData(scenario: string, spinner: any): Promise<any> {
spinner.text = `📋 Loading scenario: ${scenario}...`
switch (scenario) {
case 'onboarding':
const onboardingContext = await NewUserOnboardingScenario.setup()
return DataRelationManager.exportForDatabase()
case 'high-load':
const loadContext = await HighLoadScenario.setup()
return DataRelationManager.exportForDatabase()
case 'integration':
const integrationContext = await CrossServiceCommunicationScenario.setup()
return DataRelationManager.exportForDatabase()
default:
throw new Error(`Unknown scenario: ${scenario}`)
}
}
function listAvailableScenarios(): void {
console.log(chalk.blue('📋 Available Scenarios:'))
console.log(' 🎯 onboarding - New user onboarding journey')
console.log(' 🚀 high-load - High load performance testing')
console.log(' 🔗 integration - Cross-service communication testing')
}
async function runScenario(scenarioName: string): Promise<void> {
const spinner = ora(`🎬 Running scenario: ${scenarioName}...`).start()
try {
switch (scenarioName) {
case 'onboarding':
const context = await NewUserOnboardingScenario.setup()
const simulation = NewUserOnboardingScenario.simulateOnboardingCompletion(context)
const validation = NewUserOnboardingScenario.validateScenario(context)
spinner.succeed('✅ Onboarding scenario completed')
console.log(chalk.green(` User: ${context.user.username}`))
console.log(chalk.green(` Validation: ${validation.isValid ? 'PASSED' : 'FAILED'}`))
break
case 'high-load':
const loadContext = await HighLoadScenario.setup()
const loadTest = HighLoadScenario.simulateLoadTest(loadContext)
spinner.succeed('✅ High load scenario completed')
console.log(chalk.green(` Peak users: ${loadContext.loadProfile.peakUsers}`))
console.log(chalk.green(` Expected throughput: ${loadContext.loadProfile.expectedThroughput} req/s`))
break
case 'integration':
const integrationContext = await CrossServiceCommunicationScenario.setup()
const integrationTest = await CrossServiceCommunicationScenario.simulateIntegrationTest(integrationContext)
spinner.succeed('✅ Integration scenario completed')
console.log(chalk.green(` Services tested: ${integrationContext.testFlow.services.length}`))
console.log(chalk.green(` Interactions: ${integrationContext.expectedInteractions.length}`))
break
default:
throw new Error(`Unknown scenario: ${scenarioName}`)
}
} catch (error) {
spinner.fail(`❌ Scenario failed: ${error}`)
process.exit(1)
}
}
async function createScenario(name: string, interactive: boolean): Promise<void> {
console.log(chalk.blue(`📝 Creating new scenario: ${name}`))
if (interactive) {
const answers = await inquirer.prompt([
{
type: 'list',
name: 'type',
message: 'Scenario type:',
choices: ['user-journey', 'performance', 'integration', 'edge-cases']
},
{
type: 'input',
name: 'description',
message: 'Scenario description:'
},
{
type: 'checkbox',
name: 'services',
message: 'Services involved:',
choices: ['web', 'chat-server', 'stream-server', 'backend-api', 'docs']
}
])
console.log(chalk.green('✅ Scenario template created'))
console.log(chalk.blue('📁 Location: ') + `fixtures/scenarios/${answers.type}/${name}.ts`)
} else {
console.log(chalk.yellow('Use --interactive for guided scenario creation'))
}
}
async function performanceChecks(services: string[], spinner: any): Promise<ValidationResult[]> {
spinner.text = '⚡ Running performance checks...'
const results: ValidationResult[] = []
for (const service of services) {
// Simulate performance check
const responseTime = Math.random() * 500 + 100 // 100-600ms
const passed = responseTime < 400 // Pass if under 400ms
results.push({
type: 'performance',
service,
passed,
errors: passed ? [] : [`Response time too high: ${responseTime.toFixed(0)}ms`],
warnings: responseTime > 300 ? [`Response time warning: ${responseTime.toFixed(0)}ms`] : []
})
}
return results
}
function displayValidationResults(results: ValidationResult[]): void {
console.log(chalk.blue('\n📋 Validation Results:'))
results.forEach(result => {
const status = result.passed ? chalk.green('✅ PASS') : chalk.red('❌ FAIL')
console.log(` ${status} ${result.service} (${result.type})`)
if (result.errors.length > 0) {
result.errors.forEach(error => {
console.log(chalk.red(`${error}`))
})
}
if (result.warnings.length > 0) {
result.warnings.forEach(warning => {
console.log(chalk.yellow(` ⚠️ ${warning}`))
})
}
})
const totalTests = results.length
const passedTests = results.filter(r => r.passed).length
const passRate = ((passedTests / totalTests) * 100).toFixed(1)
console.log(chalk.blue(`\n📊 Summary: ${passedTests}/${totalTests} tests passed (${passRate}%)`))
}
async function generateValidationReport(results: ValidationResult[], reportPath: string): Promise<void> {
const fs = await import('fs/promises')
const path = await import('path')
const report = {
timestamp: new Date().toISOString(),
summary: {
total: results.length,
passed: results.filter(r => r.passed).length,
failed: results.filter(r => !r.passed).length
},
results
}
await fs.writeFile(reportPath, JSON.stringify(report, null, 2))
console.log(chalk.green(`📄 Validation report saved to: ${reportPath}`))
}
async function checkServiceHealth(verbose: boolean): Promise<void> {
try {
// Check chat server
const chatHealth = await ChatServerFixtures.healthCheck()
const chatStatus = chatHealth.database && chatHealth.redis ? '🟢' : '🔴'
console.log(` ${chatStatus} Chat Server`)
if (verbose) {
console.log(` Database: ${chatHealth.database ? '✅' : '❌'}`)
console.log(` Redis: ${chatHealth.redis ? '✅' : '❌'}`)
console.log(` Data Consistency: ${chatHealth.dataConsistency ? '✅' : '❌'}`)
}
// Check stream server
const streamHealth = await StreamServerFixtures.healthCheck()
const streamStatus = streamHealth.redis ? '🟢' : '🔴'
console.log(` ${streamStatus} Stream Server`)
if (verbose) {
console.log(` Redis: ${streamHealth.redis ? '✅' : '❌'}`)
console.log(` Active Sessions: ${streamHealth.sessionsActive}`)
console.log(` Queue Size: ${streamHealth.queueSize}`)
}
// Web service is always available (no external dependencies)
console.log(' 🟢 Web Service')
} catch (error) {
console.log(' 🔴 Service health check failed')
if (verbose) {
console.log(` Error: ${error}`)
}
}
}
function generateSQLInserts(dataset: any): string {
// Simplified SQL generation
let sql = '-- Veza Platform Fixtures SQL Export\n\n'
// Users
sql += '-- Users\n'
dataset.users.forEach((user: any) => {
sql += `INSERT INTO users (id, username, email, first_name, last_name, role, created_at) VALUES ('${user.id}', '${user.username}', '${user.email}', '${user.firstName}', '${user.lastName}', '${user.role}', '${user.createdAt}');\n`
})
return sql
}
async function generateCSVFiles(dataset: any, outputPath: string): Promise<void> {
const fs = await import('fs/promises')
const path = await import('path')
// Users CSV
const usersCSV = [
'id,username,email,firstName,lastName,role,status,createdAt',
...dataset.users.map((user: any) =>
`${user.id},${user.username},${user.email},${user.firstName},${user.lastName},${user.role},${user.status},${user.createdAt}`
)
].join('\n')
await fs.writeFile(path.join(outputPath, 'users.csv'), usersCSV)
console.log(chalk.green(' 📄 users.csv exported'))
}
interface ValidationResult {
type: string
service: string
passed: boolean
errors: string[]
warnings: string[]
}
// Parse command line arguments
program.parse()
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp()
}