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:
91
src/cli/tests/registry/ranking.test.ts
Normal file
91
src/cli/tests/registry/ranking.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { rankResults } from '../../src/registry/ranking.js';
|
||||
import type { RegistryServer } from '../../src/registry/types.js';
|
||||
|
||||
function makeServer(overrides: Partial<RegistryServer> = {}): RegistryServer {
|
||||
return {
|
||||
name: 'test-server',
|
||||
description: 'A test server',
|
||||
packages: {},
|
||||
envTemplate: [],
|
||||
transport: 'stdio',
|
||||
popularityScore: 0,
|
||||
verified: false,
|
||||
sourceRegistry: 'official',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('rankResults', () => {
|
||||
it('puts exact name match first', () => {
|
||||
const servers = [
|
||||
makeServer({ name: 'slack-extended-tools' }),
|
||||
makeServer({ name: 'slack' }),
|
||||
makeServer({ name: 'my-slack-bot' }),
|
||||
];
|
||||
const ranked = rankResults(servers, 'slack');
|
||||
expect(ranked[0]?.name).toBe('slack');
|
||||
});
|
||||
|
||||
it('ranks verified servers higher than unverified', () => {
|
||||
const servers = [
|
||||
makeServer({ name: 'server-a', verified: false }),
|
||||
makeServer({ name: 'server-b', verified: true }),
|
||||
];
|
||||
const ranked = rankResults(servers, 'server');
|
||||
expect(ranked[0]?.name).toBe('server-b');
|
||||
});
|
||||
|
||||
it('ranks popular servers higher', () => {
|
||||
const servers = [
|
||||
makeServer({ name: 'unpopular', popularityScore: 1 }),
|
||||
makeServer({ name: 'popular', popularityScore: 10000 }),
|
||||
];
|
||||
const ranked = rankResults(servers, 'test');
|
||||
expect(ranked[0]?.name).toBe('popular');
|
||||
});
|
||||
|
||||
it('considers recency', () => {
|
||||
const recent = new Date();
|
||||
const old = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
|
||||
const servers = [
|
||||
makeServer({ name: 'old-server', lastUpdated: old }),
|
||||
makeServer({ name: 'new-server', lastUpdated: recent }),
|
||||
];
|
||||
const ranked = rankResults(servers, 'test');
|
||||
expect(ranked[0]?.name).toBe('new-server');
|
||||
});
|
||||
|
||||
it('handles missing lastUpdated gracefully', () => {
|
||||
const servers = [
|
||||
makeServer({ name: 'no-date' }),
|
||||
makeServer({ name: 'has-date', lastUpdated: new Date() }),
|
||||
];
|
||||
// Should not throw
|
||||
const ranked = rankResults(servers, 'test');
|
||||
expect(ranked).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('produces stable ordering for identical scores', () => {
|
||||
const servers = Array.from({ length: 10 }, (_, i) =>
|
||||
makeServer({ name: `server-${String(i)}` }),
|
||||
);
|
||||
const ranked1 = rankResults(servers, 'test');
|
||||
const ranked2 = rankResults(servers, 'test');
|
||||
expect(ranked1.map((s) => s.name)).toEqual(ranked2.map((s) => s.name));
|
||||
});
|
||||
|
||||
it('returns empty array for empty input', () => {
|
||||
expect(rankResults([], 'test')).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not mutate original array', () => {
|
||||
const servers = [
|
||||
makeServer({ name: 'b' }),
|
||||
makeServer({ name: 'a' }),
|
||||
];
|
||||
const original = [...servers];
|
||||
rankResults(servers, 'test');
|
||||
expect(servers.map((s) => s.name)).toEqual(original.map((s) => s.name));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user