feat: add HTTP proxy, custom CA, metrics exposure, and category filtering
- Add createHttpAgent() for proxy/CA support via undici - Thread dispatcher through all registry sources - Add collectMetrics() for SRE metrics exposure - Add caPath to RegistryClientConfig - Add category field to RegistryServer with Glama extraction - Add category filtering in client search - Add pr.sh for Gitea PR creation 63 tests passing (13 new). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
164
src/cli/tests/registry/metrics.test.ts
Normal file
164
src/cli/tests/registry/metrics.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { collectMetrics, type RegistryMetrics } from '../../src/registry/metrics.js';
|
||||
import { RegistryClient } from '../../src/registry/client.js';
|
||||
import type { RegistryServer } from '../../src/registry/types.js';
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
|
||||
function makeServer(name: string, source: 'official' | 'glama' | 'smithery'): RegistryServer {
|
||||
return {
|
||||
name,
|
||||
description: `${name} description`,
|
||||
packages: { npm: `@test/${name}` },
|
||||
envTemplate: [],
|
||||
transport: 'stdio',
|
||||
popularityScore: 50,
|
||||
verified: false,
|
||||
sourceRegistry: source,
|
||||
};
|
||||
}
|
||||
|
||||
function mockAllRegistries(servers: RegistryServer[]): void {
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
if (url.includes('registry.modelcontextprotocol.io')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
servers: servers
|
||||
.filter((s) => s.sourceRegistry === 'official')
|
||||
.map((s) => ({
|
||||
server: {
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
packages: [{ registryType: 'npm', identifier: s.packages.npm, transport: { type: 'stdio' }, environmentVariables: [] }],
|
||||
remotes: [],
|
||||
},
|
||||
})),
|
||||
metadata: { nextCursor: null },
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (url.includes('glama.ai')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
servers: servers
|
||||
.filter((s) => s.sourceRegistry === 'glama')
|
||||
.map((s) => ({ id: s.name, name: s.name, description: s.description, attributes: [], slug: '' })),
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (url.includes('registry.smithery.ai')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
servers: servers
|
||||
.filter((s) => s.sourceRegistry === 'smithery')
|
||||
.map((s) => ({ qualifiedName: s.name, displayName: s.name, description: s.description, verified: false, useCount: 0, remote: false })),
|
||||
pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 1 },
|
||||
}),
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(`Unexpected URL: ${url}`));
|
||||
});
|
||||
}
|
||||
|
||||
describe('collectMetrics', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
mockFetch.mockReset();
|
||||
});
|
||||
|
||||
it('returns correct structure with all required fields', async () => {
|
||||
mockAllRegistries([makeServer('test', 'official')]);
|
||||
const client = new RegistryClient();
|
||||
await client.search({ query: 'test' });
|
||||
|
||||
const metrics = collectMetrics(client);
|
||||
|
||||
expect(metrics).toHaveProperty('queryLatencyMs');
|
||||
expect(metrics).toHaveProperty('cacheHitRatio');
|
||||
expect(metrics).toHaveProperty('cacheHits');
|
||||
expect(metrics).toHaveProperty('cacheMisses');
|
||||
expect(metrics).toHaveProperty('errorCounts');
|
||||
expect(Array.isArray(metrics.queryLatencyMs)).toBe(true);
|
||||
expect(Array.isArray(metrics.errorCounts)).toBe(true);
|
||||
expect(typeof metrics.cacheHitRatio).toBe('number');
|
||||
});
|
||||
|
||||
it('captures latencies per source', async () => {
|
||||
mockAllRegistries([
|
||||
makeServer('test', 'official'),
|
||||
makeServer('test', 'glama'),
|
||||
makeServer('test', 'smithery'),
|
||||
]);
|
||||
const client = new RegistryClient();
|
||||
await client.search({ query: 'test' });
|
||||
|
||||
const metrics = collectMetrics(client);
|
||||
|
||||
expect(metrics.queryLatencyMs.length).toBeGreaterThan(0);
|
||||
for (const entry of metrics.queryLatencyMs) {
|
||||
expect(entry).toHaveProperty('source');
|
||||
expect(entry).toHaveProperty('latencies');
|
||||
expect(Array.isArray(entry.latencies)).toBe(true);
|
||||
expect(entry.latencies.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('captures cache hit ratio', async () => {
|
||||
mockAllRegistries([makeServer('test', 'official')]);
|
||||
const client = new RegistryClient();
|
||||
|
||||
// First call: miss
|
||||
await client.search({ query: 'test' });
|
||||
// Second call: hit
|
||||
await client.search({ query: 'test' });
|
||||
|
||||
const metrics = collectMetrics(client);
|
||||
expect(metrics.cacheHits).toBe(1);
|
||||
expect(metrics.cacheMisses).toBe(1);
|
||||
expect(metrics.cacheHitRatio).toBe(0.5);
|
||||
});
|
||||
|
||||
it('captures error counts per source', async () => {
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
if (url.includes('glama.ai')) {
|
||||
return Promise.reject(new Error('fail'));
|
||||
}
|
||||
if (url.includes('registry.modelcontextprotocol.io')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ servers: [], metadata: { nextCursor: null } }),
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
servers: [],
|
||||
pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 0 },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const client = new RegistryClient();
|
||||
await client.search({ query: 'test' });
|
||||
|
||||
const metrics = collectMetrics(client);
|
||||
const glamaError = metrics.errorCounts.find((e) => e.source === 'glama');
|
||||
expect(glamaError).toBeDefined();
|
||||
expect(glamaError!.count).toBe(1);
|
||||
});
|
||||
|
||||
it('works with empty metrics (no queries made)', () => {
|
||||
const client = new RegistryClient();
|
||||
const metrics = collectMetrics(client);
|
||||
|
||||
expect(metrics.queryLatencyMs).toEqual([]);
|
||||
expect(metrics.errorCounts).toEqual([]);
|
||||
expect(metrics.cacheHits).toBe(0);
|
||||
expect(metrics.cacheMisses).toBe(0);
|
||||
expect(metrics.cacheHitRatio).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user