From 8c3301fc49223948466905f4a3bf5facdb74c209 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 18 Apr 2026 08:46:31 -0700 Subject: [PATCH] refactor(test): fold registry tests into test-installation-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No reason for RegistryClient tests to be a separate runner — the same file already tests the registry consumers in Suite 33. Drop test:registry from package.json scripts and quality gate. --- package.json | 5 +- test/test-installation-components.js | 106 ++++++++++++++ test/test-registry-client.js | 199 --------------------------- 3 files changed, 108 insertions(+), 202 deletions(-) delete mode 100644 test/test-registry-client.js diff --git a/package.json b/package.json index 9bf777f3c..a26398fdf 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,11 @@ "lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix", "lint:md": "markdownlint-cli2 \"**/*.md\"", "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0", - "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:registry && npm run validate:refs && npm run validate:skills", + "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs && npm run validate:skills", "rebundle": "node tools/installer/bundlers/bundle-web.js rebundle", - "test": "npm run test:refs && npm run test:install && npm run test:registry && npm run lint && npm run lint:md && npm run format:check", + "test": "npm run test:refs && npm run test:install && npm run lint && npm run lint:md && npm run format:check", "test:install": "node test/test-installation-components.js", "test:refs": "node test/test-file-refs-csv.js", - "test:registry": "node test/test-registry-client.js", "validate:refs": "node tools/validate-file-refs.js --strict", "validate:skills": "node tools/validate-skills.js --strict" }, diff --git a/test/test-installation-components.js b/test/test-installation-components.js index f1c1be486..c5d3540b3 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1926,6 +1926,112 @@ async function runTests() { console.log(''); + // ============================================================ + // Test Suite 34: RegistryClient GitHub API Cascade + // ============================================================ + console.log(`${colors.yellow}Test Suite 34: RegistryClient GitHub API Cascade${colors.reset}\n`); + + { + const { RegistryClient } = require('../tools/installer/modules/registry-client'); + + // Build a RegistryClient with stubbed fetch paths so we can assert on cascade behavior + // without making real network calls. + function createStubbedClient({ apiResult, rawResult }) { + const client = new RegistryClient(); + const calls = []; + + // Stub _fetchWithHeaders (GitHub API path) + client._fetchWithHeaders = async (url) => { + calls.push(`api:${url}`); + if (apiResult instanceof Error) throw apiResult; + return apiResult; + }; + + // Stub fetch (raw CDN path) — only intercept raw.githubusercontent.com calls + const originalFetch = client.fetch.bind(client); + client.fetch = async (url, timeout) => { + if (url.includes('raw.githubusercontent.com')) { + calls.push(`raw:${url}`); + if (rawResult instanceof Error) throw rawResult; + return rawResult; + } + return originalFetch(url, timeout); + }; + + return { client, calls }; + } + + // --- API success skips raw CDN --- + { + const { client, calls } = createStubbedClient({ apiResult: 'api-content', rawResult: 'raw-content' }); + const result = await client.fetchGitHubFile('owner', 'repo', 'path/file.txt', 'main'); + + assert(result === 'api-content', 'RegistryClient API success returns API content'); + assert(calls.length === 1, 'RegistryClient API success makes exactly one call'); + assert(calls[0].startsWith('api:'), 'RegistryClient API success calls API endpoint'); + } + + // --- API failure falls back to raw CDN --- + { + const { client, calls } = createStubbedClient({ apiResult: new Error('HTTP 403'), rawResult: 'raw-content' }); + const result = await client.fetchGitHubFile('owner', 'repo', 'path/file.txt', 'main'); + + assert(result === 'raw-content', 'RegistryClient API failure returns raw CDN content'); + assert(calls.length === 2, 'RegistryClient API failure makes two calls'); + assert(calls[0].startsWith('api:'), 'RegistryClient first call is to API'); + assert(calls[1].startsWith('raw:'), 'RegistryClient second call is to raw CDN'); + } + + // --- Both endpoints failing throws --- + { + const { client } = createStubbedClient({ apiResult: new Error('HTTP 403'), rawResult: new Error('HTTP 404') }); + let threw = false; + try { + await client.fetchGitHubFile('owner', 'repo', 'path/file.txt', 'main'); + } catch { + threw = true; + } + assert(threw, 'RegistryClient both endpoints failing throws an error'); + } + + // --- API URL construction --- + { + const { client, calls } = createStubbedClient({ apiResult: 'content', rawResult: 'content' }); + await client.fetchGitHubFile('bmad-code-org', 'bmad-plugins-marketplace', 'registry/official.yaml', 'main'); + + const apiCall = calls[0]; + assert( + apiCall.includes('api.github.com/repos/bmad-code-org/bmad-plugins-marketplace/contents/registry/official.yaml'), + 'RegistryClient API URL contains correct path', + ); + assert(apiCall.includes('ref=main'), 'RegistryClient API URL contains ref parameter'); + } + + // --- Raw CDN URL construction --- + { + const { client, calls } = createStubbedClient({ apiResult: new Error('fail'), rawResult: 'content' }); + await client.fetchGitHubFile('bmad-code-org', 'bmad-plugins-marketplace', 'registry/official.yaml', 'main'); + + const rawCall = calls[1]; + assert( + rawCall.includes('raw.githubusercontent.com/bmad-code-org/bmad-plugins-marketplace/main/registry/official.yaml'), + 'RegistryClient raw CDN URL contains correct path', + ); + } + + // --- fetchGitHubYaml parses YAML --- + { + const yamlContent = 'modules:\n - name: test\n description: A test module\n'; + const { client } = createStubbedClient({ apiResult: yamlContent, rawResult: yamlContent }); + const result = await client.fetchGitHubYaml('owner', 'repo', 'file.yaml', 'main'); + + assert(Array.isArray(result.modules), 'fetchGitHubYaml parses YAML correctly'); + assert(result.modules[0].name === 'test', 'fetchGitHubYaml preserves YAML values'); + } + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/test/test-registry-client.js b/test/test-registry-client.js deleted file mode 100644 index ca310c61b..000000000 --- a/test/test-registry-client.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * RegistryClient Tests - * - * Tests the GitHub API cascade logic in RegistryClient: - * - fetchGitHubFile tries API first, falls back to raw CDN - * - fetchGitHubYaml/Json parse correctly - * - Error propagation when both endpoints fail - * - * Uses monkey-patching to intercept HTTP calls without external dependencies. - * Usage: node test/test-registry-client.js - */ - -const { RegistryClient } = require('../tools/installer/modules/registry-client'); - -// ANSI colors -const colors = { - reset: '\u001B[0m', - green: '\u001B[32m', - red: '\u001B[31m', - cyan: '\u001B[36m', - dim: '\u001B[2m', -}; - -let passed = 0; -let failed = 0; - -function assert(condition, testName, errorMessage = '') { - if (condition) { - console.log(`${colors.green}✓${colors.reset} ${testName}`); - passed++; - } else { - console.log(`${colors.red}✗${colors.reset} ${testName}`); - if (errorMessage) { - console.log(` ${colors.dim}${errorMessage}${colors.reset}`); - } - failed++; - } -} - -// ─── Test Helpers ────────────────────────────────────────────────────────── - -/** - * Create a RegistryClient with stubbed fetch methods for testing cascade logic. - * @param {Object} opts - * @param {string|Error} opts.apiResult - Return value or Error to throw for API call - * @param {string|Error} opts.rawResult - Return value or Error to throw for raw CDN call - * @returns {{ client: RegistryClient, calls: string[] }} - */ -function createStubbedClient({ apiResult, rawResult }) { - const client = new RegistryClient(); - const calls = []; - - // Stub _fetchWithHeaders (GitHub API path) - client._fetchWithHeaders = async (url) => { - calls.push(`api:${url}`); - if (apiResult instanceof Error) throw apiResult; - return apiResult; - }; - - // Stub fetch (raw CDN path) - const originalFetch = client.fetch.bind(client); - client.fetch = async (url, timeout) => { - // Only intercept raw.githubusercontent.com calls - if (url.includes('raw.githubusercontent.com')) { - calls.push(`raw:${url}`); - if (rawResult instanceof Error) throw rawResult; - return rawResult; - } - return originalFetch(url, timeout); - }; - - return { client, calls }; -} - -// ─── Tests ───────────────────────────────────────────────────────────────── - -async function testApiSuccessSkipsRaw() { - const { client, calls } = createStubbedClient({ - apiResult: 'api-content', - rawResult: 'raw-content', - }); - - const result = await client.fetchGitHubFile('owner', 'repo', 'path/file.txt', 'main'); - - assert(result === 'api-content', 'API success returns API content'); - assert(calls.length === 1, 'API success makes exactly one call', `calls: ${calls.join(', ')}`); - assert(calls[0].startsWith('api:'), 'API success calls API endpoint', `got: ${calls[0]}`); -} - -async function testApiFailureFallsToRaw() { - const { client, calls } = createStubbedClient({ - apiResult: new Error('HTTP 403'), - rawResult: 'raw-content', - }); - - const result = await client.fetchGitHubFile('owner', 'repo', 'path/file.txt', 'main'); - - assert(result === 'raw-content', 'API failure returns raw CDN content'); - assert(calls.length === 2, 'API failure makes two calls', `calls: ${calls.join(', ')}`); - assert(calls[0].startsWith('api:'), 'First call is to API'); - assert(calls[1].startsWith('raw:'), 'Second call is to raw CDN'); -} - -async function testBothFailThrows() { - const { client } = createStubbedClient({ - apiResult: new Error('HTTP 403'), - rawResult: new Error('HTTP 404'), - }); - - let threw = false; - try { - await client.fetchGitHubFile('owner', 'repo', 'path/file.txt', 'main'); - } catch { - threw = true; - } - - assert(threw, 'Both endpoints failing throws an error'); -} - -async function testUrlConstruction() { - const { client, calls } = createStubbedClient({ - apiResult: 'content', - rawResult: 'content', - }); - - await client.fetchGitHubFile('bmad-code-org', 'bmad-plugins-marketplace', 'registry/official.yaml', 'main'); - - const apiCall = calls[0]; - assert( - apiCall.includes('api.github.com/repos/bmad-code-org/bmad-plugins-marketplace/contents/registry/official.yaml'), - 'API URL contains correct path', - `got: ${apiCall}`, - ); - assert(apiCall.includes('ref=main'), 'API URL contains ref parameter', `got: ${apiCall}`); -} - -async function testRawUrlConstruction() { - const { client, calls } = createStubbedClient({ - apiResult: new Error('fail'), - rawResult: 'content', - }); - - await client.fetchGitHubFile('bmad-code-org', 'bmad-plugins-marketplace', 'registry/official.yaml', 'main'); - - const rawCall = calls[1]; - assert( - rawCall.includes('raw.githubusercontent.com/bmad-code-org/bmad-plugins-marketplace/main/registry/official.yaml'), - 'Raw URL contains correct path', - `got: ${rawCall}`, - ); -} - -async function testFetchGitHubYamlParsesCorrectly() { - const yamlContent = 'modules:\n - name: test\n description: A test module\n'; - const { client } = createStubbedClient({ - apiResult: yamlContent, - rawResult: yamlContent, - }); - - const result = await client.fetchGitHubYaml('owner', 'repo', 'file.yaml', 'main'); - - assert(Array.isArray(result.modules), 'fetchGitHubYaml parses YAML correctly'); - assert(result.modules[0].name === 'test', 'fetchGitHubYaml preserves YAML values'); -} - -// ─── Runner ──────────────────────────────────────────────────────────────── - -async function runTests() { - console.log(`\n${colors.cyan}========================================`); - console.log(' RegistryClient Tests'); - console.log(`========================================${colors.reset}\n`); - - await testApiSuccessSkipsRaw(); - await testApiFailureFallsToRaw(); - await testBothFailThrows(); - await testUrlConstruction(); - await testRawUrlConstruction(); - await testFetchGitHubYamlParsesCorrectly(); - - console.log(`\n${colors.cyan}========================================`); - console.log('Test Results:'); - console.log(` Passed: ${colors.green}${passed}${colors.reset}`); - console.log(` Failed: ${colors.red}${failed}${colors.reset}`); - console.log(`========================================${colors.reset}\n`); - - if (failed === 0) { - console.log(`${colors.green}✨ All registry client tests passed!${colors.reset}\n`); - process.exit(0); - } else { - console.log(`${colors.red}❌ Some registry client tests failed${colors.reset}\n`); - process.exit(1); - } -} - -runTests().catch((error) => { - console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message); - console.error(error.stack); - process.exit(1); -});