agent-ecosystem/test/main/services/extensions/OfficialMcpRegistryService.test.ts
iliya 126f8e2865 feat: add Extension Store with plugin catalog and MCP registry
Full Extension Store implementation (Phases 0-6):
- Plugin marketplace catalog with ETag caching and search/filter/sort
- MCP server registry with Official + Glama aggregation
- Install/uninstall flows for both plugins and MCP servers via CLI
- Per-tab UI state, skeleton loading, dashed empty states, card polish
- Input validation and security hardening (scope allowlists, env/header
  key regex, projectPath validation, HTTP body size limits)
- 8 test suites covering catalog, install, aggregation, normalizers
2026-03-08 01:00:18 +02:00

164 lines
5.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { OfficialMcpRegistryService } from '@main/services/extensions/catalog/OfficialMcpRegistryService';
import registryListFixture from '../../../fixtures/extensions/official-mcp-registry-list.json';
import registrySearchFixture from '../../../fixtures/extensions/official-mcp-registry-search.json';
// ── Mock HTTP ──────────────────────────────────────────────────────────────
vi.mock('node:https', () => ({
default: { get: vi.fn() },
get: vi.fn(),
}));
vi.mock('node:http', () => ({
default: { get: vi.fn() },
get: vi.fn(),
}));
import https from 'node:https';
import type { IncomingMessage } from 'node:http';
function mockHttpsGet(statusCode: number, body: string): void {
const mockGet = https.get as ReturnType<typeof vi.fn>;
mockGet.mockImplementation((_url: string, callback: (res: IncomingMessage) => void) => {
const res = {
statusCode,
headers: {},
on: vi.fn((event: string, handler: (data?: Buffer) => void) => {
if (event === 'data') handler(Buffer.from(body));
if (event === 'end') handler();
return res;
}),
destroy: vi.fn(),
};
callback(res as unknown as IncomingMessage);
return {
setTimeout: vi.fn(),
on: vi.fn(),
destroy: vi.fn(),
};
});
}
describe('OfficialMcpRegistryService', () => {
let service: OfficialMcpRegistryService;
beforeEach(() => {
service = new OfficialMcpRegistryService();
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('search', () => {
it('returns normalized MCP servers from search results', async () => {
mockHttpsGet(200, JSON.stringify(registrySearchFixture));
const results = await service.search('github');
expect(results.length).toBeGreaterThan(0);
expect(results[0]).toHaveProperty('id');
expect(results[0]).toHaveProperty('name');
expect(results[0]).toHaveProperty('source', 'official');
});
it('returns empty array on network error', async () => {
const mockGet = https.get as ReturnType<typeof vi.fn>;
mockGet.mockImplementation((_url: string, _callback: unknown) => {
return {
setTimeout: vi.fn(),
on: vi.fn((event: string, handler: (err: Error) => void) => {
if (event === 'error') handler(new Error('Network error'));
}),
destroy: vi.fn(),
};
});
const results = await service.search('test');
expect(results).toEqual([]);
});
});
describe('browse', () => {
it('returns servers with pagination cursor', async () => {
mockHttpsGet(200, JSON.stringify(registryListFixture));
const result = await service.browse();
expect(result.servers.length).toBeGreaterThan(0);
expect(result.nextCursor).toBeDefined();
});
it('filters non-latest versions', async () => {
mockHttpsGet(200, JSON.stringify(registryListFixture));
const result = await service.browse();
// Registry fixture has AdAdvisor with isLatest: false and isLatest: true
// Only the latest should appear
const adAdvisor = result.servers.filter((s) =>
s.id === 'ai.adadvisor/mcp-server',
);
expect(adAdvisor.length).toBeLessThanOrEqual(1);
});
});
describe('normalizeEntry', () => {
it('derives stdio install spec from npm packages', async () => {
mockHttpsGet(200, JSON.stringify(registryListFixture));
const result = await service.browse();
const agentTrust = result.servers.find((s) => s.id === 'ai.agenttrust/mcp-server');
expect(agentTrust).toBeDefined();
expect(agentTrust!.installSpec).toEqual({
type: 'stdio',
npmPackage: '@agenttrust/mcp-server',
npmVersion: '1.1.1',
});
});
it('derives HTTP install spec from remotes', async () => {
mockHttpsGet(200, JSON.stringify(registryListFixture));
const result = await service.browse();
const lona = result.servers.find((s) => s.id === 'agency.lona/trading');
expect(lona).toBeDefined();
expect(lona!.installSpec).toEqual({
type: 'http',
url: 'https://mcp.lona.agency/mcp',
transportType: 'streamable-http',
});
});
it('detects auth-required servers', async () => {
mockHttpsGet(200, JSON.stringify(registryListFixture));
const result = await service.browse();
const adAdvisor = result.servers.find((s) => s.id === 'ai.adadvisor/mcp-server');
expect(adAdvisor?.requiresAuth).toBe(true);
});
it('collects environment variables', async () => {
mockHttpsGet(200, JSON.stringify(registryListFixture));
const result = await service.browse();
const agentTrust = result.servers.find((s) => s.id === 'ai.agenttrust/mcp-server');
expect(agentTrust!.envVars).toEqual([
{
name: 'AGENTTRUST_API_KEY',
isSecret: true,
description: 'Your AgentTrust API key from https://agenttrust.ai',
isRequired: true,
},
]);
});
});
});