UNPKG

claudes-office

Version:

CLI tool to initialize Claude's office in your project

311 lines (270 loc) 10.3 kB
#!/usr/bin/env node const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const { spawn } = require('child_process'); const chalk = require('chalk'); // Define the edge case test suite - these tests are designed to test specific edge cases // or potential problem areas in the codebase const tests = [ { name: 'Very long custom name handling', command: ['init', '--force'], simulatedInput: [ { input: 'y\n', delay: 1000 }, // Yes to custom name { input: 'VeryLongNameWithManyCharactersThatMightCauseProblems\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 very long name was handled properly 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('VeryLongNameWithManyCharactersThatMightCauseProblems'); } }, { name: 'Special characters in custom name', command: ['init', '--force'], simulatedInput: [ { input: 'y\n', delay: 1000 }, // Yes to custom name { input: 'Test-Bot@123\n', delay: 1000 }, // Custom name with special chars { 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 name with special chars was handled properly 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('Test-Bot@123'); } }, { name: 'No selected frameworks', command: ['init', '--force'], simulatedInput: [ { input: 'n\n', delay: 1000 }, // No custom name { input: ' \n', delay: 1000 }, // Select frontend project type { input: '\n', delay: 1000 }, // Skip framework selection { input: '\n', delay: 1000 }, // Skip additional selection { input: 'n\n', delay: 1000 } // No to DevOps roles ], verify: (testDir) => { // Should still install basic structure with generic roles if (!validateBasicInstallation(testDir)) return false; // Check if generic roles exist return fs.pathExistsSync(path.join(testDir, 'claudes-office', 'roles', 'generic')); } }, { name: 'Select multiple project types', command: ['init', '--force'], simulatedInput: [ { input: 'n\n', delay: 1000 }, // No custom name { input: ' \n', delay: 500 }, // Select frontend { input: ' \n', delay: 500 }, // Select backend { input: '\n', delay: 1000 }, // Continue with selection { input: '\n', delay: 1000 }, // Skip frontend framework selection { input: '\n', delay: 1000 }, // Skip backend framework selection { input: '\n', delay: 1000 }, // Skip additional selection { input: 'n\n', delay: 1000 } // No to DevOps ], verify: (testDir) => { // Should create directories for both frontend and backend return validateBasicInstallation(testDir) && fs.pathExistsSync(path.join(testDir, 'claudes-office', 'roles', 'project-specific', 'frontend')) && fs.pathExistsSync(path.join(testDir, 'claudes-office', 'roles', 'project-specific', 'backend')); } }, { name: 'Update command stub', command: ['update'], verify: (testDir, output) => { // Should show the update not implemented message return output.includes('will be implemented in a future version'); } }, { name: 'Re-adding roles to existing installation', setup: async (testDir) => { // First set up a basic installation await runCommand(['init', '--force', '--no-interactive'], testDir); // Then add roles once await runCommand(['add-roles', '--all', '--no-interactive'], testDir); }, command: ['add-roles', '--all', '--no-interactive'], verify: (testDir) => { // Should succeed even though roles are already there return validateAllRoles(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; } } 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 Edge Case 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=== Edge Case 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 edge case tests passed! 🎉\n`)); } } // Run the test suite runTestSuite();