470 lines
16 KiB
JavaScript
470 lines
16 KiB
JavaScript
/**
|
|
* Integration Tests for Status Command with Installation Detection
|
|
* Tests the end-to-end behavior of the status command
|
|
* Issue #478: Status command not detecting BMAD installations
|
|
*/
|
|
|
|
const path = require('node:path');
|
|
const fs = require('fs-extra');
|
|
const { execSync } = require('node:child_process');
|
|
|
|
describe('Status Command Integration Tests - status-command-detection.test.js', () => {
|
|
let testProjectRoot;
|
|
let originalCwd;
|
|
|
|
beforeEach(async () => {
|
|
// Save original working directory
|
|
originalCwd = process.cwd();
|
|
|
|
// Create test project
|
|
testProjectRoot = path.join(__dirname, '..', 'fixtures', `status-test-${Date.now()}`);
|
|
await fs.ensureDir(testProjectRoot);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Restore original working directory
|
|
process.chdir(originalCwd);
|
|
|
|
// Clean up test project
|
|
if (await fs.pathExists(testProjectRoot)) {
|
|
await fs.remove(testProjectRoot);
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// SUITE 1: Status Command from Project Root
|
|
// ==========================================
|
|
|
|
describe('Status Command from Project Root', () => {
|
|
test('REPRODUCES BUG: should detect status when run from project root', async () => {
|
|
// Setup: Create project with BMAD installation
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
await fs.ensureDir(bmadPath);
|
|
await fs.ensureDir(path.join(bmadPath, 'core'));
|
|
|
|
// Create minimal install manifest
|
|
const manifestPath = path.join(bmadPath, '.install-manifest.yaml');
|
|
await fs.writeFile(manifestPath, 'version: 1.0.0\ninstalled_at: "2025-01-01T00:00:00Z"\n');
|
|
|
|
// Change to project root
|
|
process.chdir(testProjectRoot);
|
|
|
|
// Execute: Run status command from project root
|
|
// Simulating: npx bmad-method status
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus('.');
|
|
|
|
// Assert: Should detect installation
|
|
expect(status.installed).toBe(true);
|
|
// This might work or fail depending on cwd handling
|
|
});
|
|
|
|
test('should detect with explicit current directory', async () => {
|
|
// Setup
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
await fs.ensureDir(bmadPath);
|
|
await fs.ensureDir(path.join(bmadPath, 'core'));
|
|
|
|
process.chdir(testProjectRoot);
|
|
|
|
// Execute: Explicit current directory
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(process.cwd());
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true);
|
|
});
|
|
|
|
test('should work with absolute path to project root', async () => {
|
|
// Setup
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
await fs.ensureDir(bmadPath);
|
|
await fs.ensureDir(path.join(bmadPath, 'core'));
|
|
|
|
// Execute: Absolute path
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(testProjectRoot);
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// SUITE 2: Status Command from Subdirectory
|
|
// ==========================================
|
|
|
|
describe('Status Command from Subdirectory (Issue #478)', () => {
|
|
test('REPRODUCES BUG: should search up to find parent BMAD installation', async () => {
|
|
// Setup: Create typical project structure
|
|
// project/
|
|
// ├── bmad/ ← BMAD installed here
|
|
// ├── src/
|
|
// │ └── components/
|
|
// └── package.json
|
|
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
const srcPath = path.join(testProjectRoot, 'src', 'components');
|
|
|
|
await fs.ensureDir(bmadPath);
|
|
await fs.ensureDir(path.join(bmadPath, 'core'));
|
|
await fs.ensureDir(srcPath);
|
|
await fs.writeJSON(path.join(testProjectRoot, 'package.json'), { name: 'test-project' });
|
|
|
|
// Change to subdirectory
|
|
process.chdir(srcPath);
|
|
|
|
// Execute: Run status from subdirectory
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus('.');
|
|
|
|
// Assert: Should find installation in parent directory
|
|
expect(status.installed).toBe(true); // ❌ FAILS - BUG #478
|
|
});
|
|
|
|
test('REPRODUCES BUG: should find installation 2 levels up', async () => {
|
|
// Setup: More deeply nested
|
|
// project/
|
|
// ├── bmad/
|
|
// └── src/
|
|
// └── app/
|
|
// └── utils/
|
|
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
const deepPath = path.join(testProjectRoot, 'src', 'app', 'utils');
|
|
|
|
await fs.ensureDir(bmadPath);
|
|
await fs.ensureDir(path.join(bmadPath, 'core'));
|
|
await fs.ensureDir(deepPath);
|
|
|
|
process.chdir(deepPath);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus('.');
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true); // ❌ FAILS - BUG #478
|
|
});
|
|
|
|
test('REPRODUCES BUG: should find installation 3 levels up', async () => {
|
|
// Setup: Very deeply nested
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
const veryDeepPath = path.join(testProjectRoot, 'a', 'b', 'c', 'd');
|
|
|
|
await fs.ensureDir(bmadPath);
|
|
await fs.ensureDir(path.join(bmadPath, 'core'));
|
|
await fs.ensureDir(veryDeepPath);
|
|
|
|
process.chdir(veryDeepPath);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus('.');
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true); // ❌ FAILS - BUG #478
|
|
});
|
|
|
|
test('REPRODUCES BUG: should work with relative paths from subdirectory', async () => {
|
|
// Setup
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
const srcPath = path.join(testProjectRoot, 'src');
|
|
|
|
await fs.ensureDir(bmadPath);
|
|
await fs.ensureDir(path.join(bmadPath, 'core'));
|
|
await fs.ensureDir(srcPath);
|
|
|
|
process.chdir(srcPath);
|
|
|
|
// Execute: Use relative path ..
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus('..');
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true); // ❌ FAILS
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// SUITE 3: Status Command with Legacy Folders
|
|
// ==========================================
|
|
|
|
describe('Status Command with Legacy Folders', () => {
|
|
test('REPRODUCES BUG: should detect legacy .bmad-core installation', async () => {
|
|
// Setup: Legacy project structure
|
|
const legacyPath = path.join(testProjectRoot, '.bmad-core');
|
|
const agentsPath = path.join(legacyPath, 'agents');
|
|
|
|
await fs.ensureDir(agentsPath);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(testProjectRoot);
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true); // ❌ FAILS - BUG
|
|
});
|
|
|
|
test('REPRODUCES BUG: should detect legacy .bmad-method installation', async () => {
|
|
// Setup
|
|
const legacyPath = path.join(testProjectRoot, '.bmad-method');
|
|
await fs.ensureDir(legacyPath);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(testProjectRoot);
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true); // ❌ FAILS - BUG
|
|
});
|
|
|
|
test('REPRODUCES BUG: should search parents for legacy .bmad-core/', async () => {
|
|
// Setup
|
|
const legacyPath = path.join(testProjectRoot, '.bmad-core');
|
|
const childPath = path.join(testProjectRoot, 'src');
|
|
|
|
await fs.ensureDir(legacyPath);
|
|
await fs.ensureDir(childPath);
|
|
|
|
process.chdir(childPath);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus('.');
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true); // ❌ FAILS - BUG
|
|
});
|
|
|
|
test('REPRODUCES BUG: should prefer modern bmad/ over legacy folders', async () => {
|
|
// Setup: Both exist
|
|
const modernPath = path.join(testProjectRoot, 'bmad');
|
|
const legacyPath = path.join(testProjectRoot, '.bmad-core');
|
|
|
|
await fs.ensureDir(modernPath);
|
|
await fs.ensureDir(path.join(modernPath, 'core'));
|
|
await fs.ensureDir(legacyPath);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(testProjectRoot);
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(true);
|
|
// After fix: expect(status.path).toContain('bmad'); // prefer modern
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// SUITE 4: Status Command Output
|
|
// ==========================================
|
|
|
|
describe('Status Command Output Validation', () => {
|
|
test('should output correct installation info when found', async () => {
|
|
// Setup: Create installation with metadata
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
const corePath = path.join(bmadPath, 'core');
|
|
|
|
await fs.ensureDir(corePath);
|
|
|
|
// Create manifest with version and IDEs
|
|
const manifestPath = path.join(bmadPath, '.install-manifest.yaml');
|
|
await fs.writeFile(
|
|
manifestPath,
|
|
`
|
|
version: 1.2.3
|
|
installed_at: "2025-01-01T00:00:00Z"
|
|
ides:
|
|
- vscode
|
|
- claude-code
|
|
`,
|
|
);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(testProjectRoot);
|
|
|
|
// Assert: Verify returned status object has expected fields
|
|
expect(status).toHaveProperty('installed');
|
|
expect(status).toHaveProperty('path');
|
|
expect(status).toHaveProperty('version');
|
|
expect(status).toHaveProperty('hasCore');
|
|
expect(status.installed).toBe(true);
|
|
expect(status.version).toBe('1.2.3');
|
|
expect(status.hasCore).toBe(true);
|
|
});
|
|
|
|
test('should include IDE info in status output', async () => {
|
|
// Setup
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
await fs.ensureDir(bmadPath);
|
|
|
|
const manifestPath = path.join(bmadPath, '.install-manifest.yaml');
|
|
await fs.writeFile(
|
|
manifestPath,
|
|
`
|
|
version: 1.0.0
|
|
ides:
|
|
- vscode
|
|
- claude-code
|
|
- github-copilot
|
|
`,
|
|
);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(testProjectRoot);
|
|
|
|
// Assert
|
|
expect(status.ides).toBeDefined();
|
|
expect(status.ides.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should return sensible defaults when manifest missing', async () => {
|
|
// Setup: Installation folder exists but no manifest
|
|
const bmadPath = path.join(testProjectRoot, 'bmad');
|
|
const corePath = path.join(bmadPath, 'core');
|
|
|
|
await fs.ensureDir(corePath);
|
|
|
|
// Execute
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(testProjectRoot);
|
|
|
|
// Assert: Should still detect as installed, with defaults
|
|
expect(status.installed).toBe(true);
|
|
expect(status.version).toBeDefined(); // or null, but not undefined
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// SUITE 5: Error Handling
|
|
// ==========================================
|
|
|
|
describe('Status Command Error Handling', () => {
|
|
test('should return installed=false for non-existent directory', async () => {
|
|
// Setup: Directory doesn't exist
|
|
const nonExistent = path.join(testProjectRoot, 'does-not-exist');
|
|
|
|
// Execute: Should handle gracefully
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(nonExistent);
|
|
|
|
// Assert
|
|
expect(status.installed).toBe(false);
|
|
});
|
|
|
|
test('should not crash with permission denied', async () => {
|
|
// Setup: This test may be OS-specific
|
|
// Skip on systems where we can't set permissions
|
|
|
|
if (process.platform === 'win32') {
|
|
// Skip on Windows - permission model is different
|
|
expect(true).toBe(true);
|
|
return;
|
|
}
|
|
|
|
// Create protected directory
|
|
const protectedDir = path.join(testProjectRoot, 'protected');
|
|
await fs.ensureDir(protectedDir);
|
|
|
|
try {
|
|
// Remove read permissions
|
|
fs.chmodSync(protectedDir, 0o000);
|
|
|
|
// Execute: Should handle permission error gracefully
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
|
|
// Should not throw
|
|
const status = await installer.getStatus(protectedDir);
|
|
expect(status).toBeDefined();
|
|
} finally {
|
|
// Restore permissions for cleanup
|
|
fs.chmodSync(protectedDir, 0o755);
|
|
}
|
|
});
|
|
|
|
test('should handle symlinked directories', async () => {
|
|
// Setup: Create real directory and symlink
|
|
const realBmad = path.join(testProjectRoot, 'real-bmad');
|
|
const symlinkBmad = path.join(testProjectRoot, 'link-bmad');
|
|
|
|
await fs.ensureDir(realBmad);
|
|
await fs.ensureDir(path.join(realBmad, 'core'));
|
|
|
|
try {
|
|
fs.symlinkSync(realBmad, symlinkBmad, 'dir');
|
|
|
|
// Execute: Use symlink path
|
|
const { Installer } = require('../../../tools/cli/installers/lib/core/installer');
|
|
const installer = new Installer();
|
|
const status = await installer.getStatus(path.join(testProjectRoot, 'link-bmad'));
|
|
|
|
// Assert: Should resolve symlink and detect installation
|
|
expect(status.installed).toBe(true);
|
|
} catch (error) {
|
|
// Skip if symlinks not supported (Windows without admin)
|
|
if (error.code === 'EEXIST' || error.code === 'EACCES') {
|
|
expect(true).toBe(true);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// TEST SUMMARY
|
|
// ==========================================
|
|
|
|
/*
|
|
* EXPECTED TEST RESULTS:
|
|
*
|
|
* Suite 1 (From Project Root):
|
|
* ? Root detection - UNCLEAR (may work or fail)
|
|
* ✓ Explicit cwd - PASS (if passed correctly)
|
|
* ✓ Absolute path - PASS
|
|
*
|
|
* Suite 2 (From Subdirectory):
|
|
* ✗ Find parent 1 level - FAIL (BUG #478)
|
|
* ✗ Find parent 2 levels - FAIL (BUG #478)
|
|
* ✗ Find parent 3 levels - FAIL (BUG #478)
|
|
* ✗ Relative path .. - FAIL (BUG #478)
|
|
*
|
|
* Suite 3 (Legacy Folders):
|
|
* ✗ .bmad-core detection - FAIL (BUG)
|
|
* ✗ .bmad-method detection - FAIL (BUG)
|
|
* ✗ Legacy parent search - FAIL (BUG)
|
|
* ✓ Modern preference - PASS
|
|
*
|
|
* Suite 4 (Output Validation):
|
|
* ✓ Correct info output - PASS (if detected)
|
|
* ✓ IDE info - PASS
|
|
* ✓ Sensible defaults - PASS
|
|
*
|
|
* Suite 5 (Error Handling):
|
|
* ✓ Non-existent dir - PASS
|
|
* ? Permission denied - OS-DEPENDENT
|
|
* ? Symlinks - PLATFORM-DEPENDENT
|
|
*
|
|
* SUMMARY: ~8-10 tests expected to FAIL
|
|
*/
|
|
});
|