UNPKG

claude-code-collective

Version:

Sub-agent collective framework for Claude Code with TDD validation, TaskMaster Task ID integration, hub-spoke coordination, and deterministic handoffs

547 lines (430 loc) โ€ข 16 kB
const fs = require('fs-extra'); const path = require('path'); const { exec } = require('child_process'); const { promisify } = require('util'); const os = require('os'); const chalk = require('chalk'); const execAsync = promisify(exec); class InstallationTester { constructor() { this.testResults = []; this.tempDirs = []; } async runCompleteTestSuite() { console.log(chalk.bold('๐Ÿงช Claude Code Collective - Installation Test Suite\n')); try { // Test 1: Basic installation test await this.testBasicInstallation(); // Test 2: Minimal installation test await this.testMinimalInstallation(); // Test 3: Interactive installation test await this.testInteractiveInstallation(); // Test 4: Force installation test await this.testForceInstallation(); // Test 5: Validation test await this.testValidationCommand(); // Test 6: Status command test await this.testStatusCommand(); // Test 7: Clean installation test await this.testCleanCommand(); // Test 8: Cross-platform compatibility await this.testCrossPlatformCompatibility(); // Report results this.reportResults(); } catch (error) { console.error(chalk.red('โŒ Test suite failed:'), error.message); throw error; } finally { await this.cleanup(); } } async testBasicInstallation() { const testName = 'Basic Installation Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); const testDir = await this.createTempDir('basic-test'); try { // Run installation const { stdout, stderr } = await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --force', { cwd: testDir, timeout: 30000 } ); // Validate installation const validationResults = await this.validateInstallation(testDir); this.recordTest(testName, true, { stdout: stdout.substring(0, 200), validation: validationResults }); console.log(chalk.green(`โœ… ${testName} passed`)); } catch (error) { this.recordTest(testName, false, { error: error.message, stdout: error.stdout, stderr: error.stderr }); console.log(chalk.red(`โŒ ${testName} failed: ${error.message}`)); } } async testMinimalInstallation() { const testName = 'Minimal Installation Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); const testDir = await this.createTempDir('minimal-test'); try { // Run minimal installation const { stdout } = await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --minimal --force', { cwd: testDir, timeout: 30000 } ); // Validate minimal installation const validationResults = await this.validateMinimalInstallation(testDir); this.recordTest(testName, true, { stdout: stdout.substring(0, 200), validation: validationResults }); console.log(chalk.green(`โœ… ${testName} passed`)); } catch (error) { this.recordTest(testName, false, { error: error.message }); console.log(chalk.red(`โŒ ${testName} failed: ${error.message}`)); } } async testInteractiveInstallation() { const testName = 'Interactive Installation Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); // Skip interactive test in automated environment this.recordTest(testName, true, { note: 'Skipped in automated environment - interactive prompts require manual testing' }); console.log(chalk.yellow(`โญ๏ธ ${testName} skipped (requires manual testing)`)); } async testForceInstallation() { const testName = 'Force Installation Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); const testDir = await this.createTempDir('force-test'); try { // Install first time await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --minimal --force', { cwd: testDir, timeout: 30000 } ); // Install second time with force const { stdout } = await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --force', { cwd: testDir, timeout: 30000 } ); // Validate overwrite worked const validationResults = await this.validateInstallation(testDir); this.recordTest(testName, validationResults.passed, { stdout: stdout.substring(0, 200), validation: validationResults }); if (validationResults.passed) { console.log(chalk.green(`โœ… ${testName} passed`)); } else { console.log(chalk.red(`โŒ ${testName} failed`)); } } catch (error) { this.recordTest(testName, false, { error: error.message }); console.log(chalk.red(`โŒ ${testName} failed: ${error.message}`)); } } async testValidationCommand() { const testName = 'Validation Command Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); const testDir = await this.createTempDir('validation-test'); try { // Install first await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --force', { cwd: testDir, timeout: 30000 } ); // Run validation const { stdout } = await execAsync( 'node ../claude-code-collective/bin/install-collective.js validate', { cwd: testDir, timeout: 15000 } ); const validationPassed = stdout.includes('All validation checks passed'); this.recordTest(testName, validationPassed, { stdout: stdout.substring(0, 300) }); if (validationPassed) { console.log(chalk.green(`โœ… ${testName} passed`)); } else { console.log(chalk.red(`โŒ ${testName} failed`)); } } catch (error) { this.recordTest(testName, false, { error: error.message }); console.log(chalk.red(`โŒ ${testName} failed: ${error.message}`)); } } async testStatusCommand() { const testName = 'Status Command Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); const testDir = await this.createTempDir('status-test'); try { // Install first await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --force', { cwd: testDir, timeout: 30000 } ); // Run status const { stdout } = await execAsync( 'node ../claude-code-collective/bin/install-collective.js status', { cwd: testDir, timeout: 15000 } ); const statusValid = stdout.includes('Collective Installation Status') && stdout.includes('Installed:'); this.recordTest(testName, statusValid, { stdout: stdout.substring(0, 300) }); if (statusValid) { console.log(chalk.green(`โœ… ${testName} passed`)); } else { console.log(chalk.red(`โŒ ${testName} failed`)); } } catch (error) { this.recordTest(testName, false, { error: error.message }); console.log(chalk.red(`โŒ ${testName} failed: ${error.message}`)); } } async testCleanCommand() { const testName = 'Clean Command Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); const testDir = await this.createTempDir('clean-test'); try { // Install first await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --force', { cwd: testDir, timeout: 30000 } ); // Verify installation exists const claudeExists = await fs.pathExists(path.join(testDir, '.claude')); if (!claudeExists) { throw new Error('Installation not found before clean'); } // Run clean await execAsync( 'node ../claude-code-collective/bin/install-collective.js clean --force', { cwd: testDir, timeout: 15000 } ); // Verify installation removed const claudeExistsAfter = await fs.pathExists(path.join(testDir, '.claude')); const claudeCollectiveExists = await fs.pathExists(path.join(testDir, '.claude-collective')); const cleanSuccessful = !claudeExistsAfter && !claudeCollectiveExists; this.recordTest(testName, cleanSuccessful, { beforeClean: claudeExists, afterClean: !claudeExistsAfter }); if (cleanSuccessful) { console.log(chalk.green(`โœ… ${testName} passed`)); } else { console.log(chalk.red(`โŒ ${testName} failed`)); } } catch (error) { this.recordTest(testName, false, { error: error.message }); console.log(chalk.red(`โŒ ${testName} failed: ${error.message}`)); } } async testCrossPlatformCompatibility() { const testName = 'Cross-Platform Compatibility Test'; console.log(chalk.cyan(`\n๐Ÿ”ฌ Running: ${testName}`)); const platform = os.platform(); const nodeVersion = process.version; const testDir = await this.createTempDir('platform-test'); try { // Test path handling on current platform const { stdout } = await execAsync( 'node ../claude-code-collective/bin/install-collective.js init --minimal --force', { cwd: testDir, timeout: 30000 } ); // Validate platform-specific paths const validationResults = await this.validatePlatformPaths(testDir, platform); this.recordTest(testName, validationResults.passed, { platform, nodeVersion, paths: validationResults.paths, stdout: stdout.substring(0, 200) }); if (validationResults.passed) { console.log(chalk.green(`โœ… ${testName} passed on ${platform} with Node ${nodeVersion}`)); } else { console.log(chalk.red(`โŒ ${testName} failed on ${platform}`)); } } catch (error) { this.recordTest(testName, false, { platform, nodeVersion, error: error.message }); console.log(chalk.red(`โŒ ${testName} failed: ${error.message}`)); } } async createTempDir(prefix) { const tempDir = path.join(os.tmpdir(), `collective-test-${prefix}-${Date.now()}`); await fs.ensureDir(tempDir); this.tempDirs.push(tempDir); return tempDir; } async validateInstallation(testDir) { const requiredFiles = [ 'CLAUDE.md', '.claude/settings.json', '.claude/hooks', '.claude/agents', '.claude-collective/tests' ]; const results = { passed: true, files: {} }; for (const file of requiredFiles) { const filePath = path.join(testDir, file); const exists = await fs.pathExists(filePath); results.files[file] = exists; if (!exists) { results.passed = false; } } return results; } async validateMinimalInstallation(testDir) { const requiredFiles = [ 'CLAUDE.md', '.claude/settings.json', '.claude/hooks', '.claude/agents/routing-agent.json' ]; const results = { passed: true, files: {} }; for (const file of requiredFiles) { const filePath = path.join(testDir, file); const exists = await fs.pathExists(filePath); results.files[file] = exists; if (!exists) { results.passed = false; } } // Should NOT have advanced agents in minimal mode const advancedAgent = path.join(testDir, '.claude/agents/component-implementation-agent.json'); const hasAdvanced = await fs.pathExists(advancedAgent); if (hasAdvanced) { results.passed = false; results.files['should_not_have_advanced_agents'] = false; } return results; } async validatePlatformPaths(testDir, platform) { const results = { passed: true, paths: {} }; // Test platform-specific path handling const hooks = await fs.readdir(path.join(testDir, '.claude/hooks')); for (const hook of hooks) { if (hook.endsWith('.sh')) { const hookPath = path.join(testDir, '.claude/hooks', hook); try { const stats = await fs.stat(hookPath); // On Unix-like systems, check if executable if (platform !== 'win32') { const isExecutable = !!(stats.mode & parseInt('111', 8)); results.paths[`${hook}_executable`] = isExecutable; if (!isExecutable) { results.passed = false; } } else { // On Windows, just check if file exists results.paths[`${hook}_exists`] = true; } } catch (error) { results.passed = false; results.paths[`${hook}_error`] = error.message; } } } return results; } recordTest(name, passed, details = {}) { this.testResults.push({ name, passed, timestamp: new Date().toISOString(), details }); } reportResults() { console.log(chalk.bold('\n๐Ÿ“Š Test Results Summary:\n')); const passedTests = this.testResults.filter(t => t.passed); const failedTests = this.testResults.filter(t => !t.passed); console.log(`${chalk.green('Passed:')} ${passedTests.length}`); console.log(`${chalk.red('Failed:')} ${failedTests.length}`); console.log(`${chalk.blue('Total:')} ${this.testResults.length}`); if (failedTests.length > 0) { console.log(chalk.bold('\nโŒ Failed Tests:')); failedTests.forEach(test => { console.log(` - ${test.name}`); if (test.details.error) { console.log(chalk.gray(` Error: ${test.details.error}`)); } }); } const successRate = (passedTests.length / this.testResults.length) * 100; if (successRate >= 80) { console.log(chalk.green(`\nโœ… Test suite passed with ${successRate.toFixed(1)}% success rate`)); } else { console.log(chalk.red(`\nโŒ Test suite failed with ${successRate.toFixed(1)}% success rate`)); } // Save detailed results this.saveTestReport(); } async saveTestReport() { const reportPath = path.join(__dirname, '..', 'test-results.json'); const report = { timestamp: new Date().toISOString(), platform: os.platform(), nodeVersion: process.version, results: this.testResults, summary: { total: this.testResults.length, passed: this.testResults.filter(t => t.passed).length, failed: this.testResults.filter(t => !t.passed).length } }; await fs.writeJson(reportPath, report, { spaces: 2 }); console.log(chalk.blue(`\n๐Ÿ“„ Detailed report saved to: ${reportPath}`)); } async cleanup() { console.log(chalk.gray('\n๐Ÿงน Cleaning up test directories...')); for (const tempDir of this.tempDirs) { try { await fs.remove(tempDir); } catch (error) { console.warn(chalk.yellow(`Warning: Could not remove ${tempDir}: ${error.message}`)); } } } } module.exports = { InstallationTester }; // CLI usage when run directly if (require.main === module) { const tester = new InstallationTester(); tester.runCompleteTestSuite() .then(() => { console.log(chalk.green('\n๐ŸŽ‰ Test suite completed!')); process.exit(0); }) .catch(error => { console.error(chalk.red('\n๐Ÿ’ฅ Test suite failed:'), error.message); process.exit(1); }); }