refactor(test): fold registry tests into test-installation-components

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.
This commit is contained in:
Alex Verkhovsky 2026-04-18 08:46:31 -07:00
parent 104dd9fee4
commit 8c3301fc49
3 changed files with 108 additions and 202 deletions

View File

@ -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"
},

View File

@ -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
// ============================================================

View File

@ -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);
});