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