UNPKG

claudes-office

Version:

CLI tool to initialize Claude's office in your project

346 lines (306 loc) 11 kB
#!/usr/bin/env node const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const { spawn, execSync } = require('child_process'); const chalk = require('chalk'); // Define the test suite const tests = [ { name: 'Non-interactive installation with all roles', command: ['init', '--all-roles', '--force', '--no-interactive'], verify: (testDir) => { return validateBasicInstallation(testDir) && validateAllRoles(testDir); } }, { name: 'Non-interactive roles-only installation', command: ['init', '--roles-only', '--force', '--no-interactive'], verify: (testDir) => { // Should have roles dir but not other files const officeDir = path.join(testDir, 'claudes-office'); const rolesExist = fs.pathExistsSync(path.join(officeDir, 'roles')); const rootFilesExist = fs.pathExistsSync(path.join(testDir, 'CLAUDE.md')); return rolesExist && !rootFilesExist; } }, { name: 'Add-roles command with all option', setup: async (testDir) => { // First set up a basic installation await runCommand(['init', '--force', '--no-interactive'], testDir); }, command: ['add-roles', '--all', '--no-interactive'], verify: (testDir) => { // Verify all roles directories are added return validateAllRoles(testDir); } }, { name: 'Help command displays output', command: ['help'], verify: (testDir, output) => { // Check if help output contains expected sections return output.includes('COMMANDS') && output.includes('OPTIONS') && output.includes('EXAMPLES'); } }, { name: 'Unknown command shows error', command: ['unknown-command'], expectedExitCode: 1, verify: (testDir, output) => { return output.includes('Invalid command'); } }, { name: 'Interactive installation with simulated input', command: ['init', '--force'], simulatedInput: [ { input: 'n\n', delay: 1000 }, // No custom name { input: ' \n', delay: 1000 }, // Select frontend project type { input: ' \n', delay: 1000 }, // Select React { input: '\n', delay: 1000 }, // Skip backend selection { input: '\n', delay: 1000 }, // Skip additional technologies { input: 'y\n', delay: 1000 } // Yes to DevOps roles ], verify: (testDir) => { // Check for React-specific roles return validateBasicInstallation(testDir) && fs.pathExistsSync(path.join(testDir, 'claudes-office', 'roles', 'project-specific', 'frontend', 'react')); } }, { name: 'Custom name replacement', command: ['init', '--force'], simulatedInput: [ { input: 'y\n', delay: 1000 }, // Yes to custom name { input: 'TestBot\n', delay: 1000 }, // Custom name { input: ' \n', delay: 1000 }, // Select project type { input: '\n', delay: 1000 }, // Skip framework selection { input: '\n', delay: 1000 }, // Skip additional selection { input: 'y\n', delay: 1000 } // Yes to DevOps ], verify: (testDir) => { // Check if Claude is replaced with TestBot in files if (!validateBasicInstallation(testDir)) return false; // Read CLAUDE.md to check for name replacement const claudeMdContent = fs.readFileSync(path.join(testDir, 'CLAUDE.md'), 'utf8'); return claudeMdContent.includes('TestBot') && !claudeMdContent.includes('Claude'); } }, { name: 'Installation with non-NodeJS project warning', command: ['init', '--force'], setup: async (testDir) => { // Remove package.json to simulate non-NodeJS project await fs.remove(path.join(testDir, 'package.json')); }, simulatedInput: [ { input: 'y\n', delay: 1000 }, // Yes to continue anyway { input: 'n\n', delay: 1000 }, // No custom name { input: ' \n', delay: 1000 }, // Select project type { input: '\n', delay: 1000 }, // Skip framework selection { input: '\n', delay: 1000 }, // Skip additional selection { input: 'y\n', delay: 1000 } // Yes to DevOps ], verify: (testDir, output) => { return output.includes('No package.json found') && validateBasicInstallation(testDir); } }, { name: 'Directory already exists warning', setup: async (testDir) => { // Create the office dir to trigger the warning await fs.ensureDir(path.join(testDir, 'claudes-office')); }, command: ['init'], simulatedInput: [ { input: 'y\n', delay: 1000 }, // Yes to continue anyway { input: 'n\n', delay: 1000 }, // No custom name { input: ' \n', delay: 1000 }, // Select project type { input: '\n', delay: 1000 }, // Skip framework selection { input: '\n', delay: 1000 }, // Skip additional selection { input: 'y\n', delay: 1000 } // Yes to DevOps ], verify: (testDir, output) => { return output.includes('Warning: Office directory already exists') && validateBasicInstallation(testDir); } } ]; // Helper functions function validateBasicInstallation(testDir) { // Check if base files were created const filesCreated = [ 'CLAUDE.md', 'Initialize_Project.md', 'claudes-office' ]; for (const file of filesCreated) { if (!fs.pathExistsSync(path.join(testDir, file))) { console.log(chalk.red(`❌ ${file} was not created`)); return false; } } // Check if standard directories were created const officeDir = path.join(testDir, 'claudes-office'); const standardDirs = [ 'roles', 'references', 'worksessions', 'plans', 'tasks', 'meetings', 'mail' ]; for (const dir of standardDirs) { if (!fs.pathExistsSync(path.join(officeDir, dir))) { console.log(chalk.red(`❌ ${dir} directory was not created`)); return false; } } return true; } function validateAllRoles(testDir) { const officeDir = path.join(testDir, 'claudes-office'); // Check for all project-specific role directories const domains = [ 'frontend', 'backend', 'mobile', 'data', 'devops' ]; for (const domain of domains) { if (!fs.pathExistsSync(path.join(officeDir, 'roles', 'project-specific', domain))) { console.log(chalk.red(`❌ ${domain} roles directory was not created`)); return false; } } // Check for specific framework directories (sampling a few) const frameworkPaths = [ ['frontend', 'react'], ['backend', 'express'], ['mobile', 'react-native'] ]; for (const [domain, framework] of frameworkPaths) { if (!fs.pathExistsSync(path.join(officeDir, 'roles', 'project-specific', domain, framework))) { console.log(chalk.red(`❌ ${domain}/${framework} directory was not created`)); return false; } } return true; } async function runCommand(args, cwd, simulatedInput = null) { // Path to CLI executable const cliPath = path.join(__dirname, 'bin', 'claudes-office'); // Output storage let outputData = ''; // Create stdio config based on whether we need to simulate input const stdio = simulatedInput ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe']; // Run the command const childProcess = spawn('node', [cliPath, ...args], { cwd, stdio: stdio }); // Collect output childProcess.stdout.on('data', (data) => { const text = data.toString(); outputData += text; process.stdout.write(text); }); childProcess.stderr.on('data', (data) => { const text = data.toString(); outputData += text; process.stderr.write(text); }); // Schedule input if needed if (simulatedInput) { let currentTimeout = 0; simulatedInput.forEach(response => { currentTimeout += response.delay; setTimeout(() => { console.log(chalk.yellow(`[TEST] Sending input: ${response.input.replace(/\n/g, '\\n')}`)); childProcess.stdin.write(response.input); }, currentTimeout); }); } // Wait for process to complete return new Promise((resolve, reject) => { childProcess.on('close', code => { resolve({ code, output: outputData }); }); // Handle error in spawning the process childProcess.on('error', (error) => { reject(error); }); }); } async function runTest(test, index, totalTests) { try { console.log(chalk.cyan(`\n[${index+1}/${totalTests}] Running test: ${test.name}`)); // Create a temporary test directory const testDir = path.join(os.tmpdir(), `claudes-office-test-${Date.now()}`); await fs.ensureDir(testDir); console.log(chalk.blue(`Created test directory: ${testDir}`)); // Create a dummy package.json in the test directory await fs.writeJson(path.join(testDir, 'package.json'), { name: 'test-project', version: '1.0.0' }); // Run setup if defined if (test.setup) { console.log(chalk.blue(`Running test setup...`)); await test.setup(testDir); } // Run the CLI command console.log(chalk.blue(`Running command: claudes-office ${test.command.join(' ')}`)); const { code, output } = await runCommand(test.command, testDir, test.simulatedInput); // Check exit code const expectedCode = test.expectedExitCode || 0; if (code !== expectedCode) { console.log(chalk.red(`❌ Test failed: Expected exit code ${expectedCode} but got ${code}`)); return false; } // Run verification const verificationPassed = test.verify(testDir, output); if (verificationPassed) { console.log(chalk.green(`✓ Test passed: ${test.name}`)); } else { console.log(chalk.red(`❌ Test failed: ${test.name}`)); } // Clean up console.log(chalk.blue(`Cleaning up test directory...`)); await fs.remove(testDir); return verificationPassed; } catch (error) { console.error(chalk.red(`❌ Test error: ${error.message}`)); console.error(error); return false; } } async function runTestSuite() { console.log(chalk.bold.cyan(`\n=== Claude's Office CLI Test Suite ===\n`)); console.log(chalk.cyan(`Running ${tests.length} tests...\n`)); let passedTests = 0; let failedTests = 0; for (let i = 0; i < tests.length; i++) { const passed = await runTest(tests[i], i, tests.length); if (passed) { passedTests++; } else { failedTests++; } } console.log(chalk.bold.cyan(`\n=== Test Suite Results ===\n`)); console.log(chalk.green(`Passed: ${passedTests}/${tests.length}`)); if (failedTests > 0) { console.log(chalk.red(`Failed: ${failedTests}/${tests.length}`)); process.exit(1); } else { console.log(chalk.bold.green(`\nAll tests passed! 🎉\n`)); } } // Run the test suite runTestSuite();