import { describe, it, expect, vi, beforeEach } from 'vitest'; import { RegistryClient } from '../../src/registry/client.js'; import type { RegistryServer } from '../../src/registry/types.js'; function makeServer(name: string, source: 'official' | 'glama' | 'smithery'): RegistryServer { return { name, description: `${name} description`, packages: { npm: `@test/${name}` }, envTemplate: [], transport: 'stdio', popularityScore: 50, verified: source === 'smithery', sourceRegistry: source, }; } // Mock fetch globally const mockFetch = vi.fn(); beforeEach(() => { vi.stubGlobal('fetch', mockFetch); mockFetch.mockReset(); }); function mockRegistryResponse(source: string, 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: s.packages.npm !== undefined ? [{ registryType: 'npm', identifier: s.packages.npm, transport: { type: 'stdio' }, environmentVariables: [], }] : [], remotes: [], }, })), metadata: { nextCursor: null, count: 1 }, }), }); } 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: s.packages.npm ?? '', })), 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: s.verified, useCount: s.popularityScore, remote: false, })), pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 1 }, }), }); } return Promise.reject(new Error(`Unexpected URL: ${url}`)); }); } describe('RegistryClient', () => { it('queries all enabled registries', async () => { const testServers = [ makeServer('slack-official', 'official'), makeServer('slack-glama', 'glama'), makeServer('slack-smithery', 'smithery'), ]; mockRegistryResponse('all', testServers); const client = new RegistryClient(); const results = await client.search({ query: 'slack' }); expect(results.length).toBeGreaterThan(0); expect(mockFetch).toHaveBeenCalledTimes(3); }); it('uses cached results on second call', async () => { mockRegistryResponse('all', [makeServer('slack', 'official')]); const client = new RegistryClient(); await client.search({ query: 'slack' }); mockFetch.mockClear(); await client.search({ query: 'slack' }); expect(mockFetch).not.toHaveBeenCalled(); }); it('filters by registry when specified', async () => { mockRegistryResponse('all', [makeServer('test', 'official')]); const client = new RegistryClient(); await client.search({ query: 'test', registries: ['official'] }); expect(mockFetch).toHaveBeenCalledTimes(1); const calledUrl = mockFetch.mock.calls[0]?.[0] as string; expect(calledUrl).toContain('modelcontextprotocol.io'); }); it('handles partial failures gracefully', async () => { mockFetch.mockImplementation((url: string) => { if (url.includes('glama.ai')) { return Promise.reject(new Error('Network error')); } if (url.includes('registry.smithery.ai')) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ servers: [{ qualifiedName: 'slack', displayName: 'Slack', description: 'Slack', verified: true, useCount: 100, remote: false, }], pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 1 }, }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve({ servers: [], metadata: { nextCursor: null }, }), }); }); const client = new RegistryClient(); const results = await client.search({ query: 'slack' }); // Should still return results from successful sources expect(results.length).toBeGreaterThan(0); }); it('records error counts on failures', async () => { mockFetch.mockImplementation((url: string) => { if (url.includes('glama.ai')) { return Promise.reject(new Error('fail')); } // Return empty for others if (url.includes('modelcontextprotocol')) { 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 errors = client.getErrorCounts(); expect(errors.get('glama')).toBe(1); }); it('filters by verified when specified', async () => { mockFetch.mockImplementation((url: string) => { if (url.includes('registry.smithery.ai')) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ servers: [ { qualifiedName: 'verified', displayName: 'Verified', description: '', verified: true, useCount: 100, remote: false }, { qualifiedName: 'unverified', displayName: 'Unverified', description: '', verified: false, useCount: 50, remote: false }, ], pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 2 }, }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve({ servers: [], metadata: { nextCursor: null } }), }); }); // Mock glama too mockFetch.mockImplementation((url: string) => { if (url.includes('registry.smithery.ai')) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ servers: [ { qualifiedName: 'verified', displayName: 'Verified', description: '', verified: true, useCount: 100, remote: false }, { qualifiedName: 'unverified', displayName: 'Unverified', description: '', verified: false, useCount: 50, remote: false }, ], pagination: { currentPage: 1, pageSize: 20, totalPages: 1, totalCount: 2 }, }), }); } if (url.includes('glama.ai')) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ servers: [], pageInfo: { hasNextPage: false, hasPreviousPage: false } }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve({ servers: [], metadata: { nextCursor: null } }), }); }); const client = new RegistryClient(); const results = await client.search({ query: 'test', verified: true }); for (const r of results) { expect(r.verified).toBe(true); } }); it('respects limit option', async () => { mockRegistryResponse('all', [ makeServer('a', 'official'), makeServer('b', 'glama'), makeServer('c', 'smithery'), ]); const client = new RegistryClient(); const results = await client.search({ query: 'test', limit: 1 }); expect(results.length).toBeLessThanOrEqual(1); }); it('records latency metrics', async () => { mockRegistryResponse('all', [makeServer('test', 'official')]); const client = new RegistryClient(); await client.search({ query: 'test' }); const latencies = client.getQueryLatencies(); expect(latencies.size).toBeGreaterThan(0); }); it('clearCache empties cache', async () => { mockRegistryResponse('all', [makeServer('test', 'official')]); const client = new RegistryClient(); await client.search({ query: 'test' }); client.clearCache(); mockFetch.mockClear(); mockRegistryResponse('all', [makeServer('test', 'official')]); await client.search({ query: 'test' }); // Should have fetched again after cache clear expect(mockFetch).toHaveBeenCalled(); }); });