claudes-office
Version:
CLI tool to initialize Claude's office in your project
346 lines (306 loc) • 11 kB
JavaScript
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();