fix: resolve review findings for community/custom module support
- Remove redundant CommunityModuleManager instantiation in UI display - Remove dead customModulesMeta field from Config (never populated) - Add 35 unit tests for CustomModuleManager and CommunityModuleManager pure functions: URL validation, normalization, search, featured, categories
This commit is contained in:
parent
7c58c0bab1
commit
cccc8218fb
|
|
@ -1723,6 +1723,171 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 33: Community & Custom Module Managers
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 33: Community & Custom Module Managers${colors.reset}\n`);
|
||||
|
||||
// --- CustomModuleManager.validateGitHubUrl ---
|
||||
{
|
||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
||||
const mgr = new CustomModuleManager();
|
||||
|
||||
const https1 = mgr.validateGitHubUrl('https://github.com/owner/repo');
|
||||
assert(https1.isValid === true, 'validateGitHubUrl accepts HTTPS URL');
|
||||
assert(https1.owner === 'owner' && https1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from HTTPS');
|
||||
|
||||
const https2 = mgr.validateGitHubUrl('https://github.com/owner/repo.git');
|
||||
assert(https2.isValid === true, 'validateGitHubUrl accepts HTTPS URL with .git');
|
||||
assert(https2.repo === 'repo', 'validateGitHubUrl strips .git suffix');
|
||||
|
||||
const ssh1 = mgr.validateGitHubUrl('git@github.com:owner/repo.git');
|
||||
assert(ssh1.isValid === true, 'validateGitHubUrl accepts SSH URL');
|
||||
assert(ssh1.owner === 'owner' && ssh1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from SSH');
|
||||
|
||||
const bad1 = mgr.validateGitHubUrl('https://gitlab.com/owner/repo');
|
||||
assert(bad1.isValid === false, 'validateGitHubUrl rejects non-GitHub URL');
|
||||
|
||||
const bad2 = mgr.validateGitHubUrl('');
|
||||
assert(bad2.isValid === false, 'validateGitHubUrl rejects empty string');
|
||||
|
||||
const bad3 = mgr.validateGitHubUrl(null);
|
||||
assert(bad3.isValid === false, 'validateGitHubUrl rejects null');
|
||||
|
||||
const bad4 = mgr.validateGitHubUrl('https://github.com/owner');
|
||||
assert(bad4.isValid === false, 'validateGitHubUrl rejects URL without repo');
|
||||
}
|
||||
|
||||
// --- CustomModuleManager._normalizeCustomModule ---
|
||||
{
|
||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
||||
const mgr = new CustomModuleManager();
|
||||
|
||||
const plugin = { name: 'test-plugin', description: 'A test', version: '1.0.0', author: 'tester', source: './src' };
|
||||
const data = { owner: 'Fallback Owner' };
|
||||
const result = mgr._normalizeCustomModule(plugin, 'https://github.com/o/r', data);
|
||||
|
||||
assert(result.code === 'test-plugin', 'normalizeCustomModule sets code from plugin name');
|
||||
assert(result.type === 'custom', 'normalizeCustomModule sets type to custom');
|
||||
assert(result.trustTier === 'unverified', 'normalizeCustomModule sets trustTier to unverified');
|
||||
assert(result.version === '1.0.0', 'normalizeCustomModule preserves version');
|
||||
assert(result.author === 'tester', 'normalizeCustomModule uses plugin author over data.owner');
|
||||
|
||||
const pluginNoAuthor = { name: 'x', description: '', version: null };
|
||||
const result2 = mgr._normalizeCustomModule(pluginNoAuthor, 'https://github.com/o/r', data);
|
||||
assert(result2.author === 'Fallback Owner', 'normalizeCustomModule falls back to data.owner');
|
||||
}
|
||||
|
||||
// --- CommunityModuleManager._normalizeCommunityModule ---
|
||||
{
|
||||
const { CommunityModuleManager } = require('../tools/installer/modules/community-manager');
|
||||
const mgr = new CommunityModuleManager();
|
||||
|
||||
const mod = {
|
||||
name: 'test-mod',
|
||||
display_name: 'Test Module',
|
||||
code: 'tm',
|
||||
description: 'desc',
|
||||
repository: 'https://github.com/o/r',
|
||||
module_definition: 'src/module.yaml',
|
||||
category: 'software-development',
|
||||
subcategory: 'dev-tools',
|
||||
trust_tier: 'bmad-certified',
|
||||
version: '2.0.0',
|
||||
approved_sha: 'abc123',
|
||||
promoted: true,
|
||||
promoted_rank: 1,
|
||||
keywords: ['test', 'module'],
|
||||
};
|
||||
const result = mgr._normalizeCommunityModule(mod);
|
||||
|
||||
assert(result.code === 'tm', 'normalizeCommunityModule sets code');
|
||||
assert(result.displayName === 'Test Module', 'normalizeCommunityModule sets displayName from display_name');
|
||||
assert(result.type === 'community', 'normalizeCommunityModule sets type to community');
|
||||
assert(result.category === 'software-development', 'normalizeCommunityModule preserves category');
|
||||
assert(result.trustTier === 'bmad-certified', 'normalizeCommunityModule maps trust_tier');
|
||||
assert(result.approvedSha === 'abc123', 'normalizeCommunityModule maps approved_sha');
|
||||
assert(result.promoted === true, 'normalizeCommunityModule maps promoted');
|
||||
assert(result.promotedRank === 1, 'normalizeCommunityModule maps promoted_rank');
|
||||
assert(result.builtIn === false, 'normalizeCommunityModule sets builtIn false');
|
||||
}
|
||||
|
||||
// --- CommunityModuleManager.searchByKeyword (with injected cache) ---
|
||||
{
|
||||
const { CommunityModuleManager } = require('../tools/installer/modules/community-manager');
|
||||
const mgr = new CommunityModuleManager();
|
||||
|
||||
// Inject cached index to avoid network call
|
||||
mgr._cachedIndex = {
|
||||
modules: [
|
||||
{ name: 'mod-a', display_name: 'Alpha', code: 'a', description: 'testing tools', category: 'dev', keywords: ['test'] },
|
||||
{ name: 'mod-b', display_name: 'Beta', code: 'b', description: 'design suite', category: 'design', keywords: ['ux'] },
|
||||
{ name: 'mod-c', display_name: 'Gamma', code: 'c', description: 'game engine', category: 'game', keywords: ['unity'] },
|
||||
],
|
||||
};
|
||||
|
||||
const r1 = await mgr.searchByKeyword('test');
|
||||
assert(r1.length === 1 && r1[0].code === 'a', 'searchByKeyword matches keyword');
|
||||
|
||||
const r2 = await mgr.searchByKeyword('design');
|
||||
assert(r2.length === 1 && r2[0].code === 'b', 'searchByKeyword matches description');
|
||||
|
||||
const r3 = await mgr.searchByKeyword('alpha');
|
||||
assert(r3.length === 1 && r3[0].code === 'a', 'searchByKeyword matches display name');
|
||||
|
||||
const r4 = await mgr.searchByKeyword('xyz');
|
||||
assert(r4.length === 0, 'searchByKeyword returns empty for no match');
|
||||
|
||||
const r5 = await mgr.searchByKeyword('UNITY');
|
||||
assert(r5.length === 1 && r5[0].code === 'c', 'searchByKeyword is case-insensitive');
|
||||
}
|
||||
|
||||
// --- CommunityModuleManager.listFeatured (with injected cache) ---
|
||||
{
|
||||
const { CommunityModuleManager } = require('../tools/installer/modules/community-manager');
|
||||
const mgr = new CommunityModuleManager();
|
||||
|
||||
mgr._cachedIndex = {
|
||||
modules: [
|
||||
{ name: 'a', code: 'a', promoted: true, promoted_rank: 3 },
|
||||
{ name: 'b', code: 'b', promoted: false },
|
||||
{ name: 'c', code: 'c', promoted: true, promoted_rank: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
const featured = await mgr.listFeatured();
|
||||
assert(featured.length === 2, 'listFeatured returns only promoted modules');
|
||||
assert(featured[0].code === 'c' && featured[1].code === 'a', 'listFeatured sorts by promoted_rank ascending');
|
||||
}
|
||||
|
||||
// --- CommunityModuleManager.getCategoryList (with injected cache) ---
|
||||
{
|
||||
const { CommunityModuleManager } = require('../tools/installer/modules/community-manager');
|
||||
const mgr = new CommunityModuleManager();
|
||||
|
||||
mgr._cachedIndex = {
|
||||
modules: [
|
||||
{ name: 'a', code: 'a', category: 'software-development' },
|
||||
{ name: 'b', code: 'b', category: 'design-and-creative' },
|
||||
{ name: 'c', code: 'c', category: 'software-development' },
|
||||
],
|
||||
};
|
||||
mgr._cachedCategories = {
|
||||
categories: {
|
||||
'software-development': { name: 'Software Development' },
|
||||
'design-and-creative': { name: 'Design & Creative' },
|
||||
},
|
||||
};
|
||||
|
||||
const cats = await mgr.getCategoryList();
|
||||
assert(cats.length === 2, 'getCategoryList returns categories with modules');
|
||||
const swDev = cats.find((c) => c.slug === 'software-development');
|
||||
assert(swDev && swDev.moduleCount === 2, 'getCategoryList counts modules per category');
|
||||
assert(cats[0].name === 'Design & Creative', 'getCategoryList sorts alphabetically');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* User input comes from either UI answers or headless CLI flags.
|
||||
*/
|
||||
class Config {
|
||||
constructor({ directory, modules, ides, skipPrompts, verbose, actionType, coreConfig, moduleConfigs, quickUpdate, customModulesMeta }) {
|
||||
constructor({ directory, modules, ides, skipPrompts, verbose, actionType, coreConfig, moduleConfigs, quickUpdate }) {
|
||||
this.directory = directory;
|
||||
this.modules = Object.freeze([...modules]);
|
||||
this.ides = Object.freeze([...ides]);
|
||||
|
|
@ -13,7 +13,6 @@ class Config {
|
|||
this.coreConfig = coreConfig;
|
||||
this.moduleConfigs = moduleConfigs;
|
||||
this._quickUpdate = quickUpdate;
|
||||
this.customModulesMeta = Object.freeze(customModulesMeta || []);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +37,6 @@ class Config {
|
|||
coreConfig: userInput.coreConfig || {},
|
||||
moduleConfigs: userInput.moduleConfigs || null,
|
||||
quickUpdate: userInput._quickUpdate || false,
|
||||
customModulesMeta: userInput.customModulesMeta || [],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -786,10 +786,9 @@ class UI {
|
|||
}
|
||||
|
||||
if (selectedCodes.size > 0) {
|
||||
const communityMgrForDisplay = new (require('./modules/community-manager').CommunityModuleManager)();
|
||||
const moduleLines = [];
|
||||
for (const code of selectedCodes) {
|
||||
const mod = await communityMgrForDisplay.getModuleByCode(code);
|
||||
const mod = await communityMgr.getModuleByCode(code);
|
||||
moduleLines.push(` \u2022 ${mod?.displayName || code}`);
|
||||
}
|
||||
await prompts.log.message('Selected community modules:\n' + moduleLines.join('\n'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue