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