BMAD-METHOD/.patch/477/test-integration-installer-...

515 lines
16 KiB
JavaScript

/**
* Integration Tests for Installer with Configuration Changes
* Coverage: Real-world installer scenarios, workflow integration, error paths
* File: test/integration/installer-config-changes.test.js
*/
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('js-yaml');
describe('Installer Configuration Changes - Integration', () => {
let tempDir;
let projectDir;
let bmadDir;
let configDir;
beforeEach(async () => {
tempDir = path.join(__dirname, '../fixtures/temp', `installer-${Date.now()}`);
projectDir = path.join(tempDir, 'project');
bmadDir = path.join(projectDir, 'bmad');
configDir = path.join(bmadDir, '_cfg');
await fs.ensureDir(projectDir);
});
afterEach(async () => {
if (tempDir) {
await fs.remove(tempDir);
}
});
describe('Fresh Installation Flow', () => {
test('should create manifest on fresh install', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const installData = {
version: '1.0.0',
modules: ['bmb', 'bmm'],
ides: ['claude-code'],
};
await manifest.create(bmadDir, installData);
const manifestPath = path.join(configDir, 'manifest.yaml');
expect(await fs.pathExists(manifestPath)).toBe(true);
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.version).toBe('1.0.0');
expect(data.modules).toContain('bmb');
});
test('should initialize empty arrays for fresh install', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, {});
const read = new Manifest();
const data = await read.read(bmadDir);
expect(Array.isArray(data.modules)).toBe(true);
expect(Array.isArray(data.ides)).toBe(true);
expect(data.modules.length).toBe(0);
expect(data.ides.length).toBe(0);
});
test('should set installation date on fresh install', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const beforeTime = new Date().toISOString();
await manifest.create(bmadDir, {});
const afterTime = new Date().toISOString();
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.installDate).toBeDefined();
const installDate = new Date(data.installDate);
expect(installDate.getTime()).toBeGreaterThanOrEqual(new Date(beforeTime).getTime());
expect(installDate.getTime()).toBeLessThanOrEqual(new Date(afterTime).getTime() + 1000);
});
});
describe('Update Installation Flow', () => {
test('should preserve install date on update', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const originalDate = '2025-10-20T10:00:00Z';
await manifest.create(bmadDir, {
installDate: originalDate,
});
// Update
await manifest.update(bmadDir, {
modules: ['new-module'],
});
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.installDate).toBe(originalDate);
});
test('should update version on upgrade', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, {
version: '1.0.0',
});
// Simulate upgrade
await manifest.update(bmadDir, {
version: '1.1.0',
});
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.version).toBe('1.1.0');
});
test('should handle module additions during update', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
// Initial installation
await manifest.create(bmadDir, {
modules: ['bmb'],
});
// Add module during update
await manifest.addModule(bmadDir, 'bmm');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toContain('bmb');
expect(data.modules).toContain('bmm');
expect(data.modules).toHaveLength(2);
});
test('should handle IDE additions during update', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
// Initial installation
await manifest.create(bmadDir, {
ides: ['claude-code'],
});
// Add IDE during update
await manifest.addIde(bmadDir, 'github-copilot');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.ides).toContain('claude-code');
expect(data.ides).toContain('github-copilot');
expect(data.ides).toHaveLength(2);
});
});
describe('Configuration Loading', () => {
test('should load configuration from previous installation', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const { ManifestConfigLoader } = require('../../tools/cli/lib/config-loader');
const manifest = new Manifest();
await manifest.create(bmadDir, {
version: '1.0.0',
modules: ['bmb', 'bmm'],
ides: ['claude-code'],
});
// Now load it
const loader = new ManifestConfigLoader();
const manifestPath = path.join(configDir, 'manifest.yaml');
const config = await loader.loadManifest(manifestPath);
expect(config).toBeDefined();
expect(config.installation.version).toBe('1.0.0');
expect(config.modules).toContain('bmm');
});
test('should use cached configuration on repeated access', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const { ManifestConfigLoader } = require('../../tools/cli/lib/config-loader');
const manifest = new Manifest();
await manifest.create(bmadDir, {
version: '1.0.0',
modules: ['bmb'],
});
const loader = new ManifestConfigLoader();
const manifestPath = path.join(configDir, 'manifest.yaml');
const config1 = await loader.loadManifest(manifestPath);
const config2 = await loader.loadManifest(manifestPath);
// Should be same reference (cached)
expect(config1).toBe(config2);
});
test('should detect when config was not previously saved', async () => {
const { ManifestConfigLoader } = require('../../tools/cli/lib/config-loader');
const loader = new ManifestConfigLoader();
const manifestPath = path.join(configDir, 'manifest.yaml');
const config = await loader.loadManifest(manifestPath);
expect(config).toEqual({});
});
});
describe('Complex Multi-Module Scenarios', () => {
test('should track multiple modules across installations', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const modules = ['bmb', 'bmm', 'cis', 'expansion-pack-1'];
await manifest.create(bmadDir, { modules });
for (let i = 2; i <= 4; i++) {
await manifest.addModule(bmadDir, `expansion-pack-${i}`);
}
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toHaveLength(7);
for (const mod of modules) {
expect(data.modules).toContain(mod);
}
});
test('should handle IDE ecosystem changes', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const ides = ['claude-code', 'github-copilot', 'cline', 'roo'];
await manifest.create(bmadDir, { ides: [] });
for (const ide of ides) {
await manifest.addIde(bmadDir, ide);
}
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.ides).toHaveLength(4);
for (const ide of ides) {
expect(data.ides).toContain(ide);
}
});
test('should handle mixed add/remove operations', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, {
modules: ['bmb', 'bmm', 'cis'],
});
// Remove middle module
await manifest.removeModule(bmadDir, 'bmm');
// Add new module
await manifest.addModule(bmadDir, 'new-module');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toContain('bmb');
expect(data.modules).not.toContain('bmm');
expect(data.modules).toContain('cis');
expect(data.modules).toContain('new-module');
expect(data.modules).toHaveLength(3);
});
});
describe('File System Integrity', () => {
test('should create proper directory structure', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, {});
expect(await fs.pathExists(bmadDir)).toBe(true);
expect(await fs.pathExists(configDir)).toBe(true);
expect(await fs.pathExists(path.join(configDir, 'manifest.yaml'))).toBe(true);
});
test('should handle nested directory creation', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const deepBmadDir = path.join(tempDir, 'a', 'b', 'c', 'd', 'bmad');
await manifest.create(deepBmadDir, {});
expect(await fs.pathExists(deepBmadDir)).toBe(true);
expect(await fs.pathExists(path.join(deepBmadDir, '_cfg', 'manifest.yaml'))).toBe(true);
});
test('should preserve file permissions', async () => {
if (process.platform === 'win32') {
// Skip permissions test on Windows
expect(true).toBe(true);
return;
}
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, {});
const manifestPath = path.join(configDir, 'manifest.yaml');
const stats = await fs.stat(manifestPath);
// File should be readable
expect(stats.mode & 0o400).toBeDefined();
});
});
describe('Manifest Validation During Installation', () => {
test('should validate manifest after creation', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const { ManifestValidator } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, {
version: '1.0.0',
modules: ['bmb'],
});
const manifestPath = path.join(configDir, 'manifest.yaml');
const content = await fs.readFile(manifestPath, 'utf8');
const data = yaml.load(content);
// Should be valid YAML
expect(data).toBeDefined();
expect(data.installation).toBeDefined();
expect(data.modules).toBeDefined();
});
test('should maintain data integrity through read/write cycles', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const originalData = {
version: '1.5.3',
modules: ['bmb', 'bmm', 'cis'],
ides: ['claude-code', 'github-copilot', 'roo'],
};
// Write
await manifest.create(bmadDir, originalData);
// Read
const read1 = new Manifest();
const data1 = await read1.read(bmadDir);
// Write again (update)
await manifest.update(bmadDir, {
version: '1.5.4',
});
// Read again
const read2 = new Manifest();
const data2 = await read2.read(bmadDir);
// Verify data integrity
expect(data2.version).toBe('1.5.4');
expect(data2.modules).toEqual(originalData.modules);
expect(data2.ides).toEqual(originalData.ides);
});
});
describe('Concurrency and State Management', () => {
test('should handle rapid sequential updates', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, { modules: [] });
// Rapid updates
for (let i = 1; i <= 10; i++) {
await manifest.addModule(bmadDir, `module-${i}`);
}
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toHaveLength(10);
for (let i = 1; i <= 10; i++) {
expect(data.modules).toContain(`module-${i}`);
}
});
test('should handle multiple manifest instances independently', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest1 = new Manifest();
const manifest2 = new Manifest();
const dir1 = path.join(tempDir, 'project1', 'bmad');
const dir2 = path.join(tempDir, 'project2', 'bmad');
await manifest1.create(dir1, { modules: ['m1'] });
await manifest2.create(dir2, { modules: ['m2'] });
const read1 = new Manifest();
const read2 = new Manifest();
const data1 = await read1.read(dir1);
const data2 = await read2.read(dir2);
expect(data1.modules).toEqual(['m1']);
expect(data2.modules).toEqual(['m2']);
});
});
describe('Version Tracking Across Updates', () => {
test('should track version history through updates', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
const versions = ['1.0.0', '1.0.1', '1.1.0', '2.0.0'];
// Initial install
await manifest.create(bmadDir, { version: versions[0] });
// Updates
for (let i = 1; i < versions.length; i++) {
await manifest.update(bmadDir, { version: versions[i] });
}
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.version).toBe(versions.at(-1));
});
test('should record timestamps for installations', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
await manifest.create(bmadDir, {});
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.installDate).toBeDefined();
expect(data.lastUpdated).toBeDefined();
const installDate = new Date(data.installDate);
const lastUpdated = new Date(data.lastUpdated);
expect(installDate.getTime()).toBeGreaterThan(0);
expect(lastUpdated.getTime()).toBeGreaterThan(0);
});
});
describe('Error Recovery', () => {
test('should recover from corrupted manifest', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
// Create valid manifest
let manifest = new Manifest();
await manifest.create(bmadDir, { version: '1.0.0' });
// Corrupt it
const manifestPath = path.join(configDir, 'manifest.yaml');
await fs.writeFile(manifestPath, 'invalid: yaml: [');
// Try to recover by recreating
manifest = new Manifest();
await manifest.create(bmadDir, {
version: '1.0.1',
modules: ['recovered'],
});
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.version).toBe('1.0.1');
expect(data.modules).toContain('recovered');
});
test('should handle missing _cfg directory gracefully', async () => {
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
const manifest = new Manifest();
// Ensure directory doesn't exist
const nonExistentDir = path.join(tempDir, 'nonexistent', 'bmad');
expect(await fs.pathExists(nonExistentDir)).toBe(false);
// Should create it
await manifest.create(nonExistentDir, {});
expect(await fs.pathExists(nonExistentDir)).toBe(true);
expect(await fs.pathExists(path.join(nonExistentDir, '_cfg'))).toBe(true);
});
});
});