Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export {

export {
INFERENCE_CONNECTORS_INTERNAL_API_PATH,
type ApiInferenceConnector,
type InferenceConnectorsApiResponseBody,
} from './src/inference_connectors_api';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ import type { InferenceConnector } from './connectors/connectors';
export const INFERENCE_CONNECTORS_INTERNAL_API_PATH =
'/internal/search_inference_endpoints/connectors' as const;

/**
* Connector entry returned by {@link INFERENCE_CONNECTORS_INTERNAL_API_PATH}.
* `isRecommended` is set server-side for endpoints that a feature recommends when no
* saved-object override is configured.
*/
export interface ApiInferenceConnector extends InferenceConnector {
isRecommended?: boolean;
}

/**
* Response body shape for {@link INFERENCE_CONNECTORS_INTERNAL_API_PATH}.
*/
export interface InferenceConnectorsApiResponseBody {
connectors: InferenceConnector[];
allConnectors: InferenceConnector[];
connectors: ApiInferenceConnector[];
soEntryFound: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ dependsOn:
- '@kbn/core-ui-settings-browser'
- '@kbn/connector-schemas'
- '@kbn/inference-common'
- '@kbn/management-settings-ids'
- '@kbn/alerts-ui-shared'
tags:
- shared-browser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import type { HttpSetup } from '@kbn/core-http-browser';
import {
INFERENCE_CONNECTORS_INTERNAL_API_PATH,
InferenceConnectorType,
type InferenceConnector,
type ApiInferenceConnector,
} from '@kbn/inference-common';
import { fetchConnectorsForFeature } from './fetch_connectors_for_feature';

const inferenceConnector = (connectorId: string): InferenceConnector => ({
const inferenceConnector = (connectorId: string): ApiInferenceConnector => ({
type: InferenceConnectorType.Inference,
name: connectorId,
connectorId,
Expand All @@ -24,12 +24,11 @@ const inferenceConnector = (connectorId: string): InferenceConnector => ({
});

describe('fetchConnectorsForFeature', () => {
it('calls the shared internal path with featureId and returns merged connectors with soEntryFound', async () => {
const rec = inferenceConnector('rec');
it('calls the shared internal path with featureId and returns the response as-is', async () => {
const rec = { ...inferenceConnector('rec'), isRecommended: true };
const other = inferenceConnector('other');
const httpGet = jest.fn().mockResolvedValue({
connectors: [rec],
allConnectors: [other, rec],
connectors: [rec, other],
soEntryFound: false,
});
const http = { get: httpGet } as unknown as HttpSetup;
Expand All @@ -42,15 +41,14 @@ describe('fetchConnectorsForFeature', () => {
version: '1',
});
expect(result.connectors.map((c) => c.connectorId)).toEqual(['rec', 'other']);
expect(result.connectors[0].isRecommended).toBe(true);
expect(result.soEntryFound).toBe(false);
});

it('returns SO-configured connectors unchanged when soEntryFound is true', async () => {
it('returns SO-configured connectors with soEntryFound true', async () => {
const a = inferenceConnector('a');
const b = inferenceConnector('b');
const httpGet = jest.fn().mockResolvedValue({
connectors: [a],
allConnectors: [a, b],
soEntryFound: true,
});
const http = { get: httpGet } as unknown as HttpSetup;
Expand All @@ -60,20 +58,4 @@ describe('fetchConnectorsForFeature', () => {
expect(result.connectors).toEqual([a]);
expect(result.soEntryFound).toBe(true);
});

it('returns all connectors with default first when no SO and no recommendations', async () => {
const a = inferenceConnector('a');
const b = inferenceConnector('b');
const httpGet = jest.fn().mockResolvedValue({
connectors: [],
allConnectors: [a, b],
soEntryFound: false,
});
const http = { get: httpGet } as unknown as HttpSetup;

const result = await fetchConnectorsForFeature(http, 'x');

expect(result.connectors).toEqual([a, b]);
expect(result.soEntryFound).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,28 @@
*/

import type { HttpSetup } from '@kbn/core-http-browser';
import type { InferenceConnector } from '@kbn/inference-common';
import type { ApiInferenceConnector } from '@kbn/inference-common';
import {
INFERENCE_CONNECTORS_INTERNAL_API_PATH,
type InferenceConnectorsApiResponseBody,
} from '@kbn/inference-common';
import { mergeConnectorsFromApiResponse } from './merge_connectors_from_api_response';

export interface FetchConnectorsForFeatureResult {
connectors: InferenceConnector[];
connectors: ApiInferenceConnector[];
soEntryFound: boolean;
}

export const fetchConnectorsForFeature = async (
http: HttpSetup,
featureId: string
): Promise<FetchConnectorsForFeatureResult> => {
const { connectors, allConnectors, soEntryFound } =
await http.get<InferenceConnectorsApiResponseBody>(INFERENCE_CONNECTORS_INTERNAL_API_PATH, {
const { connectors, soEntryFound } = await http.get<InferenceConnectorsApiResponseBody>(
INFERENCE_CONNECTORS_INTERNAL_API_PATH,
{
query: { featureId },
version: '1',
});
}
);

return {
connectors: mergeConnectorsFromApiResponse(connectors, allConnectors, soEntryFound),
soEntryFound,
};
return { connectors, soEntryFound };
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,18 @@
*/

import type { HttpSetup } from '@kbn/core-http-browser';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import { InferenceConnectorType, type InferenceConnector } from '@kbn/inference-common';
import {
GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR,
GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY,
} from '@kbn/management-settings-ids';
import { InferenceConnectorType, type ApiInferenceConnector } from '@kbn/inference-common';
import { loadConnectors, toAIConnector } from './load_connectors';
import { fetchConnectorsForFeature } from './fetch_connectors_for_feature';
import { fetchConnectorById } from './fetch_connector_by_id';

jest.mock('./fetch_connectors_for_feature');
const fetchConnectorsForFeatureMock = fetchConnectorsForFeature as jest.MockedFn<
typeof fetchConnectorsForFeature
>;

jest.mock('./fetch_connector_by_id');
const fetchConnectorByIdMock = fetchConnectorById as jest.MockedFn<typeof fetchConnectorById>;

const createInferenceConnector = (
overrides: Partial<InferenceConnector> = {}
): InferenceConnector => ({
overrides: Partial<ApiInferenceConnector> = {}
): ApiInferenceConnector => ({
type: InferenceConnectorType.OpenAI,
name: 'Test Connector',
connectorId: 'test-connector-id',
Expand All @@ -37,17 +28,6 @@ const createInferenceConnector = (
...overrides,
});

const createSettings = (defaultConnectorId?: string, defaultOnly = false): SettingsStart =>
({
client: {
get: jest.fn((key: string) => {
if (key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR) return defaultConnectorId;
if (key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY) return defaultOnly;
return undefined;
}),
},
} as unknown as SettingsStart);

describe('toAIConnector', () => {
it('should map InferenceConnector fields to AIConnector shape', () => {
const connector = createInferenceConnector({
Expand Down Expand Up @@ -93,6 +73,12 @@ describe('toAIConnector', () => {
expect(result.isMissingSecrets).toBe(false);
});

it('should propagate isRecommended when set on the API response', () => {
const connector = createInferenceConnector({ isRecommended: true });

expect(toAIConnector(connector).isRecommended).toBe(true);
});

it('should set apiProvider for non-preconfigured OpenAI connectors', () => {
const connector = createInferenceConnector({
isPreconfigured: false,
Expand All @@ -119,154 +105,34 @@ describe('loadConnectors', () => {
jest.clearAllMocks();
});

it('should fetch connectors and map to AIConnector', async () => {
it('fetches connectors for the feature and maps them to AIConnector', async () => {
const connector1 = createInferenceConnector({ connectorId: 'c1', name: 'Connector 1' });
const connector2 = createInferenceConnector({ connectorId: 'c2', name: 'Connector 2' });
const connector2 = createInferenceConnector({
connectorId: 'c2',
name: 'Connector 2',
isRecommended: true,
});
fetchConnectorsForFeatureMock.mockResolvedValue({
connectors: [connector1, connector2],
soEntryFound: false,
});
const settings = createSettings();

const result = await loadConnectors({ http, featureId: 'siem_migrations', settings });
const result = await loadConnectors({ http, featureId: 'siem_migrations' });

expect(fetchConnectorsForFeatureMock).toHaveBeenCalledWith(http, 'siem_migrations');
expect(result).toHaveLength(2);
expect(result[0].id).toBe('c1');
expect(result[1].id).toBe('c2');
expect(result[1].isRecommended).toBe(true);
});

it('should fetch default connector by ID when defaultOnly is true', async () => {
const aiConnector = {
id: 'c1',
name: 'Connector 1',
actionTypeId: InferenceConnectorType.OpenAI,
config: {},
secrets: {},
isPreconfigured: false,
isSystemAction: false,
isDeprecated: false,
isConnectorTypeDeprecated: false,
isMissingSecrets: false,
};
fetchConnectorByIdMock.mockResolvedValue(aiConnector as any);
const settings = createSettings('c1', true);

const result = await loadConnectors({ http, featureId: 'test', settings });

expect(fetchConnectorByIdMock).toHaveBeenCalledWith(http, 'c1');
expect(fetchConnectorsForFeatureMock).not.toHaveBeenCalled();
expect(result).toHaveLength(1);
expect(result[0].id).toBe('c1');
});

it('should return empty array when defaultOnly is true but connector is not found', async () => {
fetchConnectorByIdMock.mockResolvedValue(undefined);
const settings = createSettings('missing', true);

const result = await loadConnectors({ http, featureId: 'test', settings });

expect(fetchConnectorByIdMock).toHaveBeenCalledWith(http, 'missing');
expect(fetchConnectorsForFeatureMock).not.toHaveBeenCalled();
expect(result).toEqual([]);
});

it('should return empty array when defaultOnly is true but no default connector is configured', async () => {
const settings = createSettings(undefined, true);

const result = await loadConnectors({ http, featureId: 'test', settings });

expect(fetchConnectorByIdMock).not.toHaveBeenCalled();
expect(fetchConnectorsForFeatureMock).not.toHaveBeenCalled();
expect(result).toEqual([]);
});

it('should return default connector first when no SO entry and default is configured', async () => {
const defaultAiConnector = {
id: 'default',
name: 'Default Connector',
actionTypeId: InferenceConnectorType.OpenAI,
config: {},
secrets: {},
isPreconfigured: false,
isSystemAction: false,
isDeprecated: false,
isConnectorTypeDeprecated: false,
isMissingSecrets: false,
};
fetchConnectorByIdMock.mockResolvedValue(defaultAiConnector as any);
const connector1 = createInferenceConnector({ connectorId: 'c1', name: 'Connector 1' });
const connectorDefault = createInferenceConnector({
connectorId: 'default',
name: 'Default Connector',
});
fetchConnectorsForFeatureMock.mockResolvedValue({
connectors: [connector1, connectorDefault],
soEntryFound: false,
});
const settings = createSettings('default');

const result = await loadConnectors({ http, featureId: 'test', settings });

expect(fetchConnectorByIdMock).toHaveBeenCalledWith(http, 'default');
expect(result).toHaveLength(2);
expect(result[0].id).toBe('default');
expect(result[1].id).toBe('c1');
});

it('should prepend default connector even if not in the feature list', async () => {
const defaultAiConnector = {
id: 'external',
name: 'External Connector',
actionTypeId: InferenceConnectorType.OpenAI,
config: {},
secrets: {},
isPreconfigured: false,
isSystemAction: false,
isDeprecated: false,
isConnectorTypeDeprecated: false,
isMissingSecrets: false,
};
fetchConnectorByIdMock.mockResolvedValue(defaultAiConnector as any);
const connector1 = createInferenceConnector({ connectorId: 'c1', name: 'Connector 1' });
fetchConnectorsForFeatureMock.mockResolvedValue({
connectors: [connector1],
soEntryFound: false,
});
const settings = createSettings('external');

const result = await loadConnectors({ http, featureId: 'test', settings });

expect(result).toHaveLength(2);
expect(result[0].id).toBe('external');
expect(result[1].id).toBe('c1');
});

it('should not reorder when connectors are set via saved object', async () => {
const connector1 = createInferenceConnector({ connectorId: 'c1', name: 'Connector 1' });
const connector2 = createInferenceConnector({ connectorId: 'c2', name: 'Connector 2' });
fetchConnectorsForFeatureMock.mockResolvedValue({
connectors: [connector1, connector2],
soEntryFound: true,
});
const settings = createSettings('c2');

const result = await loadConnectors({ http, featureId: 'test', settings });

expect(fetchConnectorByIdMock).not.toHaveBeenCalled();
expect(result).toHaveLength(2);
expect(result[0].id).toBe('c1');
expect(result[1].id).toBe('c2');
});

it('should return empty array when no connectors are available', async () => {
it('returns an empty array when no connectors are available', async () => {
fetchConnectorsForFeatureMock.mockResolvedValue({
connectors: [],
soEntryFound: false,
});
const settings = createSettings();

const result = await loadConnectors({ http, featureId: 'test', settings });
const result = await loadConnectors({ http, featureId: 'test' });

expect(result).toEqual([]);
});
Expand Down
Loading
Loading