165 lines
5.5 KiB
TypeScript
165 lines
5.5 KiB
TypeScript
|
|
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);
|
||
|
|
});
|
||
|
|
});
|