diff --git a/src/gaia/apps/webui/src/services/api.ts b/src/gaia/apps/webui/src/services/api.ts index a233b9eb9..a51cd78d5 100644 --- a/src/gaia/apps/webui/src/services/api.ts +++ b/src/gaia/apps/webui/src/services/api.ts @@ -196,7 +196,7 @@ export async function rollbackAgent(agentId: string): Promise { // -- Connections (issue #915) --------------------------------------------------- -import type { AgentMcpServer, ConnectorInfo, ConnectorRow } from '../types'; +import type { AgentMcpServer, ConnectorRow } from '../types'; // New framework endpoints (T-8b) — /api/connectors const UI_HEADER = { 'x-gaia-ui': '1' }; @@ -364,52 +364,12 @@ export async function deactivateConnectorAgent( ); } -export async function listConnections(): Promise<{ connections: ConnectorInfo[] }> { - return apiFetch('GET', '/connections'); -} - -export async function getConnection(provider: string): Promise { - return apiFetch('GET', `/connections/${provider}`); -} - -export async function authorizeConnection( - provider: string, - scopes: string[], -): Promise<{ flow_id: string; authorization_url: string }> { - return apiFetch('POST', `/connections/${provider}/authorize`, { scopes }); -} - -export async function revokeConnection(provider: string): Promise { - await apiFetch('DELETE', `/connections/${provider}`); -} - -export async function listAgentGrants(provider: string): Promise<{ - grants: Record; -}> { - return apiFetch('GET', `/connections/${provider}/grants`); -} - -export async function grantAgent( - provider: string, - agentId: string, - scopes: string[], -): Promise<{ provider: string; agent_id: string; scopes: string[] }> { - return apiFetch( - 'PUT', - `/connections/${provider}/grants/${encodeURIComponent(agentId)}`, - { scopes }, - ); -} - -export async function revokeAgentGrant( - provider: string, - agentId: string, -): Promise { - await apiFetch( - 'DELETE', - `/connections/${provider}/grants/${encodeURIComponent(agentId)}`, - ); -} +// NOTE: the legacy `/connections*` helpers (listConnections, getConnection, +// authorizeConnection, revokeConnection, listAgentGrants, grantAgent, +// revokeAgentGrant) were removed in #1630. The backend no longer mounts +// `/api/connections` — connections moved to the T-8b `/api/connectors` +// framework above. Use listConnectors() / listConnectorGrants() / the +// /connectors helpers instead. // -- Sessions ------------------------------------------------------------------ diff --git a/src/gaia/apps/webui/src/stores/__tests__/connectorsStore.test.ts b/src/gaia/apps/webui/src/stores/__tests__/connectorsStore.test.ts index eb24050a7..ed617fdc0 100644 --- a/src/gaia/apps/webui/src/stores/__tests__/connectorsStore.test.ts +++ b/src/gaia/apps/webui/src/stores/__tests__/connectorsStore.test.ts @@ -1,9 +1,12 @@ // Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. // SPDX-License-Identifier: MIT -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { useConnectionsStore } from '../connectorsStore'; -import type { ConnectorInfo } from '../../types'; +import * as api from '../../services/api'; +import type { ConnectorInfo, ConnectorRow } from '../../types'; + +vi.mock('../../services/api'); const conn = (provider: string): ConnectorInfo => ({ provider, @@ -12,6 +15,31 @@ const conn = (provider: string): ConnectorInfo => ({ connected_at: null, }); +/** Build a ConnectorRow with sane defaults; override only what a test cares about. */ +const row = (overrides: Partial & { id: string }): ConnectorRow => ({ + display_name: overrides.id, + icon: null, + category: 'mail', + tier: 'standard', + type: 'oauth_pkce', + description: '', + product_url: null, + docs_url: null, + configured: true, + configurable: true, + config_error: null, + account_id: `test@${overrides.id}.com`, + scopes: ['mail.read'], + enabled: true, + activations: {}, + last_tested_at: null, + mcp_env_keys: [], + default_scopes: [], + available_scopes: [], + oauth_setup_fields: [], + ...overrides, +}); + describe('connectorsStore — pendingMailProvider clearing', () => { beforeEach(() => { useConnectionsStore.setState({ @@ -82,3 +110,69 @@ describe('connectorsStore — pendingMailProvider clearing', () => { }); }); }); + +describe('connectorsStore — refresh() migrated to /api/connectors (#1630)', () => { + beforeEach(() => { + vi.clearAllMocks(); + useConnectionsStore.setState({ + connections: [], + grants: {}, + loading: false, + error: null, + pendingMailProvider: undefined, + }); + }); + + it('maps connected OAuth connectors to ConnectorInfo and drops MCP / unconfigured tiles', async () => { + vi.mocked(api.listConnectors).mockResolvedValue({ + connectors: [ + row({ id: 'google' }), + row({ id: 'microsoft' }), + row({ id: 'outlook', configured: false }), // not connected → dropped + row({ id: 'mcp-git', type: 'mcp_server' }), // not OAuth → dropped + ], + }); + vi.mocked(api.listConnectorGrants).mockResolvedValue({ grants: {} }); + + await useConnectionsStore.getState().refresh(); + + const { connections, error, loading } = useConnectionsStore.getState(); + expect(error).toBeNull(); + expect(loading).toBe(false); + expect(connections.map((c) => c.provider)).toEqual(['google', 'microsoft']); + expect(connections[0]).toMatchObject({ + provider: 'google', + account_email: 'test@google.com', + scopes: ['mail.read'], + }); + // Grants are fetched via the framework endpoint, not the dead /connections one. + expect(api.listConnectorGrants).toHaveBeenCalledWith('google'); + expect(api.listConnectorGrants).toHaveBeenCalledWith('microsoft'); + }); + + it('records grants per provider from the framework endpoint', async () => { + vi.mocked(api.listConnectors).mockResolvedValue({ + connectors: [row({ id: 'google' })], + }); + vi.mocked(api.listConnectorGrants).mockResolvedValue({ + grants: { 'builtin:email': ['mail.read'] }, + }); + + await useConnectionsStore.getState().refresh(); + + expect(useConnectionsStore.getState().grants.google).toEqual({ + 'builtin:email': ['mail.read'], + }); + }); + + it('surfaces a list failure as error and leaves connections empty', async () => { + vi.mocked(api.listConnectors).mockRejectedValue(new Error('boom')); + + await useConnectionsStore.getState().refresh(); + + const { connections, error, loading } = useConnectionsStore.getState(); + expect(connections).toEqual([]); + expect(error).toBe('boom'); + expect(loading).toBe(false); + }); +}); diff --git a/src/gaia/apps/webui/src/stores/connectorsStore.ts b/src/gaia/apps/webui/src/stores/connectorsStore.ts index d802b127e..14a99d12e 100644 --- a/src/gaia/apps/webui/src/stores/connectorsStore.ts +++ b/src/gaia/apps/webui/src/stores/connectorsStore.ts @@ -54,13 +54,25 @@ export const useConnectionsStore = create((set, get) => ({ refresh: async () => { set({ loading: true, error: null }); try { - const { connections } = await api.listConnections(); + // T-8b framework endpoint — the legacy /api/connections routes the + // old listConnections() targeted are no longer mounted (#1630). + const { connectors } = await api.listConnectors(); + // The mail selector consumes connected OAuth providers only; MCP + // and not-yet-connected tiles never feed it. + const connections: ConnectorInfo[] = connectors + .filter((c) => c.type === 'oauth_pkce' && c.configured) + .map((c) => ({ + provider: c.id, + account_email: c.account_id ?? '', + scopes: c.scopes, + connected_at: null, + })); // Pull grants for every connected provider. const grants: Record> = {}; await Promise.all( connections.map(async (c) => { try { - const r = await api.listAgentGrants(c.provider); + const r = await api.listConnectorGrants(c.provider); grants[c.provider] = r.grants; } catch { grants[c.provider] = {};