agent-ecosystem/test/main/services/extensions/McpCatalogAggregator.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

143 lines
4.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { McpCatalogAggregator } from '@main/services/extensions/catalog/McpCatalogAggregator';
import { OfficialMcpRegistryService } from '@main/services/extensions/catalog/OfficialMcpRegistryService';
import { GlamaMcpEnrichmentService } from '@main/services/extensions/catalog/GlamaMcpEnrichmentService';
import type { McpCatalogItem } from '@shared/types/extensions';
describe('McpCatalogAggregator', () => {
let aggregator: McpCatalogAggregator;
let official: OfficialMcpRegistryService;
let glama: GlamaMcpEnrichmentService;
const makeItem = (overrides: Partial<McpCatalogItem>): McpCatalogItem => ({
id: 'test-id',
name: 'test',
description: 'test desc',
source: 'official',
installSpec: null,
envVars: [],
tools: [],
requiresAuth: false,
...overrides,
});
beforeEach(() => {
official = new OfficialMcpRegistryService();
glama = new GlamaMcpEnrichmentService();
aggregator = new McpCatalogAggregator(official, glama);
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('search', () => {
it('merges results from both sources', async () => {
const officialItem = makeItem({
id: 'io.example/server',
name: 'Example',
source: 'official',
repositoryUrl: 'https://github.com/example/server',
});
const glamaItem = makeItem({
id: 'glama:abc123',
name: 'glama-only',
source: 'glama',
repositoryUrl: 'https://github.com/glama/only',
});
vi.spyOn(official, 'search').mockResolvedValue([officialItem]);
vi.spyOn(glama, 'search').mockResolvedValue([glamaItem]);
const result = await aggregator.search('test');
expect(result.servers).toHaveLength(2);
expect(result.warnings).toEqual([]);
});
it('reports warning when official registry fails', async () => {
vi.spyOn(official, 'search').mockRejectedValue(new Error('timeout'));
vi.spyOn(glama, 'search').mockResolvedValue([makeItem({ id: 'glama:1', source: 'glama' })]);
const result = await aggregator.search('test');
expect(result.servers).toHaveLength(1);
expect(result.warnings).toContain('Official MCP Registry unavailable');
});
it('reports warning when glama fails', async () => {
vi.spyOn(official, 'search').mockResolvedValue([makeItem({ id: 'off1', source: 'official' })]);
vi.spyOn(glama, 'search').mockRejectedValue(new Error('timeout'));
const result = await aggregator.search('test');
expect(result.servers).toHaveLength(1);
expect(result.warnings).toContain('Glama enrichment unavailable');
});
it('deduplicates by repository URL (official takes priority)', async () => {
const repo = 'https://github.com/shared/repo';
const officialItem = makeItem({
id: 'io.shared/repo',
name: 'Official',
source: 'official',
repositoryUrl: repo,
});
const glamaItem = makeItem({
id: 'glama:shared',
name: 'Glama',
source: 'glama',
repositoryUrl: repo,
license: 'MIT',
});
vi.spyOn(official, 'search').mockResolvedValue([officialItem]);
vi.spyOn(glama, 'search').mockResolvedValue([glamaItem]);
const result = await aggregator.search('test');
// Should have 1 (official), not 2
expect(result.servers).toHaveLength(1);
expect(result.servers[0].source).toBe('official');
// Enriched with Glama license
expect(result.servers[0].license).toBe('MIT');
});
it('handles case-insensitive repo URL dedup', async () => {
const officialItem = makeItem({
id: 'io.example/repo',
source: 'official',
repositoryUrl: 'https://GitHub.com/Example/Repo.git',
});
const glamaItem = makeItem({
id: 'glama:x',
source: 'glama',
repositoryUrl: 'https://github.com/example/repo',
});
vi.spyOn(official, 'search').mockResolvedValue([officialItem]);
vi.spyOn(glama, 'search').mockResolvedValue([glamaItem]);
const result = await aggregator.search('test');
expect(result.servers).toHaveLength(1);
});
});
describe('getById', () => {
it('delegates to official for non-glama IDs', async () => {
const item = makeItem({ id: 'io.example/server' });
vi.spyOn(official, 'getById').mockResolvedValue(item);
const result = await aggregator.getById('io.example/server');
expect(result).toBe(item);
expect(official.getById).toHaveBeenCalledWith('io.example/server');
});
it('returns null for glama IDs (cannot auto-install)', async () => {
const result = await aggregator.getById('glama:abc123');
expect(result).toBeNull();
});
});
});