1476 lines
49 KiB
JavaScript
1476 lines
49 KiB
JavaScript
/**
|
|
* Scope CLI Test Suite
|
|
*
|
|
* Comprehensive tests for the scope CLI command including:
|
|
* - All subcommands (init, create, list, info, set, unset, remove, archive, activate, sync-up, sync-down)
|
|
* - Help system (main help and subcommand-specific help)
|
|
* - Error handling and edge cases
|
|
* - Integration with ScopeManager, ScopeSync, and other components
|
|
*
|
|
* Usage: node test/test-scope-cli.js
|
|
* Exit codes: 0 = all tests pass, 1 = test failures
|
|
*/
|
|
|
|
const fs = require('fs-extra');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const { execSync, spawnSync } = require('node:child_process');
|
|
|
|
// ANSI color codes
|
|
const colors = {
|
|
reset: '\u001B[0m',
|
|
green: '\u001B[32m',
|
|
red: '\u001B[31m',
|
|
yellow: '\u001B[33m',
|
|
blue: '\u001B[34m',
|
|
cyan: '\u001B[36m',
|
|
dim: '\u001B[2m',
|
|
bold: '\u001B[1m',
|
|
};
|
|
|
|
// Test utilities
|
|
let testCount = 0;
|
|
let passCount = 0;
|
|
let failCount = 0;
|
|
let skipCount = 0;
|
|
const failures = [];
|
|
|
|
function test(name, fn) {
|
|
testCount++;
|
|
try {
|
|
fn();
|
|
passCount++;
|
|
console.log(` ${colors.green}✓${colors.reset} ${name}`);
|
|
} catch (error) {
|
|
failCount++;
|
|
console.log(` ${colors.red}✗${colors.reset} ${name}`);
|
|
console.log(` ${colors.red}${error.message}${colors.reset}`);
|
|
failures.push({ name, error: error.message });
|
|
}
|
|
}
|
|
|
|
async function testAsync(name, fn) {
|
|
testCount++;
|
|
try {
|
|
await fn();
|
|
passCount++;
|
|
console.log(` ${colors.green}✓${colors.reset} ${name}`);
|
|
} catch (error) {
|
|
failCount++;
|
|
console.log(` ${colors.red}✗${colors.reset} ${name}`);
|
|
console.log(` ${colors.red}${error.message}${colors.reset}`);
|
|
failures.push({ name, error: error.message });
|
|
}
|
|
}
|
|
|
|
function skip(name, reason = '') {
|
|
skipCount++;
|
|
console.log(` ${colors.yellow}○${colors.reset} ${name} ${colors.dim}(skipped${reason ? ': ' + reason : ''})${colors.reset}`);
|
|
}
|
|
|
|
function assertEqual(actual, expected, message = '') {
|
|
if (actual !== expected) {
|
|
throw new Error(`${message}\n Expected: ${JSON.stringify(expected)}\n Actual: ${JSON.stringify(actual)}`);
|
|
}
|
|
}
|
|
|
|
function assertTrue(value, message = 'Expected true') {
|
|
if (!value) {
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
function assertFalse(value, message = 'Expected false') {
|
|
if (value) {
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
function assertContains(str, substring, message = '') {
|
|
if (!str.includes(substring)) {
|
|
throw new Error(`${message}\n Expected to contain: "${substring}"\n Actual: "${str.slice(0, 200)}..."`);
|
|
}
|
|
}
|
|
|
|
function assertNotContains(str, substring, message = '') {
|
|
if (str.includes(substring)) {
|
|
throw new Error(`${message}\n Expected NOT to contain: "${substring}"`);
|
|
}
|
|
}
|
|
|
|
function assertExists(filePath, message = '') {
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error(`${message || 'File does not exist'}: ${filePath}`);
|
|
}
|
|
}
|
|
|
|
function assertNotExists(filePath, message = '') {
|
|
if (fs.existsSync(filePath)) {
|
|
throw new Error(`${message || 'File should not exist'}: ${filePath}`);
|
|
}
|
|
}
|
|
|
|
// Create temporary test directory with BMAD structure
|
|
function createTestProject() {
|
|
const tmpDir = path.join(os.tmpdir(), `bmad-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
|
|
// Create minimal BMAD structure
|
|
fs.mkdirSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '_bmad-output'), { recursive: true });
|
|
|
|
return tmpDir;
|
|
}
|
|
|
|
function cleanupTestProject(tmpDir) {
|
|
try {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
|
|
// Get path to CLI
|
|
const CLI_PATH = path.join(__dirname, '..', 'tools', 'cli', 'bmad-cli.js');
|
|
|
|
// Execute CLI command and capture output (string-based, for simple cases)
|
|
function runCli(args, cwd, options = {}) {
|
|
const cmd = `node "${CLI_PATH}" ${args}`;
|
|
try {
|
|
const output = execSync(cmd, {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
timeout: options.timeout || 30_000,
|
|
env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
|
|
});
|
|
return { success: true, output, exitCode: 0 };
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
output: error.stdout || '',
|
|
stderr: error.stderr || '',
|
|
exitCode: error.status || 1,
|
|
error: error.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute CLI command using spawnSync with an array of arguments.
|
|
* This properly preserves argument boundaries, essential for arguments with spaces.
|
|
*
|
|
* @param {string[]} args - Array of arguments (NOT a joined string)
|
|
* @param {string} cwd - Working directory
|
|
* @param {Object} options - Additional options
|
|
* @returns {Object} Result with success, output, stderr, exitCode
|
|
*/
|
|
function runCliArray(args, cwd, options = {}) {
|
|
const result = spawnSync('node', [CLI_PATH, ...args], {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
timeout: options.timeout || 30_000,
|
|
env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
|
|
});
|
|
|
|
return {
|
|
success: result.status === 0,
|
|
output: result.stdout || '',
|
|
stderr: result.stderr || '',
|
|
exitCode: result.status || 0,
|
|
error: result.error ? result.error.message : null,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Help System Tests
|
|
// ============================================================================
|
|
|
|
function testHelpSystem() {
|
|
console.log(`\n${colors.blue}${colors.bold}Help System Tests${colors.reset}`);
|
|
|
|
const tmpDir = createTestProject();
|
|
|
|
try {
|
|
// Main help
|
|
test('scope help shows overview', () => {
|
|
const result = runCli('scope help', tmpDir);
|
|
assertContains(result.output, 'BMAD Scope Management');
|
|
assertContains(result.output, 'OVERVIEW');
|
|
assertContains(result.output, 'COMMANDS');
|
|
});
|
|
|
|
test('scope help shows all commands', () => {
|
|
const result = runCli('scope help', tmpDir);
|
|
assertContains(result.output, 'init');
|
|
assertContains(result.output, 'list');
|
|
assertContains(result.output, 'create');
|
|
assertContains(result.output, 'info');
|
|
assertContains(result.output, 'set');
|
|
assertContains(result.output, 'unset');
|
|
assertContains(result.output, 'remove');
|
|
assertContains(result.output, 'archive');
|
|
assertContains(result.output, 'activate');
|
|
assertContains(result.output, 'sync-up');
|
|
assertContains(result.output, 'sync-down');
|
|
});
|
|
|
|
test('scope help shows options', () => {
|
|
const result = runCli('scope help', tmpDir);
|
|
assertContains(result.output, 'OPTIONS');
|
|
assertContains(result.output, '--name');
|
|
assertContains(result.output, '--description');
|
|
assertContains(result.output, '--force');
|
|
assertContains(result.output, '--dry-run');
|
|
assertContains(result.output, '--resolution');
|
|
});
|
|
|
|
test('scope help shows quick start', () => {
|
|
const result = runCli('scope help', tmpDir);
|
|
assertContains(result.output, 'QUICK START');
|
|
assertContains(result.output, 'scope init');
|
|
assertContains(result.output, 'scope create');
|
|
assertContains(result.output, 'scope set');
|
|
});
|
|
|
|
test('scope help shows directory structure', () => {
|
|
const result = runCli('scope help', tmpDir);
|
|
assertContains(result.output, 'DIRECTORY STRUCTURE');
|
|
assertContains(result.output, '_bmad-output');
|
|
assertContains(result.output, '_shared');
|
|
assertContains(result.output, 'scopes.yaml');
|
|
});
|
|
|
|
test('scope help shows access model', () => {
|
|
const result = runCli('scope help', tmpDir);
|
|
assertContains(result.output, 'ACCESS MODEL');
|
|
assertContains(result.output, 'read-any');
|
|
assertContains(result.output, 'write-own');
|
|
});
|
|
|
|
test('scope help shows troubleshooting', () => {
|
|
const result = runCli('scope help', tmpDir);
|
|
assertContains(result.output, 'TROUBLESHOOTING');
|
|
});
|
|
|
|
// Subcommand-specific help
|
|
test('scope help init shows detailed help', () => {
|
|
const result = runCli('scope help init', tmpDir);
|
|
assertContains(result.output, 'bmad scope init');
|
|
assertContains(result.output, 'DESCRIPTION');
|
|
assertContains(result.output, 'USAGE');
|
|
assertContains(result.output, 'WHAT IT CREATES');
|
|
});
|
|
|
|
test('scope help create shows detailed help', () => {
|
|
const result = runCli('scope help create', tmpDir);
|
|
assertContains(result.output, 'bmad scope create');
|
|
assertContains(result.output, 'ARGUMENTS');
|
|
assertContains(result.output, 'OPTIONS');
|
|
assertContains(result.output, '--name');
|
|
assertContains(result.output, '--deps');
|
|
assertContains(result.output, 'SCOPE ID RULES');
|
|
});
|
|
|
|
test('scope help list shows detailed help', () => {
|
|
const result = runCli('scope help list', tmpDir);
|
|
assertContains(result.output, 'bmad scope list');
|
|
assertContains(result.output, '--status');
|
|
assertContains(result.output, 'OUTPUT COLUMNS');
|
|
});
|
|
|
|
test('scope help info shows detailed help', () => {
|
|
const result = runCli('scope help info', tmpDir);
|
|
assertContains(result.output, 'bmad scope info');
|
|
assertContains(result.output, 'DISPLAYED INFORMATION');
|
|
});
|
|
|
|
test('scope help set shows detailed help', () => {
|
|
const result = runCli('scope help set', tmpDir);
|
|
assertContains(result.output, 'bmad scope set');
|
|
assertContains(result.output, '.bmad-scope');
|
|
assertContains(result.output, 'BMAD_SCOPE');
|
|
assertContains(result.output, 'FILE FORMAT');
|
|
});
|
|
|
|
test('scope help unset shows detailed help', () => {
|
|
const result = runCli('scope help unset', tmpDir);
|
|
assertContains(result.output, 'bmad scope unset');
|
|
assertContains(result.output, 'Clear');
|
|
});
|
|
|
|
test('scope help remove shows detailed help', () => {
|
|
const result = runCli('scope help remove', tmpDir);
|
|
assertContains(result.output, 'bmad scope remove');
|
|
assertContains(result.output, '--force');
|
|
assertContains(result.output, '--no-backup');
|
|
assertContains(result.output, 'BACKUP LOCATION');
|
|
});
|
|
|
|
test('scope help archive shows detailed help', () => {
|
|
const result = runCli('scope help archive', tmpDir);
|
|
assertContains(result.output, 'bmad scope archive');
|
|
assertContains(result.output, 'BEHAVIOR');
|
|
});
|
|
|
|
test('scope help activate shows detailed help', () => {
|
|
const result = runCli('scope help activate', tmpDir);
|
|
assertContains(result.output, 'bmad scope activate');
|
|
assertContains(result.output, 'Reactivate');
|
|
});
|
|
|
|
test('scope help sync-up shows detailed help', () => {
|
|
const result = runCli('scope help sync-up', tmpDir);
|
|
assertContains(result.output, 'bmad scope sync-up');
|
|
assertContains(result.output, 'WHAT GETS PROMOTED');
|
|
assertContains(result.output, '--dry-run');
|
|
assertContains(result.output, '--resolution');
|
|
});
|
|
|
|
test('scope help sync-down shows detailed help', () => {
|
|
const result = runCli('scope help sync-down', tmpDir);
|
|
assertContains(result.output, 'bmad scope sync-down');
|
|
assertContains(result.output, '--dry-run');
|
|
assertContains(result.output, 'keep-local');
|
|
assertContains(result.output, 'keep-shared');
|
|
});
|
|
|
|
// Alias help
|
|
test('scope help ls shows list help', () => {
|
|
const result = runCli('scope help ls', tmpDir);
|
|
assertContains(result.output, 'bmad scope list');
|
|
});
|
|
|
|
test('scope help use shows set help', () => {
|
|
const result = runCli('scope help use', tmpDir);
|
|
assertContains(result.output, 'bmad scope set');
|
|
});
|
|
|
|
test('scope help clear shows unset help', () => {
|
|
const result = runCli('scope help clear', tmpDir);
|
|
assertContains(result.output, 'bmad scope unset');
|
|
});
|
|
|
|
test('scope help rm shows remove help', () => {
|
|
const result = runCli('scope help rm', tmpDir);
|
|
assertContains(result.output, 'bmad scope remove');
|
|
});
|
|
|
|
test('scope help syncup shows sync-up help', () => {
|
|
const result = runCli('scope help syncup', tmpDir);
|
|
assertContains(result.output, 'bmad scope sync-up');
|
|
});
|
|
|
|
// Unknown command help
|
|
test('scope help unknown-cmd shows error', () => {
|
|
const result = runCli('scope help foobar', tmpDir);
|
|
assertContains(result.output, 'Unknown command');
|
|
assertContains(result.output, 'foobar');
|
|
});
|
|
|
|
// No args shows help
|
|
test('scope with no args shows help', () => {
|
|
const result = runCli('scope', tmpDir);
|
|
assertContains(result.output, 'BMAD Scope Management');
|
|
});
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Init Command Tests
|
|
// ============================================================================
|
|
|
|
function testInitCommand() {
|
|
console.log(`\n${colors.blue}${colors.bold}Init Command Tests${colors.reset}`);
|
|
|
|
test('scope init creates configuration', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
const result = runCli('scope init', tmpDir);
|
|
assertTrue(result.success, `Init should succeed: ${result.stderr || result.error}`);
|
|
assertContains(result.output, 'initialized successfully');
|
|
|
|
// Check files created
|
|
assertExists(path.join(tmpDir, '_bmad', '_config', 'scopes.yaml'));
|
|
assertExists(path.join(tmpDir, '_bmad-output', '_shared'));
|
|
assertExists(path.join(tmpDir, '_bmad', '_events'));
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope init is idempotent', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
// Run init twice
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope init', tmpDir);
|
|
assertTrue(result.success, 'Second init should succeed');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Create Command Tests
|
|
// ============================================================================
|
|
|
|
function testCreateCommand() {
|
|
console.log(`\n${colors.blue}${colors.bold}Create Command Tests${colors.reset}`);
|
|
|
|
test('scope create with all options', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope create auth --name "Authentication" --description "User auth"', tmpDir);
|
|
assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
|
|
assertContains(result.output, "Scope 'auth' created successfully");
|
|
|
|
// Check directories created
|
|
assertExists(path.join(tmpDir, '_bmad-output', 'auth', 'planning-artifacts'));
|
|
assertExists(path.join(tmpDir, '_bmad-output', 'auth', 'implementation-artifacts'));
|
|
assertExists(path.join(tmpDir, '_bmad-output', 'auth', 'tests'));
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope create with dependencies', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create users --name "Users" --description ""', tmpDir);
|
|
const result = runCli('scope create auth --name "Auth" --description "" --deps users', tmpDir);
|
|
assertTrue(result.success, 'Create with deps should succeed');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope create with --context flag', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope create auth --name "Auth" --description "" --context', tmpDir);
|
|
assertTrue(result.success, 'Create with context should succeed');
|
|
// Note: project-context.md creation depends on ScopeInitializer implementation
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope create auto-initializes if needed', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
// Don't run init, but create should auto-init
|
|
const result = runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
assertTrue(result.success, 'Create should auto-init');
|
|
assertExists(path.join(tmpDir, '_bmad', '_config', 'scopes.yaml'));
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope create rejects invalid ID (uppercase)', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope create Auth --name "Auth"', tmpDir);
|
|
assertFalse(result.success, 'Should reject uppercase');
|
|
assertContains(result.output + result.stderr, 'Error');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope create rejects invalid ID (underscore)', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope create user_auth --name "Auth" --description ""', tmpDir);
|
|
assertFalse(result.success, 'Should reject underscore');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope create rejects reserved name _shared', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope create _shared --name "Shared" --description ""', tmpDir);
|
|
assertFalse(result.success, 'Should reject _shared');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope new is alias for create', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope new auth --name "Auth"', tmpDir);
|
|
assertTrue(result.success, 'new alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// List Command Tests
|
|
// ============================================================================
|
|
|
|
function testListCommand() {
|
|
console.log(`\n${colors.blue}${colors.bold}List Command Tests${colors.reset}`);
|
|
|
|
test('scope list shows no scopes initially', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope list', tmpDir);
|
|
assertContains(result.output, 'No scopes found');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope list shows created scopes', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Authentication" --description ""', tmpDir);
|
|
runCli('scope create payments --name "Payments" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope list', tmpDir);
|
|
assertContains(result.output, 'auth');
|
|
assertContains(result.output, 'payments');
|
|
assertContains(result.output, 'Authentication');
|
|
assertContains(result.output, 'Payments');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope list --status active filters', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
runCli('scope create old --name "Old" --description ""', tmpDir);
|
|
runCli('scope archive old', tmpDir);
|
|
|
|
const result = runCli('scope list --status active', tmpDir);
|
|
assertContains(result.output, 'auth');
|
|
assertNotContains(result.output, 'old');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope list --status archived filters', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
runCli('scope create old --name "Old" --description ""', tmpDir);
|
|
runCli('scope archive old', tmpDir);
|
|
|
|
const result = runCli('scope list --status archived', tmpDir);
|
|
assertContains(result.output, 'old');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope ls is alias for list', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope ls', tmpDir);
|
|
assertTrue(result.success, 'ls alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope list without init shows helpful message', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
// Remove the _config directory to simulate uninitialized
|
|
fs.rmSync(path.join(tmpDir, '_bmad', '_config'), { recursive: true, force: true });
|
|
|
|
const result = runCli('scope list', tmpDir);
|
|
assertContains(result.output, 'not initialized');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Info Command Tests
|
|
// ============================================================================
|
|
|
|
function testInfoCommand() {
|
|
console.log(`\n${colors.blue}${colors.bold}Info Command Tests${colors.reset}`);
|
|
|
|
test('scope info shows scope details', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Authentication" --description "User auth system"', tmpDir);
|
|
|
|
const result = runCli('scope info auth', tmpDir);
|
|
assertTrue(result.success, 'Info should succeed');
|
|
assertContains(result.output, 'auth');
|
|
assertContains(result.output, 'Authentication');
|
|
assertContains(result.output, 'active');
|
|
assertContains(result.output, 'planning-artifacts');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope info shows dependencies', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create users --name "Users" --description ""', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description "" --deps users', tmpDir);
|
|
|
|
const result = runCli('scope info auth', tmpDir);
|
|
assertContains(result.output, 'Dependencies');
|
|
assertContains(result.output, 'users');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope info on non-existent scope fails', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope info nonexistent', tmpDir);
|
|
assertFalse(result.success, 'Should fail for non-existent scope');
|
|
assertContains(result.output + result.stderr, 'not found');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope info requires ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope info', tmpDir);
|
|
assertFalse(result.success, 'Should require ID');
|
|
assertContains(result.output + result.stderr, 'required');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope show is alias for info', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
const result = runCli('scope show auth', tmpDir);
|
|
assertTrue(result.success, 'show alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope <id> shorthand shows info', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
const result = runCli('scope auth', tmpDir);
|
|
assertTrue(result.success, 'shorthand should work');
|
|
assertContains(result.output, 'auth');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Set/Unset Command Tests
|
|
// ============================================================================
|
|
|
|
function testSetUnsetCommands() {
|
|
console.log(`\n${colors.blue}${colors.bold}Set/Unset Command Tests${colors.reset}`);
|
|
|
|
test('scope set creates .bmad-scope file', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope set auth', tmpDir);
|
|
assertTrue(result.success, `Set should succeed: ${result.stderr || result.error}`);
|
|
assertContains(result.output, "Active scope set to 'auth'");
|
|
|
|
// Check file created
|
|
const scopeFile = path.join(tmpDir, '.bmad-scope');
|
|
assertExists(scopeFile);
|
|
|
|
const content = fs.readFileSync(scopeFile, 'utf8');
|
|
assertContains(content, 'active_scope: auth');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope set validates scope exists', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope set nonexistent', tmpDir);
|
|
assertFalse(result.success, 'Should fail for non-existent scope');
|
|
assertContains(result.output + result.stderr, 'not found');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope set warns for archived scope', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create old --name "Old" --description ""', tmpDir);
|
|
runCli('scope archive old', tmpDir);
|
|
|
|
// This will prompt for confirmation - we can't easily test interactive mode
|
|
// Just verify it doesn't crash with the scope being archived
|
|
const result = runCli('scope info old', tmpDir);
|
|
assertContains(result.output, 'archived');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope unset removes .bmad-scope file', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
runCli('scope set auth', tmpDir);
|
|
|
|
const scopeFile = path.join(tmpDir, '.bmad-scope');
|
|
assertExists(scopeFile);
|
|
|
|
const result = runCli('scope unset', tmpDir);
|
|
assertTrue(result.success, 'Unset should succeed');
|
|
assertContains(result.output, 'Active scope cleared');
|
|
assertNotExists(scopeFile);
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope unset when no scope is set', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope unset', tmpDir);
|
|
assertTrue(result.success, 'Unset should succeed even if no scope');
|
|
assertContains(result.output, 'No active scope');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope use is alias for set', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
const result = runCli('scope use auth', tmpDir);
|
|
assertTrue(result.success, 'use alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope clear is alias for unset', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
runCli('scope set auth', tmpDir);
|
|
const result = runCli('scope clear', tmpDir);
|
|
assertTrue(result.success, 'clear alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Archive/Activate Command Tests
|
|
// ============================================================================
|
|
|
|
function testArchiveActivateCommands() {
|
|
console.log(`\n${colors.blue}${colors.bold}Archive/Activate Command Tests${colors.reset}`);
|
|
|
|
test('scope archive changes status', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope archive auth', tmpDir);
|
|
assertTrue(result.success, 'Archive should succeed');
|
|
assertContains(result.output, 'archived');
|
|
|
|
// Verify status changed
|
|
const infoResult = runCli('scope info auth', tmpDir);
|
|
assertContains(infoResult.output, 'archived');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope archive requires ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope archive', tmpDir);
|
|
assertFalse(result.success, 'Should require ID');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope activate reactivates archived scope', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
runCli('scope archive auth', tmpDir);
|
|
|
|
const result = runCli('scope activate auth', tmpDir);
|
|
assertTrue(result.success, 'Activate should succeed');
|
|
assertContains(result.output, 'activated');
|
|
|
|
// Verify status changed back
|
|
const infoResult = runCli('scope info auth', tmpDir);
|
|
assertContains(infoResult.output, 'active');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope activate requires ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope activate', tmpDir);
|
|
assertFalse(result.success, 'Should require ID');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Remove Command Tests
|
|
// ============================================================================
|
|
|
|
function testRemoveCommand() {
|
|
console.log(`\n${colors.blue}${colors.bold}Remove Command Tests${colors.reset}`);
|
|
|
|
test('scope remove --force removes scope', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope remove auth --force', tmpDir);
|
|
assertTrue(result.success, 'Remove should succeed');
|
|
assertContains(result.output, 'removed successfully');
|
|
|
|
// Verify scope is gone
|
|
const listResult = runCli('scope list', tmpDir);
|
|
assertNotContains(listResult.output, 'auth');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope remove creates backup by default', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope remove auth --force', tmpDir);
|
|
assertContains(result.output, 'backup');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope remove --force --no-backup skips backup', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope remove auth --force --no-backup', tmpDir);
|
|
assertTrue(result.success, 'Remove should succeed');
|
|
assertNotContains(result.output, 'backup was created');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope remove requires ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope remove --force', tmpDir);
|
|
assertFalse(result.success, 'Should require ID');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope remove on non-existent scope fails', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope remove nonexistent --force', tmpDir);
|
|
assertFalse(result.success, 'Should fail');
|
|
assertContains(result.output + result.stderr, 'not found');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope rm is alias for remove', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
const result = runCli('scope rm auth --force', tmpDir);
|
|
assertTrue(result.success, 'rm alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope delete is alias for remove', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
const result = runCli('scope delete auth --force', tmpDir);
|
|
assertTrue(result.success, 'delete alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sync Command Tests
|
|
// ============================================================================
|
|
|
|
function testSyncCommands() {
|
|
console.log(`\n${colors.blue}${colors.bold}Sync Command Tests${colors.reset}`);
|
|
|
|
test('scope sync-up requires scope ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope sync-up', tmpDir);
|
|
assertFalse(result.success, 'Should require ID');
|
|
assertContains(result.output + result.stderr, 'required');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope sync-up validates scope exists', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope sync-up nonexistent', tmpDir);
|
|
assertFalse(result.success, 'Should fail for non-existent scope');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope sync-up --dry-run shows analysis', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope sync-up auth --dry-run', tmpDir);
|
|
assertTrue(result.success, 'Dry run should succeed');
|
|
assertContains(result.output, 'Dry Run');
|
|
assertContains(result.output, 'patterns');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope sync-up runs without errors', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope sync-up auth', tmpDir);
|
|
assertTrue(result.success, `Sync-up should succeed: ${result.stderr || result.error}`);
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope sync-down requires scope ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope sync-down', tmpDir);
|
|
assertFalse(result.success, 'Should require ID');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope sync-down validates scope exists', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope sync-down nonexistent', tmpDir);
|
|
assertFalse(result.success, 'Should fail for non-existent scope');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope sync-down --dry-run shows analysis', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope sync-down auth --dry-run', tmpDir);
|
|
assertTrue(result.success, 'Dry run should succeed');
|
|
assertContains(result.output, 'Dry Run');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope sync-down runs without errors', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope sync-down auth', tmpDir);
|
|
assertTrue(result.success, `Sync-down should succeed: ${result.stderr || result.error}`);
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope syncup is alias for sync-up', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
const result = runCli('scope syncup auth --dry-run', tmpDir);
|
|
assertTrue(result.success, 'syncup alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope syncdown is alias for sync-down', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
const result = runCli('scope syncdown auth --dry-run', tmpDir);
|
|
assertTrue(result.success, 'syncdown alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edge Cases and Error Handling Tests
|
|
// ============================================================================
|
|
|
|
function testEdgeCases() {
|
|
console.log(`\n${colors.blue}${colors.bold}Edge Cases and Error Handling Tests${colors.reset}`);
|
|
|
|
test('handles special characters in scope name', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope create auth --name "Auth & Users (v2)" --description ""', tmpDir);
|
|
assertTrue(result.success, 'Should handle special chars in name');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('handles empty description', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const result = runCli('scope create auth --name "Auth" --description "" --description ""', tmpDir);
|
|
assertTrue(result.success, 'Should handle empty description');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('handles multiple dependencies', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create users --name "Users" --description ""', tmpDir);
|
|
runCli('scope create notifications --name "Notifications" --description ""', tmpDir);
|
|
runCli('scope create logging --name "Logging" --description ""', tmpDir);
|
|
|
|
const result = runCli('scope create auth --name "Auth" --description "" --deps users,notifications,logging', tmpDir);
|
|
assertTrue(result.success, 'Should handle multiple deps');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('handles long scope ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const longId = 'a'.repeat(50);
|
|
const result = runCli(`scope create ${longId} --name "Long ID"`, tmpDir);
|
|
assertTrue(result.success, 'Should handle long ID');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('rejects too long scope ID', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
const tooLongId = 'a'.repeat(51);
|
|
const result = runCli(`scope create ${tooLongId} --name "Too Long"`, tmpDir);
|
|
assertFalse(result.success, 'Should reject too long ID');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('DEBUG env var enables verbose output', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
runCli('scope create auth --name "Auth" --description ""', tmpDir);
|
|
|
|
// Trigger an error with DEBUG enabled
|
|
const result = runCli('scope info nonexistent', tmpDir, { env: { DEBUG: 'true' } });
|
|
// Just verify it doesn't crash with DEBUG enabled
|
|
assertFalse(result.success);
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests
|
|
// ============================================================================
|
|
|
|
function testIntegration() {
|
|
console.log(`\n${colors.blue}${colors.bold}Integration Tests${colors.reset}`);
|
|
|
|
test('full workflow: init -> create -> set -> list -> archive -> activate -> remove', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
// Init
|
|
let result = runCli('scope init', tmpDir);
|
|
assertTrue(result.success, 'Init failed');
|
|
|
|
// Create scopes
|
|
result = runCli('scope create auth --name "Authentication" --description ""', tmpDir);
|
|
assertTrue(result.success, 'Create auth failed');
|
|
|
|
result = runCli('scope create payments --name "Payments" --description "" --deps auth', tmpDir);
|
|
assertTrue(result.success, 'Create payments failed');
|
|
|
|
// Set active scope
|
|
result = runCli('scope set auth', tmpDir);
|
|
assertTrue(result.success, 'Set failed');
|
|
|
|
// List scopes
|
|
result = runCli('scope list', tmpDir);
|
|
assertTrue(result.success, 'List failed');
|
|
assertContains(result.output, 'auth');
|
|
assertContains(result.output, 'payments');
|
|
|
|
// Archive
|
|
result = runCli('scope archive auth', tmpDir);
|
|
assertTrue(result.success, 'Archive failed');
|
|
|
|
// Activate
|
|
result = runCli('scope activate auth', tmpDir);
|
|
assertTrue(result.success, 'Activate failed');
|
|
|
|
// Unset
|
|
result = runCli('scope unset', tmpDir);
|
|
assertTrue(result.success, 'Unset failed');
|
|
|
|
// Remove
|
|
result = runCli('scope remove payments --force', tmpDir);
|
|
assertTrue(result.success, 'Remove payments failed');
|
|
|
|
result = runCli('scope remove auth --force', tmpDir);
|
|
assertTrue(result.success, 'Remove auth failed');
|
|
|
|
// Verify all gone
|
|
result = runCli('scope list', tmpDir);
|
|
assertContains(result.output, 'No scopes found');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('parallel scopes simulation', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCli('scope init', tmpDir);
|
|
|
|
// Create multiple scopes (simulating parallel development)
|
|
runCli('scope create frontend --name "Frontend" --description ""', tmpDir);
|
|
runCli('scope create backend --name "Backend" --description ""', tmpDir);
|
|
runCli('scope create mobile --name "Mobile" --description "" --deps backend', tmpDir);
|
|
|
|
// Verify all exist
|
|
const result = runCli('scope list', tmpDir);
|
|
assertContains(result.output, 'frontend');
|
|
assertContains(result.output, 'backend');
|
|
assertContains(result.output, 'mobile');
|
|
|
|
// Check dependencies
|
|
const infoResult = runCli('scope info mobile', tmpDir);
|
|
assertContains(infoResult.output, 'backend');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Argument Handling Tests (using runCliArray for proper boundary preservation)
|
|
// ============================================================================
|
|
|
|
function testArgumentHandling() {
|
|
console.log(`\n${colors.blue}${colors.bold}Argument Handling Tests${colors.reset}`);
|
|
|
|
test('scope create with multi-word description (array args)', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCliArray(['scope', 'init'], tmpDir);
|
|
const result = runCliArray(
|
|
['scope', 'create', 'auth', '--name', 'Auth Service', '--description', 'Handles user authentication and sessions'],
|
|
tmpDir,
|
|
);
|
|
assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
|
|
|
|
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
assertContains(infoResult.output, 'Auth Service');
|
|
assertContains(infoResult.output, 'Handles user authentication and sessions');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('scope create with 9-word description (regression test)', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCliArray(['scope', 'init'], tmpDir);
|
|
// This exact case caused "too many arguments" error before the fix
|
|
const result = runCliArray(
|
|
['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', 'PRD Auto queue for not inbound yet products'],
|
|
tmpDir,
|
|
);
|
|
assertTrue(result.success, `Should not fail with "too many arguments": ${result.stderr}`);
|
|
assertNotContains(result.stderr || '', 'too many arguments');
|
|
|
|
const infoResult = runCliArray(['scope', 'info', 'auto-queue'], tmpDir);
|
|
assertContains(infoResult.output, 'PRD Auto queue for not inbound yet products');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('all subcommands work with array args', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
// init
|
|
let result = runCliArray(['scope', 'init'], tmpDir);
|
|
assertTrue(result.success, 'init should work');
|
|
|
|
// create
|
|
result = runCliArray(['scope', 'create', 'test', '--name', 'Test Scope', '--description', 'A test scope'], tmpDir);
|
|
assertTrue(result.success, 'create should work');
|
|
|
|
// list
|
|
result = runCliArray(['scope', 'list'], tmpDir);
|
|
assertTrue(result.success, 'list should work');
|
|
assertContains(result.output, 'test');
|
|
|
|
// info
|
|
result = runCliArray(['scope', 'info', 'test'], tmpDir);
|
|
assertTrue(result.success, 'info should work');
|
|
|
|
// set
|
|
result = runCliArray(['scope', 'set', 'test'], tmpDir);
|
|
assertTrue(result.success, 'set should work');
|
|
|
|
// archive
|
|
result = runCliArray(['scope', 'archive', 'test'], tmpDir);
|
|
assertTrue(result.success, 'archive should work');
|
|
|
|
// activate
|
|
result = runCliArray(['scope', 'activate', 'test'], tmpDir);
|
|
assertTrue(result.success, 'activate should work');
|
|
|
|
// sync-up
|
|
result = runCliArray(['scope', 'sync-up', 'test', '--dry-run'], tmpDir);
|
|
assertTrue(result.success, 'sync-up should work');
|
|
|
|
// sync-down
|
|
result = runCliArray(['scope', 'sync-down', 'test', '--dry-run'], tmpDir);
|
|
assertTrue(result.success, 'sync-down should work');
|
|
|
|
// unset
|
|
result = runCliArray(['scope', 'unset'], tmpDir);
|
|
assertTrue(result.success, 'unset should work');
|
|
|
|
// remove
|
|
result = runCliArray(['scope', 'remove', 'test', '--force'], tmpDir);
|
|
assertTrue(result.success, 'remove should work');
|
|
|
|
// help
|
|
result = runCliArray(['scope', 'help'], tmpDir);
|
|
assertTrue(result.success, 'help should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
|
|
test('subcommand aliases work with array args', () => {
|
|
const tmpDir = createTestProject();
|
|
try {
|
|
runCliArray(['scope', 'init'], tmpDir);
|
|
|
|
// new (alias for create) - include --description to avoid interactive prompt
|
|
let result = runCliArray(['scope', 'new', 'test', '--name', 'Test', '--description', ''], tmpDir);
|
|
assertTrue(result.success, 'new alias should work');
|
|
|
|
// ls (alias for list)
|
|
result = runCliArray(['scope', 'ls'], tmpDir);
|
|
assertTrue(result.success, 'ls alias should work');
|
|
|
|
// show (alias for info)
|
|
result = runCliArray(['scope', 'show', 'test'], tmpDir);
|
|
assertTrue(result.success, 'show alias should work');
|
|
|
|
// use (alias for set)
|
|
result = runCliArray(['scope', 'use', 'test'], tmpDir);
|
|
assertTrue(result.success, 'use alias should work');
|
|
|
|
// clear (alias for unset)
|
|
result = runCliArray(['scope', 'clear'], tmpDir);
|
|
assertTrue(result.success, 'clear alias should work');
|
|
|
|
// syncup (alias for sync-up)
|
|
result = runCliArray(['scope', 'syncup', 'test', '--dry-run'], tmpDir);
|
|
assertTrue(result.success, 'syncup alias should work');
|
|
|
|
// syncdown (alias for sync-down)
|
|
result = runCliArray(['scope', 'syncdown', 'test', '--dry-run'], tmpDir);
|
|
assertTrue(result.success, 'syncdown alias should work');
|
|
|
|
// rm (alias for remove)
|
|
result = runCliArray(['scope', 'rm', 'test', '--force'], tmpDir);
|
|
assertTrue(result.success, 'rm alias should work');
|
|
} finally {
|
|
cleanupTestProject(tmpDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Test Runner
|
|
// ============================================================================
|
|
|
|
function main() {
|
|
console.log(`\n${colors.bold}BMAD Scope CLI Test Suite${colors.reset}`);
|
|
console.log(colors.dim + '═'.repeat(70) + colors.reset);
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Run all test suites
|
|
testHelpSystem();
|
|
testInitCommand();
|
|
testCreateCommand();
|
|
testListCommand();
|
|
testInfoCommand();
|
|
testSetUnsetCommands();
|
|
testArchiveActivateCommands();
|
|
testRemoveCommand();
|
|
testSyncCommands();
|
|
testEdgeCases();
|
|
testIntegration();
|
|
testArgumentHandling();
|
|
|
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
|
|
// Summary
|
|
console.log(`\n${colors.dim}${'─'.repeat(70)}${colors.reset}`);
|
|
console.log(`\n${colors.bold}Test Results${colors.reset}`);
|
|
console.log(` Total: ${testCount}`);
|
|
console.log(` ${colors.green}Passed: ${passCount}${colors.reset}`);
|
|
if (failCount > 0) {
|
|
console.log(` ${colors.red}Failed: ${failCount}${colors.reset}`);
|
|
}
|
|
if (skipCount > 0) {
|
|
console.log(` ${colors.yellow}Skipped: ${skipCount}${colors.reset}`);
|
|
}
|
|
console.log(` Time: ${duration}s`);
|
|
|
|
if (failures.length > 0) {
|
|
console.log(`\n${colors.red}${colors.bold}Failures:${colors.reset}`);
|
|
for (const { name, error } of failures) {
|
|
console.log(`\n ${colors.red}✗${colors.reset} ${name}`);
|
|
console.log(` ${colors.dim}${error}${colors.reset}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n${colors.green}${colors.bold}All tests passed!${colors.reset}\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
main();
|