BMAD-METHOD/.patch/477/test/unit/config-loader-advanced.test.js

418 lines
14 KiB
JavaScript

/**
* Advanced Tests for ManifestConfigLoader
* Coverage: Edge cases, error scenarios, performance, complex nested structures
* File: test/unit/config-loader-advanced.test.js
*/
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('js-yaml');
const { ManifestConfigLoader } = require('../../tools/cli/lib/config-loader');
describe('ManifestConfigLoader - Advanced Scenarios', () => {
let tempDir;
let loader;
beforeEach(async () => {
tempDir = path.join(__dirname, '../fixtures/temp', `loader-${Date.now()}`);
await fs.ensureDir(tempDir);
loader = new ManifestConfigLoader();
});
afterEach(async () => {
if (tempDir) {
await fs.remove(tempDir);
}
});
describe('Complex Nested Structures', () => {
test('should handle deeply nested keys with multiple levels', async () => {
const manifestPath = path.join(tempDir, 'deep.yaml');
const manifest = {
level1: {
level2: {
level3: {
level4: {
level5: 'deep value',
},
},
},
},
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('level1.level2.level3.level4.level5')).toBe('deep value');
});
test('should handle arrays in nested structures', async () => {
const manifestPath = path.join(tempDir, 'arrays.yaml');
const manifest = {
modules: ['bmb', 'bmm', 'cis'],
ides: {
configured: ['claude-code', 'github-copilot'],
available: ['roo', 'cline'],
},
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
const ides = loader.getConfig('ides');
expect(ides.configured).toContain('claude-code');
expect(ides.available).toContain('cline');
});
test('should handle mixed data types in nested structures', async () => {
const manifestPath = path.join(tempDir, 'mixed.yaml');
const manifest = {
config: {
string: 'value',
number: 42,
boolean: true,
null: null,
array: [1, 2, 3],
nested: {
date: '2025-10-26T12:00:00Z',
version: '1.0.0',
},
},
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('config.string')).toBe('value');
expect(loader.getConfig('config.number')).toBe(42);
expect(loader.getConfig('config.boolean')).toBe(true);
expect(loader.getConfig('config.null')).toBeNull();
expect(loader.getConfig('config.nested.version')).toBe('1.0.0');
});
});
describe('Edge Cases - Empty and Null Values', () => {
test('should handle empty config objects', async () => {
const manifestPath = path.join(tempDir, 'empty.yaml');
await fs.writeFile(manifestPath, yaml.dump({}));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('any.key', 'default')).toBe('default');
});
test('should differentiate between null and undefined', async () => {
const manifestPath = path.join(tempDir, 'nulls.yaml');
const manifest = {
explicit_null: null,
explicit_value: 'value',
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('explicit_null')).toBeNull();
expect(loader.getConfig('explicit_null', 'default')).toBeNull();
expect(loader.getConfig('missing_key', 'default')).toBe('default');
});
test('should handle empty arrays', async () => {
const manifestPath = path.join(tempDir, 'empty_arrays.yaml');
const manifest = {
ides: [],
modules: [],
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('ides')).toEqual([]);
expect(loader.getConfig('modules')).toEqual([]);
});
test('should handle empty strings', async () => {
const manifestPath = path.join(tempDir, 'empty_strings.yaml');
const manifest = {
empty: '',
normal: 'value',
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('empty')).toBe('');
expect(loader.getConfig('empty', 'default')).toBe('');
});
});
describe('Caching Behavior - Advanced', () => {
test('should return cached config on subsequent calls with same path', async () => {
const manifestPath = path.join(tempDir, 'cache.yaml');
const manifest = { test: 'value', updated: '2025-10-26' };
await fs.writeFile(manifestPath, yaml.dump(manifest));
const first = await loader.loadManifest(manifestPath);
const second = await loader.loadManifest(manifestPath);
expect(first).toEqual(second);
expect(first).toBe(second); // Same reference
});
test('should reload config when path changes', async () => {
const path1 = path.join(tempDir, 'manifest1.yaml');
const path2 = path.join(tempDir, 'manifest2.yaml');
const manifest1 = { source: 'manifest1' };
const manifest2 = { source: 'manifest2' };
await fs.writeFile(path1, yaml.dump(manifest1));
await fs.writeFile(path2, yaml.dump(manifest2));
await loader.loadManifest(path1);
expect(loader.getConfig('source')).toBe('manifest1');
await loader.loadManifest(path2);
expect(loader.getConfig('source')).toBe('manifest2');
});
test('should return cached config after clearCache and hasConfig check', async () => {
const manifestPath = path.join(tempDir, 'cache2.yaml');
const manifest = { key: 'value' };
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
loader.clearCache();
expect(loader.getConfig('key', 'default')).toBe('default');
expect(loader.hasConfig('key')).toBe(false);
});
test('should handle rapid sequential loads efficiently', async () => {
const manifestPath = path.join(tempDir, 'rapid.yaml');
const manifest = { data: 'value'.repeat(1000) };
await fs.writeFile(manifestPath, yaml.dump(manifest));
const results = [];
for (let i = 0; i < 100; i++) {
const result = await loader.loadManifest(manifestPath);
results.push(result);
}
// All should be same reference (cached)
for (let i = 1; i < results.length; i++) {
expect(results[i]).toBe(results[0]);
}
});
});
describe('Error Handling - Invalid Files', () => {
test('should handle non-existent manifest files', async () => {
const manifestPath = path.join(tempDir, 'nonexistent.yaml');
const result = await loader.loadManifest(manifestPath);
expect(result).toEqual({});
expect(loader.getConfig('any', 'default')).toBe('default');
});
test('should throw on invalid YAML syntax', async () => {
const manifestPath = path.join(tempDir, 'invalid.yaml');
await fs.writeFile(manifestPath, 'invalid: yaml: content: [');
await expect(loader.loadManifest(manifestPath)).rejects.toThrow('Invalid YAML in manifest');
});
test('should throw on malformed YAML structures', async () => {
const manifestPath = path.join(tempDir, 'malformed.yaml');
await fs.writeFile(manifestPath, 'key: value\n invalid indentation: here');
await expect(loader.loadManifest(manifestPath)).rejects.toThrow();
});
test('should handle binary/non-text files gracefully', async () => {
const manifestPath = path.join(tempDir, 'binary.yaml');
await fs.writeFile(manifestPath, Buffer.from([0xff, 0xfe, 0x00, 0x00]));
// YAML parser will fail on binary data
await expect(loader.loadManifest(manifestPath)).rejects.toThrow();
});
test('should handle permission errors', async () => {
if (process.platform === 'win32') {
// Skip on Windows as permissions work differently
expect(true).toBe(true);
return;
}
const manifestPath = path.join(tempDir, 'noperms.yaml');
await fs.writeFile(manifestPath, yaml.dump({ test: 'value' }));
await fs.chmod(manifestPath, 0o000);
try {
await expect(loader.loadManifest(manifestPath)).rejects.toThrow();
} finally {
// Restore permissions for cleanup
await fs.chmod(manifestPath, 0o644);
}
});
});
describe('hasConfig Method - Advanced', () => {
test('should correctly identify nested keys existence', async () => {
const manifestPath = path.join(tempDir, 'hasconfig.yaml');
const manifest = {
installation: {
version: '1.0.0',
date: '2025-10-26',
},
modules: ['bmb', 'bmm'],
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.hasConfig('installation.version')).toBe(true);
expect(loader.hasConfig('installation.missing')).toBe(false);
expect(loader.hasConfig('modules')).toBe(true);
expect(loader.hasConfig('missing')).toBe(false);
});
test('should handle hasConfig on null values', async () => {
const manifestPath = path.join(tempDir, 'hasnull.yaml');
const manifest = {
explicit_null: null,
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.hasConfig('explicit_null')).toBe(true);
expect(loader.getConfig('explicit_null')).toBeNull();
});
test('should handle hasConfig before loadManifest', () => {
expect(loader.hasConfig('any.key')).toBe(false);
});
test('should return false for paths through non-objects', async () => {
const manifestPath = path.join(tempDir, 'paththrough.yaml');
const manifest = {
scalar: 'value',
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.hasConfig('scalar.nested')).toBe(false);
});
});
describe('Special Characters and Encoding', () => {
test('should handle unicode characters in values', async () => {
const manifestPath = path.join(tempDir, 'unicode.yaml');
const manifest = {
emoji: '🎯 BMAD ✨',
chinese: '中文测试',
arabic: 'اختبار عربي',
};
await fs.writeFile(manifestPath, yaml.dump(manifest, { lineWidth: -1 }));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('emoji')).toBe('🎯 BMAD ✨');
expect(loader.getConfig('chinese')).toBe('中文测试');
expect(loader.getConfig('arabic')).toBe('اختبار عربي');
});
test('should handle paths with special characters', async () => {
const manifestPath = path.join(tempDir, 'special_chars.yaml');
const manifest = {
'installation-date': '2025-10-26',
last_updated: '2025-10-26T12:00:00Z',
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('installation-date')).toBe('2025-10-26');
expect(loader.getConfig('last_updated')).toBe('2025-10-26T12:00:00Z');
});
test('should handle multiline strings', async () => {
const manifestPath = path.join(tempDir, 'multiline.yaml');
const manifest = {
description: 'This is a\nmultiline\ndescription',
config: 'Line 1\nLine 2\nLine 3',
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
expect(loader.getConfig('description')).toContain('\n');
expect(loader.getConfig('description')).toContain('multiline');
});
});
describe('Performance and Scale', () => {
test('should handle large manifest files', async () => {
const manifestPath = path.join(tempDir, 'large.yaml');
const manifest = {
modules: Array.from({ length: 1000 }, (_, i) => `module-${i}`),
configs: {},
};
// Add 500 config entries
for (let i = 0; i < 500; i++) {
manifest.configs[`config-${i}`] = `value-${i}`;
}
await fs.writeFile(manifestPath, yaml.dump(manifest));
const start = Date.now();
await loader.loadManifest(manifestPath);
const loadTime = Date.now() - start;
expect(loader.getConfig('modules.0')).toBe('module-0');
expect(loader.getConfig('modules.999')).toBe('module-999');
expect(loader.getConfig('configs.config-250')).toBe('value-250');
expect(loadTime).toBeLessThan(1000); // Should load in under 1 second
});
test('should handle many sequential getConfig calls efficiently', async () => {
const manifestPath = path.join(tempDir, 'perf.yaml');
const manifest = {
a: { b: { c: { d: 'value' } } },
x: 'test',
};
await fs.writeFile(manifestPath, yaml.dump(manifest));
await loader.loadManifest(manifestPath);
const start = Date.now();
for (let i = 0; i < 10_000; i++) {
loader.getConfig('a.b.c.d');
}
const time = Date.now() - start;
expect(time).toBeLessThan(100); // Should be very fast (cached)
});
});
describe('State Management', () => {
test('should maintain separate state for multiple loaders', async () => {
const loader1 = new ManifestConfigLoader();
const loader2 = new ManifestConfigLoader();
const path1 = path.join(tempDir, 'loader1.yaml');
const path2 = path.join(tempDir, 'loader2.yaml');
await fs.writeFile(path1, yaml.dump({ source: 'loader1' }));
await fs.writeFile(path2, yaml.dump({ source: 'loader2' }));
await loader1.loadManifest(path1);
await loader2.loadManifest(path2);
expect(loader1.getConfig('source')).toBe('loader1');
expect(loader2.getConfig('source')).toBe('loader2');
});
test('should clear cache properly', async () => {
const manifestPath = path.join(tempDir, 'clear.yaml');
await fs.writeFile(manifestPath, yaml.dump({ test: 'value' }));
await loader.loadManifest(manifestPath);
expect(loader.hasConfig('test')).toBe(true);
loader.clearCache();
expect(loader.hasConfig('test')).toBe(false);
expect(loader.getConfig('test', 'default')).toBe('default');
});
});
});