import { describe, it, expect, vi } from 'vitest'; import { refreshProjectUpstreams } from '../src/discovery.js'; import { McpRouter } from '../src/router.js'; function mockMcpdClient(servers: Array<{ id: string; name: string; transport: string }>) { return { baseUrl: 'http://test:3100', token: 'test-token', get: vi.fn(async () => servers), post: vi.fn(async () => ({ result: {} })), put: vi.fn(), delete: vi.fn(), forward: vi.fn(async () => ({ status: 200, body: servers })), }; } describe('refreshProjectUpstreams', () => { it('registers project-scoped servers as upstreams', async () => { const router = new McpRouter(); const client = mockMcpdClient([ { id: 'srv-1', name: 'grafana', transport: 'stdio' }, { id: 'srv-2', name: 'ha', transport: 'stdio' }, ]); const registered = await refreshProjectUpstreams(router, client as any, 'smart-home'); expect(registered).toEqual(['grafana', 'ha']); expect(router.getUpstreamNames()).toContain('grafana'); expect(router.getUpstreamNames()).toContain('ha'); expect(client.get).toHaveBeenCalledWith('/api/v1/projects/smart-home/servers'); }); it('removes stale upstreams on refresh', async () => { const router = new McpRouter(); // First refresh: 2 servers const client1 = mockMcpdClient([ { id: 'srv-1', name: 'grafana', transport: 'stdio' }, { id: 'srv-2', name: 'ha', transport: 'stdio' }, ]); await refreshProjectUpstreams(router, client1 as any, 'smart-home'); expect(router.getUpstreamNames()).toHaveLength(2); // Second refresh: only 1 server const client2 = mockMcpdClient([ { id: 'srv-1', name: 'grafana', transport: 'stdio' }, ]); await refreshProjectUpstreams(router, client2 as any, 'smart-home'); expect(router.getUpstreamNames()).toEqual(['grafana']); }); it('forwards auth token via forward() method', async () => { const router = new McpRouter(); const servers = [{ id: 'srv-1', name: 'grafana', transport: 'stdio' }]; const client = mockMcpdClient(servers); await refreshProjectUpstreams(router, client as any, 'smart-home', 'user-token-123'); expect(client.forward).toHaveBeenCalledWith('GET', '/api/v1/projects/smart-home/servers', '', undefined, 'user-token-123'); expect(router.getUpstreamNames()).toContain('grafana'); }); it('throws on failed project fetch', async () => { const router = new McpRouter(); const client = mockMcpdClient([]); client.forward.mockResolvedValue({ status: 403, body: { error: 'Forbidden' } }); await expect( refreshProjectUpstreams(router, client as any, 'secret-project', 'bad-token'), ).rejects.toThrow('Failed to fetch project servers: 403'); }); it('URL-encodes project name', async () => { const router = new McpRouter(); const client = mockMcpdClient([]); await refreshProjectUpstreams(router, client as any, 'my project'); expect(client.get).toHaveBeenCalledWith('/api/v1/projects/my%20project/servers'); }); it('handles empty project server list', async () => { const router = new McpRouter(); const client = mockMcpdClient([]); const registered = await refreshProjectUpstreams(router, client as any, 'empty-project'); expect(registered).toEqual([]); expect(router.getUpstreamNames()).toHaveLength(0); }); });