Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 7 additions & 47 deletions src/gaia/apps/webui/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export async function rollbackAgent(agentId: string): Promise<InstallStatus> {

// -- 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' };
Expand Down Expand Up @@ -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<ConnectorInfo> {
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<void> {
await apiFetch<unknown>('DELETE', `/connections/${provider}`);
}

export async function listAgentGrants(provider: string): Promise<{
grants: Record<string, string[]>;
}> {
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<void> {
await apiFetch<unknown>(
'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 ------------------------------------------------------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<ConnectorRow> & { 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({
Expand Down Expand Up @@ -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);
});
});
16 changes: 14 additions & 2 deletions src/gaia/apps/webui/src/stores/connectorsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,25 @@ export const useConnectionsStore = create<ConnectionsState>((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<string, Record<string, string[]>> = {};
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] = {};
Expand Down