feat: implement MCP registry client with multi-source search
Add registry client that queries Official, Glama, and Smithery MCP registries with caching, request deduplication, retry logic, and result ranking/dedup. Includes 53 tests covering all components. Also fix null priority values in cancelled tasks (19-21) that broke Task Master, and add new tasks 25-27 for registry completion and CLI discover/install commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
90
src/cli/tests/registry/cache.test.ts
Normal file
90
src/cli/tests/registry/cache.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { RegistryCache } from '../../src/registry/cache.js';
|
||||
import type { RegistryServer, SearchOptions } from '../../src/registry/types.js';
|
||||
|
||||
function makeServer(name: string): RegistryServer {
|
||||
return {
|
||||
name,
|
||||
description: `${name} server`,
|
||||
packages: {},
|
||||
envTemplate: [],
|
||||
transport: 'stdio',
|
||||
popularityScore: 0,
|
||||
verified: false,
|
||||
sourceRegistry: 'official',
|
||||
};
|
||||
}
|
||||
|
||||
const defaultOptions: SearchOptions = { query: 'test' };
|
||||
|
||||
describe('RegistryCache', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns null for cache miss', () => {
|
||||
const cache = new RegistryCache();
|
||||
expect(cache.get('unknown', defaultOptions)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns data for cache hit within TTL', () => {
|
||||
const cache = new RegistryCache();
|
||||
const data = [makeServer('test')];
|
||||
cache.set('test', defaultOptions, data);
|
||||
expect(cache.get('test', defaultOptions)).toEqual(data);
|
||||
});
|
||||
|
||||
it('returns null after TTL expires', () => {
|
||||
const cache = new RegistryCache(1000); // 1 second TTL
|
||||
cache.set('test', defaultOptions, [makeServer('test')]);
|
||||
|
||||
vi.advanceTimersByTime(1001);
|
||||
expect(cache.get('test', defaultOptions)).toBeNull();
|
||||
});
|
||||
|
||||
it('generates deterministic cache keys', () => {
|
||||
const cache = new RegistryCache();
|
||||
const data = [makeServer('test')];
|
||||
cache.set('query', { query: 'query', limit: 10 }, data);
|
||||
expect(cache.get('query', { query: 'query', limit: 10 })).toEqual(data);
|
||||
});
|
||||
|
||||
it('generates different keys for different queries', () => {
|
||||
const cache = new RegistryCache();
|
||||
cache.set('a', { query: 'a' }, [makeServer('a')]);
|
||||
expect(cache.get('b', { query: 'b' })).toBeNull();
|
||||
});
|
||||
|
||||
it('tracks hits and misses correctly', () => {
|
||||
const cache = new RegistryCache();
|
||||
cache.set('test', defaultOptions, [makeServer('test')]);
|
||||
|
||||
cache.get('test', defaultOptions); // hit
|
||||
cache.get('test', defaultOptions); // hit
|
||||
cache.get('miss', { query: 'miss' }); // miss
|
||||
|
||||
const ratio = cache.getHitRatio();
|
||||
expect(ratio.hits).toBe(2);
|
||||
expect(ratio.misses).toBe(1);
|
||||
expect(ratio.ratio).toBeCloseTo(2 / 3);
|
||||
});
|
||||
|
||||
it('returns 0 ratio when no accesses', () => {
|
||||
const cache = new RegistryCache();
|
||||
expect(cache.getHitRatio().ratio).toBe(0);
|
||||
});
|
||||
|
||||
it('clears all entries and resets metrics', () => {
|
||||
const cache = new RegistryCache();
|
||||
cache.set('a', { query: 'a' }, [makeServer('a')]);
|
||||
cache.get('a', { query: 'a' }); // hit
|
||||
cache.clear();
|
||||
|
||||
expect(cache.get('a', { query: 'a' })).toBeNull();
|
||||
expect(cache.size).toBe(0);
|
||||
expect(cache.getHitRatio().hits).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user