diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 82094165a..da3e287a8 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -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 // ============================================================ diff --git a/tools/installer/core/config.js b/tools/installer/core/config.js index 03ffbe279..c844e2d00 100644 --- a/tools/installer/core/config.js +++ b/tools/installer/core/config.js @@ -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 || [], }); } diff --git a/tools/installer/ui.js b/tools/installer/ui.js index e94d1c6ab..d819861a0 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -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'));