BMAD-METHOD/.patch/477/test/integration/invalid-manifest-fallback.t...

313 lines
10 KiB
JavaScript

/**
* Integration Tests - Invalid Manifest Fallback
* Tests for graceful handling of corrupted or invalid manifests
* File: test/integration/invalid-manifest-fallback.test.js
*/
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('js-yaml');
const { Installer } = require('../../tools/cli/installers/lib/core/installer');
describe('Invalid Manifest Handling', () => {
let tempDir;
let installer;
beforeEach(() => {
tempDir = path.join(__dirname, '../fixtures/temp', `invalid-${Date.now()}`);
fs.ensureDirSync(tempDir);
installer = new Installer();
});
afterEach(() => {
if (fs.existsSync(tempDir)) {
fs.removeSync(tempDir);
}
});
describe('Corrupted Manifest Recovery', () => {
// Test 7.1: Fallback on Corrupted File
it('should fallback on corrupted manifest file', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const corruptedYaml = `
version: 4.36.2
installed_at: [invalid yaml format
install_type: full
`;
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
fs.writeFileSync(manifestPath, corruptedYaml);
const mode = installer.detectInstallMode(projectDir, '4.39.2');
expect(mode).toBe('invalid');
});
it('should not throw when reading corrupted manifest', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
fs.writeFileSync(path.join(bmadDir, 'install-manifest.yaml'), '{invalid}');
expect(() => {
installer.detectInstallMode(projectDir, '4.39.2');
}).not.toThrow();
});
it('should treat corrupted manifest as fresh install', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
fs.writeFileSync(path.join(bmadDir, 'install-manifest.yaml'), 'bad yaml: [');
const mode = installer.detectInstallMode(projectDir, '4.39.2');
expect(mode).toBe('invalid');
// In context: invalid = ask all questions (same as fresh)
const shouldAskQuestions = mode === 'fresh' || mode === 'invalid';
expect(shouldAskQuestions).toBe(true);
});
});
describe('Missing Required Fields', () => {
// Test 7.2: Fallback on Missing Required Field
it('should fallback on missing required field', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const manifestMissingVersion = {
installed_at: '2025-08-12T23:51:04.439Z',
install_type: 'full',
// version is missing - required field
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
fs.writeFileSync(manifestPath, yaml.dump(manifestMissingVersion));
const validator = installer.getValidator();
const result = validator.validateManifest(manifestMissingVersion);
expect(result.isValid).toBe(false);
expect(result.errors.some((e) => e.includes('version'))).toBe(true);
});
it('should ask questions when validation fails', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const invalidManifest = {
installed_at: '2025-08-12T23:51:04.439Z',
// Missing required fields: version, install_type
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
fs.writeFileSync(manifestPath, yaml.dump(invalidManifest));
const validator = installer.getValidator();
const result = validator.validateManifest(invalidManifest);
// When validation fails, should ask questions
const shouldAskQuestions = !result.isValid;
expect(shouldAskQuestions).toBe(true);
});
it('should log reason for validation failure', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const manifestMissingInstallType = {
version: '4.36.2',
installed_at: '2025-08-12T23:51:04.439Z',
// install_type is missing
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
fs.writeFileSync(manifestPath, yaml.dump(manifestMissingInstallType));
const validator = installer.getValidator();
const result = validator.validateManifest(manifestMissingInstallType);
expect(result.errors).toBeDefined();
expect(result.errors.length).toBeGreaterThan(0);
});
});
describe('Manifest Preservation on Error', () => {
// Test 7.3: No Manifest Corruption
it('should never corrupt existing manifest on error', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const originalManifest = {
version: '4.36.2',
installed_at: '2025-08-12T23:51:04.439Z',
install_type: 'full',
custom_data: 'important-value',
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
const originalContent = yaml.dump(originalManifest);
fs.writeFileSync(manifestPath, originalContent);
// Try to process manifest (even if there's an error)
try {
await installer.loadConfigForProject(projectDir);
} catch {
// Ignore errors
}
// Original manifest should be unchanged
const fileContent = fs.readFileSync(manifestPath, 'utf8');
expect(fileContent).toBe(originalContent);
});
it('should not write to manifest during detection', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const manifest = {
version: '4.36.2',
installed_at: '2025-08-12T23:51:04.439Z',
install_type: 'full',
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
fs.writeFileSync(manifestPath, yaml.dump(manifest));
const originalStats = fs.statSync(manifestPath);
const originalMtime = originalStats.mtime.getTime();
// Run detection
installer.detectInstallMode(projectDir, '4.39.2');
// File should not be modified
const newStats = fs.statSync(manifestPath);
expect(newStats.mtime.getTime()).toBe(originalMtime);
});
it('should create backup before any write operations', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const manifest = {
version: '4.36.2',
installed_at: '2025-08-12T23:51:04.439Z',
install_type: 'full',
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
const content = yaml.dump(manifest);
fs.writeFileSync(manifestPath, content);
// In real implementation, backup would be created before write
const backupPath = `${manifestPath}.bak`;
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(manifestPath, backupPath);
}
// Verify backup exists
expect(fs.existsSync(backupPath)).toBe(true);
// Clean up
fs.removeSync(backupPath);
});
});
describe('Error Recovery and User Feedback', () => {
it('should provide clear error messages for invalid manifest', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const invalidManifest = {
version: 'invalid-format',
installed_at: '2025-08-12T23:51:04.439Z',
install_type: 'full',
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
fs.writeFileSync(manifestPath, yaml.dump(invalidManifest));
const validator = installer.getValidator();
const result = validator.validateManifest(invalidManifest);
expect(result.errors).toBeDefined();
expect(result.errors.length).toBeGreaterThan(0);
expect(result.errors[0]).toContain('version');
});
it('should allow recovery by asking for confirmation', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const corruptedManifest = 'invalid';
fs.writeFileSync(path.join(bmadDir, 'install-manifest.yaml'), corruptedManifest);
const mode = installer.detectInstallMode(projectDir, '4.39.2');
// When invalid, user can choose to reconfigure
const context = {
mode,
userChoice: mode === 'invalid' ? 'reconfigure' : 'skip-questions',
};
expect(context.mode).toBe('invalid');
expect(context.userChoice).toBe('reconfigure');
});
});
describe('Graceful Degradation', () => {
it('should handle missing optional fields without error', async () => {
const projectDir = tempDir;
const bmadDir = path.join(projectDir, '.bmad-core');
fs.ensureDirSync(bmadDir);
const manifest = {
version: '4.36.2',
installed_at: '2025-08-12T23:51:04.439Z',
install_type: 'full',
// Missing: ides_setup, expansion_packs (optional)
};
const manifestPath = path.join(bmadDir, 'install-manifest.yaml');
fs.writeFileSync(manifestPath, yaml.dump(manifest));
const validator = installer.getValidator();
const result = validator.validateManifest(manifest);
expect(result.isValid).toBe(true);
});
it('should apply defaults for missing optional fields', async () => {
const manifest = {
version: '4.36.2',
installed_at: '2025-08-12T23:51:04.439Z',
install_type: 'full',
};
const config = {
getConfig: (key, defaultValue) => {
const config = manifest;
return key in config ? config[key] : defaultValue;
},
};
expect(config.getConfig('ides_setup', [])).toEqual([]);
expect(config.getConfig('expansion_packs', [])).toEqual([]);
expect(config.getConfig('some_unknown_field', 'default')).toBe('default');
});
});
});