BMAD-METHOD/.patch/477/test-unit-manifest-advanced...

510 lines
16 KiB
JavaScript

/**
* Advanced Tests for Manifest Class
* Coverage: Edge cases, YAML operations, file integrity, migration scenarios
* File: test/unit/manifest-advanced.test.js
*/
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('js-yaml');
const { Manifest } = require('../../tools/cli/installers/lib/core/manifest');
describe('Manifest - Advanced Scenarios', () => {
let tempDir;
let manifest;
beforeEach(async () => {
tempDir = path.join(__dirname, '../fixtures/temp', `manifest-${Date.now()}`);
await fs.ensureDir(tempDir);
manifest = new Manifest();
});
afterEach(async () => {
if (tempDir) {
await fs.remove(tempDir);
}
});
describe('Create Manifest - Advanced', () => {
test('should create manifest with all fields populated', async () => {
const bmadDir = path.join(tempDir, 'bmad');
const data = {
version: '1.0.0',
installDate: '2025-10-26T10:00:00Z',
lastUpdated: '2025-10-26T12:00:00Z',
modules: ['bmb', 'bmm', 'cis'],
ides: ['claude-code', 'github-copilot'],
};
const result = await manifest.create(bmadDir, data);
expect(result.success).toBe(true);
expect(result.path).toContain('manifest.yaml');
// Verify file was created
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
expect(await fs.pathExists(manifestPath)).toBe(true);
// Verify content
const content = await fs.readFile(manifestPath, 'utf8');
const parsed = yaml.load(content);
expect(parsed.installation.version).toBe('1.0.0');
expect(parsed.modules).toContain('bmm');
expect(parsed.ides).toContain('claude-code');
});
test('should create manifest with defaults when data is minimal', async () => {
const bmadDir = path.join(tempDir, 'bmad');
const data = {};
await manifest.create(bmadDir, data);
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
const content = await fs.readFile(manifestPath, 'utf8');
const parsed = yaml.load(content);
expect(parsed.installation).toHaveProperty('version');
expect(parsed.installation).toHaveProperty('installDate');
expect(parsed.installation).toHaveProperty('lastUpdated');
expect(parsed.modules).toEqual([]);
expect(parsed.ides).toEqual([]);
});
test('should overwrite existing manifest', async () => {
const bmadDir = path.join(tempDir, 'bmad');
// Create initial manifest
await manifest.create(bmadDir, {
modules: ['old-module'],
ides: ['old-ide'],
});
// Create new manifest (should overwrite)
await manifest.create(bmadDir, {
modules: ['new-module'],
ides: ['new-ide'],
});
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toContain('new-module');
expect(data.modules).not.toContain('old-module');
expect(data.ides).toContain('new-ide');
});
test('should ensure _cfg directory is created', async () => {
const bmadDir = path.join(tempDir, 'nonexistent', 'bmad');
expect(await fs.pathExists(bmadDir)).toBe(false);
await manifest.create(bmadDir, { modules: [] });
expect(await fs.pathExists(path.join(bmadDir, '_cfg'))).toBe(true);
});
});
describe('Read Manifest - Error Handling', () => {
test('should return null when manifest does not exist', async () => {
const bmadDir = path.join(tempDir, 'nonexistent');
const result = await manifest.read(bmadDir);
expect(result).toBeNull();
});
test('should handle corrupted YAML gracefully', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await fs.ensureDir(path.join(bmadDir, '_cfg'));
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
await fs.writeFile(manifestPath, 'invalid: yaml: [');
const result = await manifest.read(bmadDir);
expect(result).toBeNull();
});
test('should handle empty manifest file', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await fs.ensureDir(path.join(bmadDir, '_cfg'));
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
await fs.writeFile(manifestPath, '');
const result = await manifest.read(bmadDir);
// Empty YAML returns null
expect(result).toBeNull();
});
test('should handle manifest with unexpected structure', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await fs.ensureDir(path.join(bmadDir, '_cfg'));
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
await fs.writeFile(
manifestPath,
yaml.dump({
unexpected: 'structure',
notTheRightFields: true,
}),
);
const result = await manifest.read(bmadDir);
expect(result).toHaveProperty('modules');
expect(result).toHaveProperty('ides');
expect(result.modules).toEqual([]);
});
});
describe('Update Manifest - Advanced', () => {
test('should update specific fields while preserving others', async () => {
const bmadDir = path.join(tempDir, 'bmad');
// Create initial manifest
await manifest.create(bmadDir, {
version: '1.0.0',
modules: ['bmb'],
ides: ['claude-code'],
});
// Update only version
const result = await manifest.update(bmadDir, {
version: '1.1.0',
});
expect(result.version).toBe('1.1.0');
expect(result.modules).toEqual(['bmb']);
expect(result.ides).toEqual(['claude-code']);
});
test('should update lastUpdated timestamp', async () => {
const bmadDir = path.join(tempDir, 'bmad');
const originalDate = '2024-10-20T10:00:00Z';
await manifest.create(bmadDir, {
installDate: originalDate,
lastUpdated: originalDate,
});
// Wait a bit and update
await new Promise((resolve) => setTimeout(resolve, 100));
const result = await manifest.update(bmadDir, { modules: ['new'] });
expect(result.lastUpdated).not.toBe(originalDate);
// Just verify it changed, don't compare exact times due to system clock variations
expect(result.lastUpdated).toBeDefined();
expect(result.installDate).toBe(originalDate);
});
test('should handle updating when manifest does not exist', async () => {
const bmadDir = path.join(tempDir, 'bmad');
// This should create a new manifest
const result = await manifest.update(bmadDir, {
version: '1.0.0',
modules: ['test'],
});
expect(result.version).toBe('1.0.0');
expect(result.modules).toEqual(['test']);
});
test('should handle array field updates correctly', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, {
modules: ['module1', 'module2'],
});
const result = await manifest.update(bmadDir, {
modules: ['module1', 'module2', 'module3'],
});
expect(result.modules).toHaveLength(3);
expect(result.modules).toContain('module3');
});
});
describe('Module Management', () => {
test('should add module to manifest', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { modules: ['bmb'] });
await manifest.addModule(bmadDir, 'bmm');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toContain('bmm');
expect(data.modules).toHaveLength(2);
});
test('should not duplicate modules when adding', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { modules: ['bmb'] });
await manifest.addModule(bmadDir, 'bmb');
await manifest.addModule(bmadDir, 'bmb');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules.filter((m) => m === 'bmb')).toHaveLength(1);
});
test('should handle adding module when none exist', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { modules: [] });
await manifest.addModule(bmadDir, 'first-module');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toEqual(['first-module']);
});
test('should remove module from manifest', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { modules: ['bmb', 'bmm', 'cis'] });
await manifest.removeModule(bmadDir, 'bmm');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).not.toContain('bmm');
expect(data.modules).toContain('bmb');
expect(data.modules).toContain('cis');
});
test('should handle removing non-existent module gracefully', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { modules: ['bmb'] });
await manifest.removeModule(bmadDir, 'nonexistent');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toEqual(['bmb']);
});
test('should handle removing from empty modules', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { modules: [] });
await manifest.removeModule(bmadDir, 'any');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules).toEqual([]);
});
});
describe('IDE Management', () => {
test('should add IDE to manifest', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { ides: ['claude-code'] });
await manifest.addIde(bmadDir, 'github-copilot');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.ides).toContain('github-copilot');
expect(data.ides).toHaveLength(2);
});
test('should not duplicate IDEs when adding', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { ides: ['claude-code'] });
await manifest.addIde(bmadDir, 'claude-code');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.ides.filter((i) => i === 'claude-code')).toHaveLength(1);
});
test('should handle adding to empty IDE list', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { ides: [] });
await manifest.addIde(bmadDir, 'roo');
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.ides).toEqual(['roo']);
});
test('should throw when adding IDE without manifest', async () => {
const bmadDir = path.join(tempDir, 'nonexistent');
await expect(manifest.addIde(bmadDir, 'test')).rejects.toThrow('No manifest found');
});
});
describe('File Hash Calculation', () => {
test('should calculate SHA256 hash of file', async () => {
const filePath = path.join(tempDir, 'test.txt');
const content = 'test content';
await fs.writeFile(filePath, content);
const hash = await manifest.calculateFileHash(filePath);
expect(hash).toBeDefined();
expect(hash).toHaveLength(64); // SHA256 hex string is 64 chars
expect(/^[a-f0-9]{64}$/.test(hash)).toBe(true);
});
test('should return consistent hash for same content', async () => {
const file1 = path.join(tempDir, 'file1.txt');
const file2 = path.join(tempDir, 'file2.txt');
const content = 'identical content';
await fs.writeFile(file1, content);
await fs.writeFile(file2, content);
const hash1 = await manifest.calculateFileHash(file1);
const hash2 = await manifest.calculateFileHash(file2);
expect(hash1).toBe(hash2);
});
test('should return different hash for different content', async () => {
const file1 = path.join(tempDir, 'file1.txt');
const file2 = path.join(tempDir, 'file2.txt');
await fs.writeFile(file1, 'content 1');
await fs.writeFile(file2, 'content 2');
const hash1 = await manifest.calculateFileHash(file1);
const hash2 = await manifest.calculateFileHash(file2);
expect(hash1).not.toBe(hash2);
});
test('should handle non-existent file', async () => {
const filePath = path.join(tempDir, 'nonexistent.txt');
const hash = await manifest.calculateFileHash(filePath);
expect(hash).toBeNull();
});
test('should handle large files', async () => {
const filePath = path.join(tempDir, 'large.txt');
const largeContent = 'x'.repeat(1024 * 1024); // 1MB
await fs.writeFile(filePath, largeContent);
const hash = await manifest.calculateFileHash(filePath);
expect(hash).toBeDefined();
expect(hash).toHaveLength(64);
});
});
describe('YAML Formatting', () => {
test('should format YAML with proper indentation', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, {
version: '1.0.0',
modules: ['bmb', 'bmm', 'cis'],
ides: ['claude-code'],
});
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
const content = await fs.readFile(manifestPath, 'utf8');
// Check for proper YAML formatting
expect(content).toContain('installation:');
expect(content).toContain(' version:');
expect(content).toContain('modules:');
expect(content).not.toContain('\t'); // No tabs, only spaces
});
test('should preserve multiline strings in YAML', async () => {
const bmadDir = path.join(tempDir, 'bmad');
// Create manifest with description
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
await fs.ensureDir(path.dirname(manifestPath));
await fs.writeFile(
manifestPath,
`installation:
version: 1.0.0
description: |
This is a
multiline
description
modules: []
ides: []`,
);
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data).toBeDefined();
});
});
describe('Concurrent Operations', () => {
test('should handle concurrent reads', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, {
modules: ['test'],
ides: ['test-ide'],
});
// Perform concurrent reads
const results = await Promise.all([manifest.read(bmadDir), manifest.read(bmadDir), manifest.read(bmadDir), manifest.read(bmadDir)]);
for (const result of results) {
expect(result.modules).toContain('test');
expect(result.ides).toContain('test-ide');
}
});
test('should handle concurrent module additions', async () => {
const bmadDir = path.join(tempDir, 'bmad');
await manifest.create(bmadDir, { modules: [] });
// Perform concurrent adds (sequential due to file I/O)
await Promise.all([
manifest.addModule(bmadDir, 'module1'),
manifest.addModule(bmadDir, 'module2'),
manifest.addModule(bmadDir, 'module3'),
]);
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.modules.length).toBeGreaterThan(0);
});
});
describe('Edge Cases - Special Values', () => {
test('should handle special characters in module names', async () => {
const bmadDir = path.join(tempDir, 'bmad');
const specialModules = ['module-1', 'module_2', 'module.3', 'module@4'];
await manifest.create(bmadDir, { modules: specialModules });
const read = new Manifest();
const data = await read.read(bmadDir);
for (const mod of specialModules) {
expect(data.modules).toContain(mod);
}
});
test('should handle version strings with special formats', async () => {
const bmadDir = path.join(tempDir, 'bmad');
const versions = ['1.0.0', '1.0.0-alpha', '1.0.0-beta.1', '1.0.0+build.1'];
for (const version of versions) {
await manifest.create(bmadDir, { version });
const read = new Manifest();
const data = await read.read(bmadDir);
expect(data.version).toBe(version);
}
});
});
});