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
JavaScript
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);
});
}