mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security solution] More Elastic Assistant tests (#168146)
This commit is contained in:
parent
e672224597
commit
413715f134
52 changed files with 3508 additions and 196 deletions
|
@ -8,7 +8,14 @@
|
|||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
|
||||
|
||||
import { fetchConnectorExecuteAction, FetchConnectorExecuteAction } from './api';
|
||||
import {
|
||||
deleteKnowledgeBase,
|
||||
fetchConnectorExecuteAction,
|
||||
FetchConnectorExecuteAction,
|
||||
getKnowledgeBaseStatus,
|
||||
postEvaluation,
|
||||
postKnowledgeBase,
|
||||
} from './api';
|
||||
import type { Conversation, Message } from '../assistant_context/types';
|
||||
import { API_ERROR } from './translations';
|
||||
|
||||
|
@ -28,136 +35,277 @@ const messages: Message[] = [
|
|||
{ content: 'This is a test', role: 'user', timestamp: new Date().toLocaleString() },
|
||||
];
|
||||
|
||||
describe('fetchConnectorExecuteAction', () => {
|
||||
describe('API tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls the internal assistant API when assistantLangChain is true', async () => {
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
describe('fetchConnectorExecuteAction', () => {
|
||||
it('calls the internal assistant API when assistantLangChain is true', async () => {
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
await fetchConnectorExecuteAction(testProps);
|
||||
await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"}}',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
signal: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the actions connector api when assistantLangChain is false', async () => {
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: false,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith('/api/actions/connector/foo/_execute', {
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"}}',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
signal: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the actions connector api when assistantLangChain is false', async () => {
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: false,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
it('returns API_ERROR when the response status is not ok', async () => {
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({ status: 'error' });
|
||||
|
||||
await fetchConnectorExecuteAction(testProps);
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: false,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith('/api/actions/connector/foo/_execute', {
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"}}',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
signal: undefined,
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response: API_ERROR, isError: true });
|
||||
});
|
||||
|
||||
it('returns API_ERROR when there are no choices', async () => {
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({ status: 'ok', data: '' });
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: false,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response: API_ERROR, isError: true });
|
||||
});
|
||||
|
||||
it('returns the value of the action_input property when assistantLangChain is true, and `content` has properly prefixed and suffixed JSON with the action_input property', async () => {
|
||||
const response = '```json\n{"action_input": "value from action_input"}\n```';
|
||||
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({
|
||||
status: 'ok',
|
||||
data: response,
|
||||
});
|
||||
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true, // <-- requires response parsing
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response: 'value from action_input', isError: false });
|
||||
});
|
||||
|
||||
it('returns the original content when assistantLangChain is true, and `content` has properly formatted JSON WITHOUT the action_input property', async () => {
|
||||
const response = '```json\n{"some_key": "some value"}\n```';
|
||||
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({
|
||||
status: 'ok',
|
||||
data: response,
|
||||
});
|
||||
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true, // <-- requires response parsing
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response, isError: false });
|
||||
});
|
||||
|
||||
it('returns the original when assistantLangChain is true, and `content` is not JSON', async () => {
|
||||
const response = 'plain text content';
|
||||
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({
|
||||
status: 'ok',
|
||||
data: response,
|
||||
});
|
||||
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true, // <-- requires response parsing
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response, isError: false });
|
||||
});
|
||||
});
|
||||
|
||||
it('returns API_ERROR when the response status is not ok', async () => {
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({ status: 'error' });
|
||||
describe('getKnowledgeBaseStatus', () => {
|
||||
it('calls the knowledge base API when correct resource path', async () => {
|
||||
const testProps = {
|
||||
resource: 'a-resource',
|
||||
http: mockHttp,
|
||||
};
|
||||
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: false,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
await getKnowledgeBaseStatus(testProps);
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response: API_ERROR, isError: true });
|
||||
});
|
||||
|
||||
it('returns API_ERROR when there are no choices', async () => {
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({ status: 'ok', data: '' });
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: false,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response: API_ERROR, isError: true });
|
||||
});
|
||||
|
||||
it('returns the value of the action_input property when assistantLangChain is true, and `content` has properly prefixed and suffixed JSON with the action_input property', async () => {
|
||||
const response = '```json\n{"action_input": "value from action_input"}\n```';
|
||||
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({
|
||||
status: 'ok',
|
||||
data: response,
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/a-resource',
|
||||
{
|
||||
method: 'GET',
|
||||
signal: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
it('returns error when error is an error', async () => {
|
||||
const testProps = {
|
||||
resource: 'a-resource',
|
||||
http: mockHttp,
|
||||
};
|
||||
const error = 'simulated error';
|
||||
(mockHttp.fetch as jest.Mock).mockImplementation(() => {
|
||||
throw new Error(error);
|
||||
});
|
||||
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true, // <-- requires response parsing
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response: 'value from action_input', isError: false });
|
||||
await expect(getKnowledgeBaseStatus(testProps)).resolves.toThrowError('simulated error');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the original content when assistantLangChain is true, and `content` has properly formatted JSON WITHOUT the action_input property', async () => {
|
||||
const response = '```json\n{"some_key": "some value"}\n```';
|
||||
describe('postKnowledgeBase', () => {
|
||||
it('calls the knowledge base API when correct resource path', async () => {
|
||||
const testProps = {
|
||||
resource: 'a-resource',
|
||||
http: mockHttp,
|
||||
};
|
||||
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({
|
||||
status: 'ok',
|
||||
data: response,
|
||||
await postKnowledgeBase(testProps);
|
||||
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/a-resource',
|
||||
{
|
||||
method: 'POST',
|
||||
signal: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
it('returns error when error is an error', async () => {
|
||||
const testProps = {
|
||||
resource: 'a-resource',
|
||||
http: mockHttp,
|
||||
};
|
||||
const error = 'simulated error';
|
||||
(mockHttp.fetch as jest.Mock).mockImplementation(() => {
|
||||
throw new Error(error);
|
||||
});
|
||||
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true, // <-- requires response parsing
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
|
||||
expect(result).toEqual({ response, isError: false });
|
||||
await expect(postKnowledgeBase(testProps)).resolves.toThrowError('simulated error');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the original when assistantLangChain is true, and `content` is not JSON', async () => {
|
||||
const response = 'plain text content';
|
||||
describe('deleteKnowledgeBase', () => {
|
||||
it('calls the knowledge base API when correct resource path', async () => {
|
||||
const testProps = {
|
||||
resource: 'a-resource',
|
||||
http: mockHttp,
|
||||
};
|
||||
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({
|
||||
status: 'ok',
|
||||
data: response,
|
||||
await deleteKnowledgeBase(testProps);
|
||||
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/a-resource',
|
||||
{
|
||||
method: 'DELETE',
|
||||
signal: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
it('returns error when error is an error', async () => {
|
||||
const testProps = {
|
||||
resource: 'a-resource',
|
||||
http: mockHttp,
|
||||
};
|
||||
const error = 'simulated error';
|
||||
(mockHttp.fetch as jest.Mock).mockImplementation(() => {
|
||||
throw new Error(error);
|
||||
});
|
||||
|
||||
const testProps: FetchConnectorExecuteAction = {
|
||||
assistantLangChain: true, // <-- requires response parsing
|
||||
http: mockHttp,
|
||||
messages,
|
||||
apiConfig,
|
||||
};
|
||||
await expect(deleteKnowledgeBase(testProps)).resolves.toThrowError('simulated error');
|
||||
});
|
||||
});
|
||||
|
||||
const result = await fetchConnectorExecuteAction(testProps);
|
||||
describe('postEvaluation', () => {
|
||||
it('calls the knowledge base API when correct resource path', async () => {
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({ success: true });
|
||||
const testProps = {
|
||||
http: mockHttp,
|
||||
evalParams: {
|
||||
agents: ['not', 'alphabetical'],
|
||||
dataset: '{}',
|
||||
evalModel: ['not', 'alphabetical'],
|
||||
evalPrompt: 'evalPrompt',
|
||||
evaluationType: ['not', 'alphabetical'],
|
||||
models: ['not', 'alphabetical'],
|
||||
outputIndex: 'outputIndex',
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual({ response, isError: false });
|
||||
await postEvaluation(testProps);
|
||||
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', {
|
||||
method: 'POST',
|
||||
body: '{"dataset":{},"evalPrompt":"evalPrompt"}',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
query: {
|
||||
models: 'alphabetical,not',
|
||||
agents: 'alphabetical,not',
|
||||
evaluationType: 'alphabetical,not',
|
||||
evalModel: 'alphabetical,not',
|
||||
outputIndex: 'outputIndex',
|
||||
},
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
it('returns error when error is an error', async () => {
|
||||
const testProps = {
|
||||
resource: 'a-resource',
|
||||
http: mockHttp,
|
||||
};
|
||||
const error = 'simulated error';
|
||||
(mockHttp.fetch as jest.Mock).mockImplementation(() => {
|
||||
throw new Error(error);
|
||||
});
|
||||
|
||||
await expect(postEvaluation(testProps)).resolves.toThrowError('simulated error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ConversationSelector } from '.';
|
||||
import { render, fireEvent, within } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation';
|
||||
import { CONVERSATION_SELECTOR_PLACE_HOLDER } from './translations';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
|
||||
const setConversation = jest.fn();
|
||||
const deleteConversation = jest.fn();
|
||||
const mockConversation = {
|
||||
appendMessage: jest.fn(),
|
||||
appendReplacements: jest.fn(),
|
||||
clearConversation: jest.fn(),
|
||||
createConversation: jest.fn(),
|
||||
deleteConversation,
|
||||
setApiConfig: jest.fn(),
|
||||
setConversation,
|
||||
};
|
||||
|
||||
jest.mock('../../use_conversation', () => ({
|
||||
useConversation: () => mockConversation,
|
||||
}));
|
||||
|
||||
const onConversationSelected = jest.fn();
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
onConversationSelected,
|
||||
selectedConversationId: 'Welcome',
|
||||
defaultConnectorId: '123',
|
||||
defaultProvider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
describe('Conversation selector', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('renders with correct selected conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId('conversation-selector')).toBeInTheDocument();
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(welcomeConvo.id);
|
||||
});
|
||||
it('On change, selects new item', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
fireEvent.click(getByTestId(`convo-option-${alertConvo.id}`));
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(alertConvo.id);
|
||||
});
|
||||
it('On clear input, clears selected options', () => {
|
||||
const { getByText, queryByText, getByTestId, queryByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId('euiComboBoxPill')).toBeInTheDocument();
|
||||
expect(queryByText(CONVERSATION_SELECTOR_PLACE_HOLDER)).not.toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('comboBoxClearButton'));
|
||||
expect(getByText(CONVERSATION_SELECTOR_PLACE_HOLDER)).toBeInTheDocument();
|
||||
expect(queryByTestId('euiComboBoxPill')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('We can add a custom option', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const customOption = 'Custom option';
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(setConversation).toHaveBeenCalledWith({
|
||||
conversation: {
|
||||
id: customOption,
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
defaultSystemPromptId: undefined,
|
||||
provider: 'OpenAI',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Only custom options can be deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
expect(
|
||||
within(getByTestId(`convo-option-${customConvo.id}`)).getByTestId('delete-option')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(getByTestId(`convo-option-${alertConvo.id}`)).queryByTestId('delete-option')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Custom options can be deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
fireEvent.click(
|
||||
within(getByTestId(`convo-option-${customConvo.id}`)).getByTestId('delete-option')
|
||||
);
|
||||
jest.runAllTimers();
|
||||
expect(onConversationSelected).not.toHaveBeenCalled();
|
||||
|
||||
expect(deleteConversation).toHaveBeenCalledWith(customConvo.id);
|
||||
});
|
||||
|
||||
it('Previous conversation is set to active when selected conversation is deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} selectedConversationId={customConvo.id} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
fireEvent.click(
|
||||
within(getByTestId(`convo-option-${customConvo.id}`)).getByTestId('delete-option')
|
||||
);
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(welcomeConvo.id);
|
||||
});
|
||||
|
||||
it('Left arrow selects first conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'ArrowLeft',
|
||||
ctrlKey: true,
|
||||
code: 'ArrowLeft',
|
||||
charCode: 27,
|
||||
});
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(alertConvo.id);
|
||||
});
|
||||
|
||||
it('Right arrow selects last conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'ArrowRight',
|
||||
ctrlKey: true,
|
||||
code: 'ArrowRight',
|
||||
charCode: 26,
|
||||
});
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(customConvo.id);
|
||||
});
|
||||
|
||||
it('Right arrow does nothing when ctrlKey is false', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'ArrowRight',
|
||||
ctrlKey: false,
|
||||
code: 'ArrowRight',
|
||||
charCode: 26,
|
||||
});
|
||||
expect(onConversationSelected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Right arrow does nothing when conversation lenth is 1', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'ArrowRight',
|
||||
ctrlKey: true,
|
||||
code: 'ArrowRight',
|
||||
charCode: 26,
|
||||
});
|
||||
expect(onConversationSelected).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -63,11 +63,10 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
shouldDisableKeyboardShortcut = () => false,
|
||||
isDisabled = false,
|
||||
}) => {
|
||||
const { allSystemPrompts } = useAssistantContext();
|
||||
const { allSystemPrompts, conversations } = useAssistantContext();
|
||||
|
||||
const { deleteConversation, setConversation } = useConversation();
|
||||
|
||||
const { conversations } = useAssistantContext();
|
||||
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
|
||||
const conversationOptions = useMemo<ConversationSelectorOption[]>(() => {
|
||||
return Object.values(conversations).map((conversation) => ({
|
||||
|
@ -203,6 +202,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
className={'parentFlexGroup'}
|
||||
component={'span'}
|
||||
justifyContent="spaceBetween"
|
||||
data-test-subj={`convo-option-${label}`}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component={'span'}
|
||||
|
@ -232,6 +232,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
e.stopPropagation();
|
||||
onDelete(label);
|
||||
}}
|
||||
data-test-subj="delete-option"
|
||||
css={css`
|
||||
visibility: hidden;
|
||||
.parentFlexGroup:hover & {
|
||||
|
@ -255,6 +256,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
`}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="conversation-selector"
|
||||
aria-label={i18n.CONVERSATION_SELECTOR_ARIA_LABEL}
|
||||
customOptionText={`${i18n.CONVERSATION_SELECTOR_CUSTOM_OPTION_TEXT} {searchValue}`}
|
||||
placeholder={i18n.CONVERSATION_SELECTOR_PLACE_HOLDER}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { ConversationSelectorSettings } from '.';
|
||||
import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation';
|
||||
|
||||
const onConversationSelectionChange = jest.fn();
|
||||
const onConversationDeleted = jest.fn();
|
||||
const mockConversations = {
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
};
|
||||
const testProps = {
|
||||
conversations: mockConversations,
|
||||
selectedConversationId: welcomeConvo.id,
|
||||
onConversationDeleted,
|
||||
onConversationSelectionChange,
|
||||
};
|
||||
|
||||
describe('ConversationSelectorSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selects an existing conversation', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
expect(getByTestId('comboBoxInput')).toHaveTextContent(welcomeConvo.id);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
fireEvent.click(getByTestId(alertConvo.id));
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith(alertConvo);
|
||||
});
|
||||
it('Only custom option can be deleted', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
// there is only one delete conversation because there is only one custom convo
|
||||
fireEvent.click(getByTestId('delete-conversation'));
|
||||
expect(onConversationDeleted).toHaveBeenCalledWith(customConvo.id);
|
||||
});
|
||||
it('Selects existing conversation from the search input', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: alertConvo.id } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith(alertConvo);
|
||||
});
|
||||
it('Creates a new conversation', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
const customOption = 'Cool new conversation';
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith(customOption);
|
||||
});
|
||||
|
||||
it('Left arrow selects previous conversation', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
|
||||
fireEvent.click(getByTestId('arrowLeft'));
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith(alertConvo);
|
||||
});
|
||||
|
||||
it('Right arrow selects next conversation', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
|
||||
fireEvent.click(getByTestId('arrowRight'));
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith(customConvo);
|
||||
});
|
||||
});
|
|
@ -18,20 +18,16 @@ import {
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { Conversation, Prompt } from '../../../..';
|
||||
import { Conversation } from '../../../..';
|
||||
import { UseAssistantContext } from '../../../assistant_context';
|
||||
import * as i18n from './translations';
|
||||
import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector';
|
||||
|
||||
interface Props {
|
||||
allSystemPrompts: Prompt[];
|
||||
conversations: UseAssistantContext['conversations'];
|
||||
onConversationDeleted: (conversationId: string) => void;
|
||||
onConversationSelectionChange: (conversation?: Conversation | string) => void;
|
||||
selectedConversationId?: string;
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
}
|
||||
|
||||
const getPreviousConversationId = (conversationIds: string[], selectedConversationId = '') => {
|
||||
|
@ -58,13 +54,10 @@ export type ConversationSelectorSettingsOption = EuiComboBoxOptionOption<{
|
|||
*/
|
||||
export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
||||
({
|
||||
allSystemPrompts,
|
||||
conversations,
|
||||
onConversationDeleted,
|
||||
onConversationSelectionChange,
|
||||
selectedConversationId,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
}) => {
|
||||
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
|
||||
|
||||
|
@ -74,6 +67,7 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
return Object.values(conversations).map((conversation) => ({
|
||||
value: { isDefault: conversation.isDefault ?? false },
|
||||
label: conversation.id,
|
||||
'data-test-subj': conversation.id,
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -195,6 +189,7 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
iconType="cross"
|
||||
aria-label={i18n.DELETE_CONVERSATION}
|
||||
color="danger"
|
||||
data-test-subj="delete-conversation"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDelete(label);
|
||||
|
@ -235,6 +230,7 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
prepend={
|
||||
<EuiButtonIcon
|
||||
iconType="arrowLeft"
|
||||
data-test-subj="arrowLeft"
|
||||
aria-label={i18n.PREVIOUS_CONVERSATION_TITLE}
|
||||
onClick={onLeftArrowClick}
|
||||
disabled={conversationIds.length <= 1}
|
||||
|
@ -243,6 +239,7 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
append={
|
||||
<EuiButtonIcon
|
||||
iconType="arrowRight"
|
||||
data-test-subj="arrowRight"
|
||||
aria-label={i18n.NEXT_CONVERSATION_TITLE}
|
||||
onClick={onRightArrowClick}
|
||||
disabled={conversationIds.length <= 1}
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { ConversationSettings, ConversationSettingsProps } from './conversation_settings';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation';
|
||||
import { mockSystemPrompts } from '../../../mock/system_prompt';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { mockConnectors } from '../../../mock/connectors';
|
||||
|
||||
const mockConvos = {
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[alertConvo.id]: alertConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
};
|
||||
const onSelectedConversationChange = jest.fn();
|
||||
|
||||
const setUpdatedConversationSettings = jest.fn().mockImplementation((fn) => {
|
||||
return fn(mockConvos);
|
||||
});
|
||||
|
||||
const testProps = {
|
||||
allSystemPrompts: mockSystemPrompts,
|
||||
conversationSettings: mockConvos,
|
||||
defaultConnectorId: '123',
|
||||
defaultProvider: OpenAiProviderType.OpenAi,
|
||||
http: { basePath: { get: jest.fn() } },
|
||||
onSelectedConversationChange,
|
||||
selectedConversation: welcomeConvo,
|
||||
setUpdatedConversationSettings,
|
||||
} as unknown as ConversationSettingsProps;
|
||||
|
||||
jest.mock('../../../connectorland/use_load_connectors', () => ({
|
||||
useLoadConnectors: () => ({
|
||||
data: mockConnectors,
|
||||
error: null,
|
||||
isSuccess: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockConvo = alertConvo;
|
||||
jest.mock('../conversation_selector_settings', () => ({
|
||||
// @ts-ignore
|
||||
ConversationSelectorSettings: ({ onConversationDeleted, onConversationSelectionChange }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="delete-convo"
|
||||
onClick={() => onConversationDeleted('Custom option')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-convo"
|
||||
onClick={() => onConversationSelectionChange(mockConvo)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-convo-custom"
|
||||
onClick={() => onConversationSelectionChange('Cool new conversation')}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
jest.mock('../../prompt_editor/system_prompt/select_system_prompt', () => ({
|
||||
// @ts-ignore
|
||||
SelectSystemPrompt: ({ onSystemPromptSelectionChange }) => (
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-sp"
|
||||
onClick={() => onSystemPromptSelectionChange(mockSystemPrompts[1].id)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
jest.mock('../../../connectorland/models/model_selector/model_selector', () => ({
|
||||
// @ts-ignore
|
||||
ModelSelector: ({ onModelSelectionChange }) => (
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-model"
|
||||
onClick={() => onModelSelectionChange('MODEL_GPT_4')}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
const mockConnector = {
|
||||
id: 'cool-id-bro',
|
||||
actionTypeId: '.gen-ai',
|
||||
name: 'cool name',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
};
|
||||
jest.mock('../../../connectorland/connector_selector', () => ({
|
||||
// @ts-ignore
|
||||
ConnectorSelector: ({ onConnectorSelectionChange }) => (
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-connector"
|
||||
onClick={() => onConnectorSelectionChange(mockConnector)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('ConversationSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selecting a system prompt updates the defaultSystemPromptId for the selected conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-sp'));
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
...mockConvos,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
defaultSystemPromptId: 'mock-superhero-system-prompt-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Selecting an existing conversation updates the selected convo and does not update convo settings', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-convo'));
|
||||
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith(mockConvos);
|
||||
expect(onSelectedConversationChange).toHaveBeenCalledWith(alertConvo);
|
||||
});
|
||||
it('Selecting an existing conversation updates the selected convo and is added to the convo settings', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-convo-custom'));
|
||||
const newConvo = {
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
provider: 'OpenAI',
|
||||
},
|
||||
id: 'Cool new conversation',
|
||||
messages: [],
|
||||
};
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
...mockConvos,
|
||||
[newConvo.id]: newConvo,
|
||||
});
|
||||
expect(onSelectedConversationChange).toHaveBeenCalledWith(newConvo);
|
||||
});
|
||||
it('Deleting a conversation removes it from the convo settings', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('delete-convo'));
|
||||
const { [customConvo.id]: _, ...rest } = mockConvos;
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith(rest);
|
||||
});
|
||||
it('Selecting a new connector updates the conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-connector'));
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
...mockConvos,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
connectorId: mockConnector.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('Selecting a new connector model updates the conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-model'));
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
...mockConvos,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
model: 'MODEL_GPT_4',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -202,7 +202,6 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
|
||||
<ConversationSelectorSettings
|
||||
selectedConversationId={selectedConversation?.id}
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
conversations={conversationSettings}
|
||||
onConversationDeleted={onConversationDeleted}
|
||||
onConversationSelectionChange={onConversationSelectionChange}
|
||||
|
@ -220,7 +219,7 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
compressed
|
||||
conversation={selectedConversation}
|
||||
isEditing={true}
|
||||
isDisabled={selectedConversation == null}
|
||||
isDisabled={isDisabled}
|
||||
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
|
||||
selectedPrompt={selectedSystemPrompt}
|
||||
showTitles={true}
|
||||
|
@ -247,7 +246,7 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
}
|
||||
>
|
||||
<ConnectorSelector
|
||||
isDisabled={selectedConversation == null}
|
||||
isDisabled={isDisabled}
|
||||
onConnectorSelectionChange={handleOnConnectorSelectionChange}
|
||||
selectedConnectorId={selectedConnector?.id}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { ConversationMultiSelector } from './conversation_multi_selector';
|
||||
import { alertConvo, welcomeConvo, customConvo } from '../../../../../mock/conversation';
|
||||
|
||||
const onConversationSelectionChange = jest.fn();
|
||||
const testProps = {
|
||||
conversations: [alertConvo, welcomeConvo, customConvo],
|
||||
onConversationSelectionChange,
|
||||
selectedConversations: [welcomeConvo],
|
||||
};
|
||||
|
||||
describe('ConversationMultiSelector', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selects an existing quick prompt', () => {
|
||||
const { getByTestId } = render(<ConversationMultiSelector {...testProps} />);
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(welcomeConvo.id);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
fireEvent.click(getByTestId(`conversationMultiSelectorOption-${alertConvo.id}`));
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith([alertConvo, welcomeConvo]);
|
||||
});
|
||||
|
||||
it('Selects existing conversation from the search input', () => {
|
||||
const { getByTestId } = render(<ConversationMultiSelector {...testProps} />);
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), {
|
||||
target: { value: alertConvo.id },
|
||||
});
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith([alertConvo, welcomeConvo]);
|
||||
});
|
||||
it('Does not support custom options', () => {
|
||||
const { getByTestId } = render(<ConversationMultiSelector {...testProps} />);
|
||||
const customOption = 'Cool new prompt';
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onConversationSelectionChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { SystemPromptSelector } from './system_prompt_selector';
|
||||
import { mockSystemPrompts } from '../../../../../mock/system_prompt';
|
||||
|
||||
const onSystemPromptSelectionChange = jest.fn();
|
||||
const onSystemPromptDeleted = jest.fn();
|
||||
const testProps = {
|
||||
systemPrompts: mockSystemPrompts,
|
||||
onSystemPromptSelectionChange,
|
||||
onSystemPromptDeleted,
|
||||
selectedSystemPrompt: mockSystemPrompts[0],
|
||||
};
|
||||
|
||||
describe('SystemPromptSelector', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selects an existing quick prompt', () => {
|
||||
const { getByTestId } = render(<SystemPromptSelector {...testProps} />);
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(mockSystemPrompts[0].name);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
fireEvent.click(getByTestId(`systemPromptSelector-${mockSystemPrompts[1].id}`));
|
||||
expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(mockSystemPrompts[1]);
|
||||
});
|
||||
it('Deletes a system prompt that is not selected', () => {
|
||||
const { getByTestId, getAllByTestId } = render(<SystemPromptSelector {...testProps} />);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
// there is only one delete quick prompt because there is only one custom option
|
||||
fireEvent.click(getAllByTestId('delete-prompt')[1]);
|
||||
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[1].name);
|
||||
expect(onSystemPromptSelectionChange).not.toHaveBeenCalled();
|
||||
});
|
||||
it('Deletes a system prompt that is selected', () => {
|
||||
const { getByTestId, getAllByTestId } = render(<SystemPromptSelector {...testProps} />);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
// there is only one delete quick prompt because there is only one custom option
|
||||
fireEvent.click(getAllByTestId('delete-prompt')[0]);
|
||||
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[0].name);
|
||||
expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
it('Selects existing quick prompt from the search input', () => {
|
||||
const { getByTestId } = render(<SystemPromptSelector {...testProps} />);
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), {
|
||||
target: { value: mockSystemPrompts[1].name },
|
||||
});
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(mockSystemPrompts[1]);
|
||||
});
|
||||
it('Creates a new system prompt', () => {
|
||||
const { getByTestId } = render(<SystemPromptSelector {...testProps} />);
|
||||
const customOption = 'Cool new prompt';
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(customOption);
|
||||
});
|
||||
});
|
|
@ -193,6 +193,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
iconType="cross"
|
||||
aria-label={i18n.DELETE_SYSTEM_PROMPT}
|
||||
color="danger"
|
||||
data-test-subj="delete-prompt"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDelete(label);
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { SystemPromptSettings } from './system_prompt_settings';
|
||||
import { TestProviders } from '../../../../mock/test_providers/test_providers';
|
||||
import { alertConvo, welcomeConvo } from '../../../../mock/conversation';
|
||||
import { mockSystemPrompts } from '../../../../mock/system_prompt';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
const onSelectedSystemPromptChange = jest.fn();
|
||||
const setUpdatedSystemPromptSettings = jest.fn().mockImplementation((fn) => {
|
||||
return fn(mockSystemPrompts);
|
||||
});
|
||||
const setUpdatedConversationSettings = jest.fn().mockImplementation((fn) => {
|
||||
return fn({
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[alertConvo.id]: alertConvo,
|
||||
});
|
||||
});
|
||||
|
||||
const testProps = {
|
||||
conversationSettings: {
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
},
|
||||
onSelectedSystemPromptChange,
|
||||
selectedSystemPrompt: mockSystemPrompts[0],
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedConversationSettings,
|
||||
systemPromptSettings: mockSystemPrompts,
|
||||
};
|
||||
|
||||
jest.mock('./system_prompt_selector/system_prompt_selector', () => ({
|
||||
// @ts-ignore
|
||||
SystemPromptSelector: ({ onSystemPromptDeleted, onSystemPromptSelectionChange }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="delete-sp"
|
||||
onClick={() => onSystemPromptDeleted(mockSystemPrompts[1].name)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-sp"
|
||||
onClick={() => onSystemPromptSelectionChange(mockSystemPrompts[1])}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-sp-custom"
|
||||
onClick={() => onSystemPromptSelectionChange('sooper custom prompt')}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
const mockConvos = [alertConvo, welcomeConvo];
|
||||
jest.mock('./conversation_multi_selector/conversation_multi_selector', () => ({
|
||||
// @ts-ignore
|
||||
ConversationMultiSelector: ({ onConversationSelectionChange }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-multi"
|
||||
onClick={() => onConversationSelectionChange(mockConvos)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('SystemPromptSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selecting a system prompt updates the selected system prompts', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SystemPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-sp'));
|
||||
expect(setUpdatedSystemPromptSettings).toHaveReturnedWith(mockSystemPrompts);
|
||||
expect(onSelectedSystemPromptChange).toHaveBeenCalledWith(mockSystemPrompts[1]);
|
||||
});
|
||||
it('Entering a custom system prompt creates a new system prompt', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SystemPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-sp-custom'));
|
||||
const customOption = {
|
||||
content: '',
|
||||
id: 'sooper custom prompt',
|
||||
name: 'sooper custom prompt',
|
||||
promptType: 'system',
|
||||
};
|
||||
expect(setUpdatedSystemPromptSettings).toHaveReturnedWith([...mockSystemPrompts, customOption]);
|
||||
expect(onSelectedSystemPromptChange).toHaveBeenCalledWith(customOption);
|
||||
});
|
||||
it('Updating the current prompt input updates the prompt', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SystemPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.change(getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT), {
|
||||
target: { value: 'what does this do' },
|
||||
});
|
||||
const mutatableQuickPrompts = [...mockSystemPrompts];
|
||||
const previousFirstElementOfTheArray = mutatableQuickPrompts.shift();
|
||||
|
||||
expect(setUpdatedSystemPromptSettings).toHaveReturnedWith([
|
||||
{ ...previousFirstElementOfTheArray, content: 'what does this do' },
|
||||
...mutatableQuickPrompts,
|
||||
]);
|
||||
});
|
||||
it('Updating prompt contexts updates the categories of the prompt', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SystemPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-multi'));
|
||||
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
defaultSystemPromptId: 'mock-system-prompt-1',
|
||||
},
|
||||
},
|
||||
[alertConvo.id]: {
|
||||
...alertConvo,
|
||||
apiConfig: {
|
||||
...alertConvo.apiConfig,
|
||||
defaultSystemPromptId: 'mock-system-prompt-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('Toggle default conversation checkbox works', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SystemPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId(TEST_IDS.SYSTEM_PROMPT_MODAL.TOGGLE_ALL_DEFAULT_CONVERSATIONS));
|
||||
const mutatableQuickPrompts = [...mockSystemPrompts];
|
||||
const previousFirstElementOfTheArray = mutatableQuickPrompts.shift();
|
||||
|
||||
expect(setUpdatedSystemPromptSettings).toHaveReturnedWith([
|
||||
{ ...previousFirstElementOfTheArray, isNewConversationDefault: true },
|
||||
...mutatableQuickPrompts.map((p) => ({ ...p, isNewConversationDefault: false })),
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { PromptContextSelector } from './prompt_context_selector';
|
||||
import { mockPromptContexts } from '../../../mock/prompt_context';
|
||||
|
||||
const onPromptContextSelectionChange = jest.fn();
|
||||
const testProps = {
|
||||
promptContexts: mockPromptContexts,
|
||||
selectedPromptContexts: [mockPromptContexts[0]],
|
||||
onPromptContextSelectionChange,
|
||||
};
|
||||
|
||||
describe('PromptContextSelector', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selects an existing prompt context and adds it to the previous selection', () => {
|
||||
const { getByTestId } = render(<PromptContextSelector {...testProps} />);
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(mockPromptContexts[0].description);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
fireEvent.click(getByTestId(mockPromptContexts[1].description));
|
||||
expect(onPromptContextSelectionChange).toHaveBeenCalledWith(mockPromptContexts);
|
||||
});
|
||||
|
||||
it('Selects existing prompt context from the search input', () => {
|
||||
const { getByTestId } = render(<PromptContextSelector {...testProps} />);
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), {
|
||||
target: { value: mockPromptContexts[1].description },
|
||||
});
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onPromptContextSelectionChange).toHaveBeenCalledWith(mockPromptContexts);
|
||||
});
|
||||
it('Does not support custom options', () => {
|
||||
const { getByTestId } = render(<PromptContextSelector {...testProps} />);
|
||||
const customOption = 'Cool new prompt';
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onPromptContextSelectionChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -33,6 +33,7 @@ export const PromptContextSelector: React.FC<Props> = React.memo(
|
|||
category: pc.category,
|
||||
},
|
||||
label: pc.description,
|
||||
'data-test-subj': pc.description,
|
||||
})),
|
||||
[promptContexts]
|
||||
);
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { QuickPromptSelector } from './quick_prompt_selector';
|
||||
import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
|
||||
|
||||
const onQuickPromptSelectionChange = jest.fn();
|
||||
const onQuickPromptDeleted = jest.fn();
|
||||
const testProps = {
|
||||
quickPrompts: MOCK_QUICK_PROMPTS,
|
||||
selectedQuickPrompt: MOCK_QUICK_PROMPTS[0],
|
||||
onQuickPromptDeleted,
|
||||
onQuickPromptSelectionChange,
|
||||
};
|
||||
|
||||
describe('QuickPromptSelector', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selects an existing quick prompt', () => {
|
||||
const { getByTestId } = render(<QuickPromptSelector {...testProps} />);
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MOCK_QUICK_PROMPTS[0].title);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
fireEvent.click(getByTestId(MOCK_QUICK_PROMPTS[1].title));
|
||||
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(MOCK_QUICK_PROMPTS[1]);
|
||||
});
|
||||
it('Only custom option can be deleted', () => {
|
||||
const { getByTestId } = render(<QuickPromptSelector {...testProps} />);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
// there is only one delete quick prompt because there is only one custom option
|
||||
fireEvent.click(getByTestId('delete-quick-prompt'));
|
||||
expect(onQuickPromptDeleted).toHaveBeenCalledWith('A_CUSTOM_OPTION');
|
||||
});
|
||||
it('Selects existing quick prompt from the search input', () => {
|
||||
const { getByTestId } = render(<QuickPromptSelector {...testProps} />);
|
||||
const customOption = 'A_CUSTOM_OPTION';
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith({
|
||||
categories: [],
|
||||
color: '#D36086',
|
||||
prompt: 'quickly prompt please',
|
||||
title: 'A_CUSTOM_OPTION',
|
||||
});
|
||||
});
|
||||
it('Creates a new quick prompt', () => {
|
||||
const { getByTestId } = render(<QuickPromptSelector {...testProps} />);
|
||||
const customOption = 'Cool new prompt';
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(customOption);
|
||||
});
|
||||
});
|
|
@ -49,6 +49,7 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
|
|||
isDefault: qp.isDefault ?? false,
|
||||
},
|
||||
label: qp.title,
|
||||
'data-test-subj': qp.title,
|
||||
color: qp.color,
|
||||
}))
|
||||
);
|
||||
|
@ -164,6 +165,7 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
|
|||
<EuiButtonIcon
|
||||
iconType="cross"
|
||||
aria-label={i18n.DELETE_QUICK_PROMPT_}
|
||||
data-test-subj="delete-quick-prompt"
|
||||
color="danger"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
@ -185,6 +187,7 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
data-test-subj="quickPromptSelector"
|
||||
aria-label={i18n.QUICK_PROMPT_SELECTOR}
|
||||
compressed
|
||||
isDisabled={isDisabled}
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { QuickPromptSettings } from './quick_prompt_settings';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
|
||||
import { mockPromptContexts } from '../../../mock/prompt_context';
|
||||
|
||||
const onSelectedQuickPromptChange = jest.fn();
|
||||
const setUpdatedQuickPromptSettings = jest.fn().mockImplementation((fn) => {
|
||||
return fn(MOCK_QUICK_PROMPTS);
|
||||
});
|
||||
|
||||
const testProps = {
|
||||
onSelectedQuickPromptChange,
|
||||
quickPromptSettings: MOCK_QUICK_PROMPTS,
|
||||
selectedQuickPrompt: MOCK_QUICK_PROMPTS[0],
|
||||
setUpdatedQuickPromptSettings,
|
||||
};
|
||||
const mockContext = {
|
||||
basePromptContexts: MOCK_QUICK_PROMPTS,
|
||||
};
|
||||
|
||||
jest.mock('../../../assistant_context', () => ({
|
||||
...jest.requireActual('../../../assistant_context'),
|
||||
useAssistantContext: () => mockContext,
|
||||
}));
|
||||
|
||||
jest.mock('../quick_prompt_selector/quick_prompt_selector', () => ({
|
||||
// @ts-ignore
|
||||
QuickPromptSelector: ({ onQuickPromptDeleted, onQuickPromptSelectionChange }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="delete-qp"
|
||||
onClick={() => onQuickPromptDeleted('A_CUSTOM_OPTION')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-qp"
|
||||
onClick={() => onQuickPromptSelectionChange(MOCK_QUICK_PROMPTS[3])}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-qp-custom"
|
||||
onClick={() => onQuickPromptSelectionChange('sooper custom prompt')}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
jest.mock('../prompt_context_selector/prompt_context_selector', () => ({
|
||||
// @ts-ignore
|
||||
PromptContextSelector: ({ onPromptContextSelectionChange }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-pc"
|
||||
onClick={() => onPromptContextSelectionChange(mockPromptContexts)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('QuickPromptSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Selecting a quick prompt updates the selected quick prompts', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<QuickPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-qp'));
|
||||
expect(setUpdatedQuickPromptSettings).toHaveReturnedWith(MOCK_QUICK_PROMPTS);
|
||||
expect(onSelectedQuickPromptChange).toHaveBeenCalledWith(MOCK_QUICK_PROMPTS[3]);
|
||||
});
|
||||
it('Entering a custom quick prompt creates a new quick prompt', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<QuickPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-qp-custom'));
|
||||
const customOption = {
|
||||
categories: [],
|
||||
color: '#D36086',
|
||||
prompt: '',
|
||||
title: 'sooper custom prompt',
|
||||
};
|
||||
expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([...MOCK_QUICK_PROMPTS, customOption]);
|
||||
expect(onSelectedQuickPromptChange).toHaveBeenCalledWith(customOption);
|
||||
});
|
||||
it('Quick prompt badge color can be updated', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<QuickPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.change(getByTestId('euiColorPickerAnchor'), { target: { value: '#000' } });
|
||||
fireEvent.keyDown(getByTestId('euiColorPickerAnchor'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
const mutatableQuickPrompts = [...MOCK_QUICK_PROMPTS];
|
||||
const previousFirstElementOfTheArray = mutatableQuickPrompts.shift();
|
||||
|
||||
expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([
|
||||
{ ...previousFirstElementOfTheArray, color: '#000' },
|
||||
...mutatableQuickPrompts,
|
||||
]);
|
||||
});
|
||||
it('Updating the current prompt input updates the prompt', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<QuickPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.change(getByTestId('quick-prompt-prompt'), {
|
||||
target: { value: 'what does this do' },
|
||||
});
|
||||
const mutatableQuickPrompts = [...MOCK_QUICK_PROMPTS];
|
||||
const previousFirstElementOfTheArray = mutatableQuickPrompts.shift();
|
||||
|
||||
expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([
|
||||
{ ...previousFirstElementOfTheArray, prompt: 'what does this do' },
|
||||
...mutatableQuickPrompts,
|
||||
]);
|
||||
});
|
||||
it('Updating prompt contexts updates the categories of the prompt', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<QuickPromptSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-pc'));
|
||||
const mutatableQuickPrompts = [...MOCK_QUICK_PROMPTS];
|
||||
const previousFirstElementOfTheArray = mutatableQuickPrompts.shift();
|
||||
|
||||
expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([
|
||||
{ ...previousFirstElementOfTheArray, categories: ['alert', 'event'] },
|
||||
...mutatableQuickPrompts,
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -197,6 +197,7 @@ export const QuickPromptSettings: React.FC<Props> = React.memo<Props>(
|
|||
compressed
|
||||
disabled={selectedQuickPrompt == null}
|
||||
fullWidth
|
||||
data-test-subj="quick-prompt-prompt"
|
||||
onChange={handlePromptChange}
|
||||
placeholder={i18n.QUICK_PROMPT_PROMPT_PLACEHOLDER}
|
||||
value={prompt}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { alertConvo, customConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import {
|
||||
AssistantSettings,
|
||||
ANONYMIZATION_TAB,
|
||||
CONVERSATIONS_TAB,
|
||||
EVALUATION_TAB,
|
||||
KNOWLEDGE_BASE_TAB,
|
||||
QUICK_PROMPTS_TAB,
|
||||
SYSTEM_PROMPTS_TAB,
|
||||
} from './assistant_settings';
|
||||
import React from 'react';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
|
||||
|
||||
const mockConversations = {
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
};
|
||||
const saveSettings = jest.fn();
|
||||
|
||||
const mockValues = {
|
||||
conversationSettings: mockConversations,
|
||||
saveSettings,
|
||||
};
|
||||
|
||||
const setSelectedSettingsTab = jest.fn();
|
||||
const mockContext = {
|
||||
basePromptContexts: MOCK_QUICK_PROMPTS,
|
||||
setSelectedSettingsTab,
|
||||
http: {},
|
||||
modelEvaluatorEnabled: true,
|
||||
selectedSettingsTab: 'CONVERSATIONS_TAB',
|
||||
};
|
||||
const onClose = jest.fn();
|
||||
const onSave = jest.fn();
|
||||
const setSelectedConversationId = jest.fn();
|
||||
|
||||
const testProps = {
|
||||
defaultConnectorId: '123',
|
||||
defaultProvider: OpenAiProviderType.OpenAi,
|
||||
selectedConversation: welcomeConvo,
|
||||
onClose,
|
||||
onSave,
|
||||
setSelectedConversationId,
|
||||
};
|
||||
jest.mock('../../assistant_context');
|
||||
|
||||
jest.mock('.', () => {
|
||||
return {
|
||||
AnonymizationSettings: () => <span data-test-subj="ANONYMIZATION_TAB-tab" />,
|
||||
ConversationSettings: () => <span data-test-subj={`CONVERSATION_TAB-tab`} />,
|
||||
EvaluationSettings: () => <span data-test-subj="EVALUATION_TAB-tab" />,
|
||||
KnowledgeBaseSettings: () => <span data-test-subj="KNOWLEDGE_BASE_TAB-tab" />,
|
||||
QuickPromptSettings: () => <span data-test-subj="QUICK_PROMPTS_TAB-tab" />,
|
||||
SystemPromptSettings: () => <span data-test-subj="SYSTEM_PROMPTS_TAB-tab" />,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./use_settings_updater/use_settings_updater', () => {
|
||||
const original = jest.requireActual('./use_settings_updater/use_settings_updater');
|
||||
return {
|
||||
...original,
|
||||
useSettingsUpdater: jest.fn().mockImplementation(() => mockValues),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AssistantSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useAssistantContext as jest.Mock).mockImplementation(() => mockContext);
|
||||
});
|
||||
|
||||
it('saves changes', () => {
|
||||
const { getByTestId } = render(<AssistantSettings {...testProps} />);
|
||||
fireEvent.click(getByTestId('save-button'));
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(saveSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('saves changes and updates selected conversation when selected conversation has been deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantSettings {...testProps} selectedConversation={customConvo} />
|
||||
);
|
||||
fireEvent.click(getByTestId('save-button'));
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(setSelectedConversationId).toHaveBeenCalled();
|
||||
expect(saveSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('on close is called when settings modal closes', () => {
|
||||
const { getByTestId } = render(<AssistantSettings {...testProps} />);
|
||||
fireEvent.click(getByTestId('cancel-button'));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
ANONYMIZATION_TAB,
|
||||
CONVERSATIONS_TAB,
|
||||
EVALUATION_TAB,
|
||||
KNOWLEDGE_BASE_TAB,
|
||||
QUICK_PROMPTS_TAB,
|
||||
SYSTEM_PROMPTS_TAB,
|
||||
])('%s', (tab) => {
|
||||
it('Opens the tab on button click', () => {
|
||||
(useAssistantContext as jest.Mock).mockImplementation(() => ({
|
||||
...mockContext,
|
||||
selectedSettingsTab: tab === CONVERSATIONS_TAB ? ANONYMIZATION_TAB : CONVERSATIONS_TAB,
|
||||
}));
|
||||
const { getByTestId } = render(<AssistantSettings {...testProps} />);
|
||||
fireEvent.click(getByTestId(`${tab}-button`));
|
||||
expect(setSelectedSettingsTab).toHaveBeenCalledWith(tab);
|
||||
});
|
||||
it('renders with the correct tab open', () => {
|
||||
(useAssistantContext as jest.Mock).mockImplementation(() => ({
|
||||
...mockContext,
|
||||
selectedSettingsTab: tab,
|
||||
}));
|
||||
const { getByTestId } = render(<AssistantSettings {...testProps} />);
|
||||
expect(getByTestId(`${tab}-tab`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -27,14 +27,16 @@ import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/c
|
|||
import { Conversation, Prompt, QuickPrompt } from '../../..';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { AnonymizationSettings } from '../../data_anonymization/settings/anonymization_settings';
|
||||
import { QuickPromptSettings } from '../quick_prompts/quick_prompt_settings/quick_prompt_settings';
|
||||
import { SystemPromptSettings } from '../prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings';
|
||||
import { KnowledgeBaseSettings } from '../../knowledge_base/knowledge_base_settings/knowledge_base_settings';
|
||||
import { ConversationSettings } from '../conversations/conversation_settings/conversation_settings';
|
||||
import { TEST_IDS } from '../constants';
|
||||
import { useSettingsUpdater } from './use_settings_updater/use_settings_updater';
|
||||
import { EvaluationSettings } from './evaluation_settings/evaluation_settings';
|
||||
import {
|
||||
AnonymizationSettings,
|
||||
ConversationSettings,
|
||||
EvaluationSettings,
|
||||
KnowledgeBaseSettings,
|
||||
QuickPromptSettings,
|
||||
SystemPromptSettings,
|
||||
} from '.';
|
||||
|
||||
const StyledEuiModal = styled(EuiModal)`
|
||||
width: 800px;
|
||||
|
@ -81,6 +83,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
}) => {
|
||||
const { modelEvaluatorEnabled, http, selectedSettingsTab, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
|
||||
const {
|
||||
conversationSettings,
|
||||
defaultAllow,
|
||||
|
@ -171,6 +174,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
label={i18n.CONVERSATIONS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === CONVERSATIONS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(CONVERSATIONS_TAB)}
|
||||
data-test-subj={`${CONVERSATIONS_TAB}-button`}
|
||||
>
|
||||
<>
|
||||
<EuiIcon
|
||||
|
@ -197,6 +201,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
label={i18n.QUICK_PROMPTS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === QUICK_PROMPTS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(QUICK_PROMPTS_TAB)}
|
||||
data-test-subj={`${QUICK_PROMPTS_TAB}-button`}
|
||||
>
|
||||
<>
|
||||
<EuiIcon type="editorComment" size="xxl" />
|
||||
|
@ -217,6 +222,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
label={i18n.SYSTEM_PROMPTS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === SYSTEM_PROMPTS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(SYSTEM_PROMPTS_TAB)}
|
||||
data-test-subj={`${SYSTEM_PROMPTS_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="editorComment" size="xxl" />
|
||||
<EuiIcon
|
||||
|
@ -235,6 +241,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
label={i18n.ANONYMIZATION_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === ANONYMIZATION_TAB}
|
||||
onClick={() => setSelectedSettingsTab(ANONYMIZATION_TAB)}
|
||||
data-test-subj={`${ANONYMIZATION_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="eyeClosed" size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
|
@ -243,6 +250,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
label={i18n.KNOWLEDGE_BASE_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === KNOWLEDGE_BASE_TAB}
|
||||
onClick={() => setSelectedSettingsTab(KNOWLEDGE_BASE_TAB)}
|
||||
data-test-subj={`${KNOWLEDGE_BASE_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="notebookApp" size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
|
@ -252,6 +260,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
label={i18n.EVALUATION_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === EVALUATION_TAB}
|
||||
onClick={() => setSelectedSettingsTab(EVALUATION_TAB)}
|
||||
data-test-subj={`${EVALUATION_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="crossClusterReplicationApp" size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
|
@ -276,6 +285,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
setUpdatedConversationSettings={setUpdatedConversationSettings}
|
||||
allSystemPrompts={systemPromptSettings}
|
||||
selectedConversation={selectedConversation}
|
||||
isDisabled={selectedConversation == null}
|
||||
onSelectedConversationChange={onHandleSelectedConversationChange}
|
||||
http={http}
|
||||
/>
|
||||
|
@ -327,11 +337,17 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
padding: 4px;
|
||||
`}
|
||||
>
|
||||
<EuiButtonEmpty size="s" onClick={onClose}>
|
||||
<EuiButtonEmpty size="s" data-test-subj="cancel-button" onClick={onClose}>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton size="s" type="submit" onClick={handleSave} fill>
|
||||
<EuiButton
|
||||
size="s"
|
||||
type="submit"
|
||||
data-test-subj="save-button"
|
||||
onClick={handleSave}
|
||||
fill
|
||||
>
|
||||
{i18n.SAVE}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
|
||||
import { AssistantSettingsButton } from './assistant_settings_button';
|
||||
import { welcomeConvo } from '../../mock/conversation';
|
||||
import { CONVERSATIONS_TAB } from './assistant_settings';
|
||||
|
||||
const setIsSettingsModalVisible = jest.fn();
|
||||
const setSelectedConversationId = jest.fn();
|
||||
const testProps = {
|
||||
defaultConnectorId: '123',
|
||||
defaultProvider: OpenAiProviderType.OpenAi,
|
||||
isSettingsModalVisible: false,
|
||||
selectedConversation: welcomeConvo,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedConversationId,
|
||||
};
|
||||
const setSelectedSettingsTab = jest.fn();
|
||||
const mockUseAssistantContext = {
|
||||
setSelectedSettingsTab,
|
||||
};
|
||||
jest.mock('../../assistant_context', () => {
|
||||
const original = jest.requireActual('../../assistant_context');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useAssistantContext: () => mockUseAssistantContext,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./assistant_settings', () => ({
|
||||
...jest.requireActual('./assistant_settings'),
|
||||
// @ts-ignore
|
||||
AssistantSettings: ({ onClose, onSave }) => (
|
||||
<>
|
||||
<button type="button" data-test-subj="on-close" onClick={onClose} />
|
||||
<button type="button" data-test-subj="on-save" onClick={onSave} />
|
||||
</>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('AssistantSettingsButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Clicking the settings gear opens the conversations tab', () => {
|
||||
const { getByTestId } = render(<AssistantSettingsButton {...testProps} />);
|
||||
fireEvent.click(getByTestId('settings'));
|
||||
expect(setSelectedSettingsTab).toHaveBeenCalledWith(CONVERSATIONS_TAB);
|
||||
expect(setIsSettingsModalVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('Settings modal is visble and calls correct actions per click', () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantSettingsButton {...testProps} isSettingsModalVisible />
|
||||
);
|
||||
fireEvent.click(getByTestId('on-close'));
|
||||
expect(setIsSettingsModalVisible).toHaveBeenCalledWith(false);
|
||||
fireEvent.click(getByTestId('on-save'));
|
||||
expect(setIsSettingsModalVisible).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { usePerformEvaluation, UsePerformEvaluationParams } from './use_perform_evaluation';
|
||||
import { postEvaluation as _postEvaluation } from '../../api';
|
||||
import { useMutation as _useMutation } from '@tanstack/react-query';
|
||||
|
||||
const useMutationMock = _useMutation as jest.Mock;
|
||||
const postEvaluationMock = _postEvaluation as jest.Mock;
|
||||
|
||||
jest.mock('../../api', () => {
|
||||
const actual = jest.requireActual('../../api');
|
||||
return {
|
||||
...actual,
|
||||
postEvaluation: jest.fn((...args) => actual.postEvaluation(...args)),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useMutation: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn();
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
const statusResponse = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
const http = {
|
||||
fetch: jest.fn().mockResolvedValue(statusResponse),
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
const defaultProps = { http, toasts } as unknown as UsePerformEvaluationParams;
|
||||
|
||||
describe('usePerformEvaluation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should call api with undefined evalParams', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => usePerformEvaluation(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', {
|
||||
method: 'POST',
|
||||
body: '{"dataset":[],"evalPrompt":""}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
query: {
|
||||
agents: undefined,
|
||||
evalModel: undefined,
|
||||
evaluationType: undefined,
|
||||
models: undefined,
|
||||
outputIndex: undefined,
|
||||
},
|
||||
signal: undefined,
|
||||
});
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('Correctly passes and formats evalParams', async () => {
|
||||
useMutationMock.mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn({
|
||||
agents: ['d', 'c'],
|
||||
dataset: '["kewl"]',
|
||||
evalModel: ['b', 'a'],
|
||||
evalPrompt: 'evalPrompt',
|
||||
evaluationType: ['f', 'e'],
|
||||
models: ['h', 'g'],
|
||||
outputIndex: 'outputIndex',
|
||||
});
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
});
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => usePerformEvaluation(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', {
|
||||
method: 'POST',
|
||||
body: '{"dataset":["kewl"],"evalPrompt":"evalPrompt"}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
query: {
|
||||
agents: 'c,d',
|
||||
evalModel: 'a,b',
|
||||
evaluationType: 'e,f',
|
||||
models: 'g,h',
|
||||
outputIndex: 'outputIndex',
|
||||
},
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return evaluation response', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => usePerformEvaluation(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
await expect(result.current).resolves.toStrictEqual(statusResponse);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast when api throws error', async () => {
|
||||
postEvaluationMock.mockRejectedValue(new Error('this is an error'));
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => usePerformEvaluation(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { AnonymizationSettings } from '../../data_anonymization/settings/anonymization_settings';
|
||||
export { ConversationSettings } from '../conversations/conversation_settings/conversation_settings';
|
||||
export { EvaluationSettings } from './evaluation_settings/evaluation_settings';
|
||||
export { KnowledgeBaseSettings } from '../../knowledge_base/knowledge_base_settings';
|
||||
export { QuickPromptSettings } from '../quick_prompts/quick_prompt_settings/quick_prompt_settings';
|
||||
export { SystemPromptSettings } from '../prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings';
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation';
|
||||
import { useSettingsUpdater } from './use_settings_updater';
|
||||
import { Prompt } from '../../../..';
|
||||
import {
|
||||
defaultSystemPrompt,
|
||||
mockSuperheroSystemPrompt,
|
||||
mockSystemPrompt,
|
||||
} from '../../../mock/system_prompt';
|
||||
|
||||
const mockConversations = {
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
};
|
||||
|
||||
const mockSystemPrompts: Prompt[] = [mockSystemPrompt];
|
||||
const mockQuickPrompts: Prompt[] = [defaultSystemPrompt];
|
||||
|
||||
const initialDefaultAllow = ['allow1'];
|
||||
const initialDefaultAllowReplacement = ['replacement1'];
|
||||
|
||||
const setAllQuickPromptsMock = jest.fn();
|
||||
const setAllSystemPromptsMock = jest.fn();
|
||||
const setConversationsMock = jest.fn();
|
||||
const setDefaultAllowMock = jest.fn();
|
||||
const setDefaultAllowReplacementMock = jest.fn();
|
||||
const setKnowledgeBaseMock = jest.fn();
|
||||
|
||||
const mockValues = {
|
||||
conversations: mockConversations,
|
||||
allSystemPrompts: mockSystemPrompts,
|
||||
allQuickPrompts: mockQuickPrompts,
|
||||
defaultAllow: initialDefaultAllow,
|
||||
defaultAllowReplacement: initialDefaultAllowReplacement,
|
||||
knowledgeBase: {
|
||||
assistantLangChain: true,
|
||||
},
|
||||
setAllQuickPrompts: setAllQuickPromptsMock,
|
||||
setConversations: setConversationsMock,
|
||||
setAllSystemPrompts: setAllSystemPromptsMock,
|
||||
setDefaultAllow: setDefaultAllowMock,
|
||||
setDefaultAllowReplacement: setDefaultAllowReplacementMock,
|
||||
setKnowledgeBase: setKnowledgeBaseMock,
|
||||
};
|
||||
|
||||
const updatedValues = {
|
||||
conversations: { [customConvo.id]: customConvo },
|
||||
allSystemPrompts: [mockSuperheroSystemPrompt],
|
||||
allQuickPrompts: [{ title: 'Prompt 2', prompt: 'Prompt 2', color: 'red' }],
|
||||
defaultAllow: ['allow2'],
|
||||
defaultAllowReplacement: ['replacement2'],
|
||||
knowledgeBase: {
|
||||
assistantLangChain: false,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../../../assistant_context', () => {
|
||||
const original = jest.requireActual('../../../assistant_context');
|
||||
return {
|
||||
...original,
|
||||
useAssistantContext: jest.fn().mockImplementation(() => mockValues),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useSettingsUpdater', () => {
|
||||
it('should set all state variables to their initial values when resetSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater());
|
||||
await waitForNextUpdate();
|
||||
const {
|
||||
setUpdatedConversationSettings,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
resetSettings,
|
||||
} = result.current;
|
||||
|
||||
setUpdatedConversationSettings(updatedValues.conversations);
|
||||
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
|
||||
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
|
||||
setUpdatedDefaultAllow(updatedValues.defaultAllow);
|
||||
setUpdatedDefaultAllowReplacement(updatedValues.defaultAllowReplacement);
|
||||
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
|
||||
|
||||
expect(result.current.conversationSettings).toEqual(updatedValues.conversations);
|
||||
expect(result.current.quickPromptSettings).toEqual(updatedValues.allQuickPrompts);
|
||||
expect(result.current.systemPromptSettings).toEqual(updatedValues.allSystemPrompts);
|
||||
expect(result.current.defaultAllow).toEqual(updatedValues.defaultAllow);
|
||||
expect(result.current.defaultAllowReplacement).toEqual(updatedValues.defaultAllowReplacement);
|
||||
expect(result.current.knowledgeBase).toEqual(updatedValues.knowledgeBase);
|
||||
|
||||
resetSettings();
|
||||
|
||||
expect(result.current.conversationSettings).toEqual(mockValues.conversations);
|
||||
expect(result.current.quickPromptSettings).toEqual(mockValues.allQuickPrompts);
|
||||
expect(result.current.systemPromptSettings).toEqual(mockValues.allSystemPrompts);
|
||||
expect(result.current.defaultAllow).toEqual(mockValues.defaultAllow);
|
||||
expect(result.current.defaultAllowReplacement).toEqual(mockValues.defaultAllowReplacement);
|
||||
expect(result.current.knowledgeBase).toEqual(mockValues.knowledgeBase);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update all state variables to their updated values when saveSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater());
|
||||
await waitForNextUpdate();
|
||||
const {
|
||||
setUpdatedConversationSettings,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
} = result.current;
|
||||
|
||||
setUpdatedConversationSettings(updatedValues.conversations);
|
||||
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
|
||||
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
|
||||
setUpdatedDefaultAllow(updatedValues.defaultAllow);
|
||||
setUpdatedDefaultAllowReplacement(updatedValues.defaultAllowReplacement);
|
||||
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
|
||||
|
||||
result.current.saveSettings();
|
||||
|
||||
expect(setAllQuickPromptsMock).toHaveBeenCalledWith(updatedValues.allQuickPrompts);
|
||||
expect(setAllSystemPromptsMock).toHaveBeenCalledWith(updatedValues.allSystemPrompts);
|
||||
expect(setConversationsMock).toHaveBeenCalledWith(updatedValues.conversations);
|
||||
expect(setDefaultAllowMock).toHaveBeenCalledWith(updatedValues.defaultAllow);
|
||||
expect(setDefaultAllowReplacementMock).toHaveBeenCalledWith(
|
||||
updatedValues.defaultAllowReplacement
|
||||
);
|
||||
expect(setKnowledgeBaseMock).toHaveBeenCalledWith(updatedValues.knowledgeBase);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useConversation } from '.';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { alertConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import React from 'react';
|
||||
import { ConversationRole } from '../../assistant_context/types';
|
||||
const message = {
|
||||
content: 'You are a robot',
|
||||
role: 'user' as ConversationRole,
|
||||
timestamp: '10/04/2023, 1:00:36 PM',
|
||||
};
|
||||
|
||||
const mockConvo = {
|
||||
id: 'new-convo',
|
||||
messages: [message],
|
||||
apiConfig: { defaultSystemPromptId: 'default-system-prompt' },
|
||||
theme: {
|
||||
title: 'Elastic AI Assistant',
|
||||
titleIcon: 'logoSecurity',
|
||||
assistant: { name: 'Assistant', icon: 'logoSecurity' },
|
||||
system: { icon: 'logoElastic' },
|
||||
user: {},
|
||||
},
|
||||
};
|
||||
|
||||
describe('useConversation', () => {
|
||||
it('should append a message to an existing conversation when called with valid conversationId and message', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const appendResult = result.current.appendMessage({
|
||||
conversationId: welcomeConvo.id,
|
||||
message,
|
||||
});
|
||||
expect(appendResult).toHaveLength(3);
|
||||
expect(appendResult[2]).toEqual(message);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new conversation when called with valid conversationId and message', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const createResult = result.current.createConversation({
|
||||
conversationId: mockConvo.id,
|
||||
messages: mockConvo.messages,
|
||||
});
|
||||
|
||||
expect(createResult).toEqual(mockConvo);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete an existing conversation when called with valid conversationId', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[mockConvo.id]: mockConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const deleteResult = result.current.deleteConversation('new-convo');
|
||||
|
||||
expect(deleteResult).toEqual(mockConvo);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the apiConfig for an existing conversation when called with a valid conversationId and apiConfig', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.setApiConfig({
|
||||
conversationId: welcomeConvo.id,
|
||||
apiConfig: mockConvo.apiConfig,
|
||||
});
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[welcomeConvo.id]: { ...welcomeConvo, apiConfig: mockConvo.apiConfig },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('overwrites a conversation', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.setConversation({
|
||||
conversation: {
|
||||
...mockConvo,
|
||||
id: welcomeConvo.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: { ...mockConvo, id: welcomeConvo.id },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('clears a conversation', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.clearConversation(welcomeConvo.id);
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
},
|
||||
messages: [],
|
||||
replacements: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('appends replacements', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.appendReplacements({
|
||||
conversationId: welcomeConvo.id,
|
||||
replacements: {
|
||||
'1.0.0.721': '127.0.0.1',
|
||||
'1.0.0.01': '10.0.0.1',
|
||||
'tsoh-tset': 'test-host',
|
||||
},
|
||||
});
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
replacements: {
|
||||
'1.0.0.721': '127.0.0.1',
|
||||
'1.0.0.01': '10.0.0.1',
|
||||
'tsoh-tset': 'test-host',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ActionType } from '@kbn/actions-plugin/common';
|
||||
import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import {
|
||||
ActionConnector,
|
||||
ActionTypeRegistryContract,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { ActionTypeSelectorModal } from '../connector_selector_inline/action_type_selector_modal';
|
||||
interface Props {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
actionTypes?: ActionType[];
|
||||
onClose: () => void;
|
||||
onSaveConnector: (connector: ActionConnector) => void;
|
||||
onSelectActionType: (actionType: ActionType) => void;
|
||||
selectedActionType: ActionType | null;
|
||||
}
|
||||
export const AddConnectorModal: React.FC<Props> = React.memo(
|
||||
({
|
||||
actionTypeRegistry,
|
||||
actionTypes,
|
||||
onClose,
|
||||
onSaveConnector,
|
||||
onSelectActionType,
|
||||
selectedActionType,
|
||||
}) =>
|
||||
!selectedActionType ? (
|
||||
<ActionTypeSelectorModal
|
||||
actionTypes={actionTypes}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onClose={onClose}
|
||||
onSelect={onSelectActionType}
|
||||
/>
|
||||
) : (
|
||||
<ConnectorAddModal
|
||||
actionType={selectedActionType}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onClose={onClose}
|
||||
postSaveEventHandler={onSaveConnector}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
AddConnectorModal.displayName = 'AddConnectorModal';
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ConnectorSelector } from '.';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { mockActionTypes, mockConnectors } from '../../mock/connectors';
|
||||
|
||||
const onConnectorSelectionChange = jest.fn();
|
||||
const setIsOpen = jest.fn();
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
onConnectorSelectionChange,
|
||||
selectedConnectorId: 'connectorId',
|
||||
setIsOpen,
|
||||
};
|
||||
|
||||
const connectorTwo = mockConnectors[1];
|
||||
|
||||
const mockRefetchConnectors = jest.fn();
|
||||
jest.mock('../use_load_connectors', () => ({
|
||||
useLoadConnectors: jest.fn(() => {
|
||||
return {
|
||||
data: mockConnectors,
|
||||
error: null,
|
||||
isSuccess: true,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
refetch: mockRefetchConnectors,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../use_load_action_types', () => ({
|
||||
useLoadActionTypes: jest.fn(() => {
|
||||
return {
|
||||
data: mockActionTypes,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const newConnector = { actionTypeId: '.gen-ai', name: 'cool name' };
|
||||
|
||||
jest.mock('../add_connector_modal', () => ({
|
||||
// @ts-ignore
|
||||
AddConnectorModal: ({ onSaveConnector }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="modal-mock"
|
||||
onClick={() => onSaveConnector(newConnector)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('Connector selector', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('renders empty selection if no selected connector is provided', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConnectorSelector {...defaultProps} selectedConnectorId={undefined} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId('connector-selector')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-selector')).toHaveTextContent('');
|
||||
});
|
||||
it('renders with provided selected connector', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConnectorSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId('connector-selector')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-selector')).toHaveTextContent('Captain Connector');
|
||||
});
|
||||
it('Calls onConnectorSelectionChange with new selection', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConnectorSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId('connector-selector')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('connector-selector'));
|
||||
fireEvent.click(getByTestId(connectorTwo.id));
|
||||
expect(onConnectorSelectionChange).toHaveBeenCalledWith({
|
||||
...connectorTwo,
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
});
|
||||
});
|
||||
it('Calls onConnectorSelectionChange once new connector is saved', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConnectorSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('connector-selector'));
|
||||
fireEvent.click(getByTestId('addNewConnectorButton'));
|
||||
|
||||
fireEvent.click(getByTestId('modal-mock'));
|
||||
|
||||
expect(onConnectorSelectionChange).toHaveBeenCalledWith({
|
||||
...newConnector,
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
});
|
||||
expect(mockRefetchConnectors).toHaveBeenCalled();
|
||||
expect(setIsOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
|
@ -10,13 +10,12 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
|
||||
import { ActionConnector, ActionType } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import { ActionTypeSelectorModal } from '../connector_selector_inline/action_type_selector_modal';
|
||||
import { useLoadConnectors } from '../use_load_connectors';
|
||||
import * as i18n from '../translations';
|
||||
import { useLoadActionTypes } from '../use_load_action_types';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { getActionTypeTitle, getGenAiConfig } from '../helpers';
|
||||
import { AddConnectorModal } from '../add_connector_modal';
|
||||
|
||||
export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR';
|
||||
|
||||
|
@ -105,6 +104,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
: connectorTypeTitle;
|
||||
return {
|
||||
value: connector.id,
|
||||
'data-test-subj': connector.id,
|
||||
inputDisplay: displayFancy ? displayFancy(connector.name) : connector.name,
|
||||
dropdownDisplay: (
|
||||
<React.Fragment key={connector.id}>
|
||||
|
@ -171,6 +171,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
<EuiSuperSelect
|
||||
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
|
||||
compressed={true}
|
||||
data-test-subj="connector-selector"
|
||||
disabled={localIsDisabled}
|
||||
hasDividers={true}
|
||||
isLoading={isLoading}
|
||||
|
@ -179,20 +180,14 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
options={allConnectorOptions}
|
||||
valueOfSelected={selectedConnectorId ?? ''}
|
||||
/>
|
||||
{isConnectorModalVisible && !selectedActionType && (
|
||||
<ActionTypeSelectorModal
|
||||
{isConnectorModalVisible && (
|
||||
<AddConnectorModal
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionTypes={actionTypes}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onClose={() => setIsConnectorModalVisible(false)}
|
||||
onSelect={(actionType: ActionType) => setSelectedActionType(actionType)}
|
||||
/>
|
||||
)}
|
||||
{isConnectorModalVisible && selectedActionType && (
|
||||
<ConnectorAddModal
|
||||
actionType={selectedActionType}
|
||||
onClose={cleanupAndCloseModal}
|
||||
postSaveEventHandler={onSaveConnector}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onSaveConnector={onSaveConnector}
|
||||
onSelectActionType={(actionType: ActionType) => setSelectedActionType(actionType)}
|
||||
selectedActionType={selectedActionType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { ActionTypeSelectorModal } from './action_type_selector_modal';
|
||||
import { ActionType } from '@kbn/actions-plugin/common';
|
||||
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
|
||||
const actionTypes = [
|
||||
{
|
||||
id: '123',
|
||||
name: 'Gen AI',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isSystemActionType: true,
|
||||
supportedFeatureIds: ['generativeAI'],
|
||||
} as ActionType,
|
||||
{
|
||||
id: '456',
|
||||
name: 'Another one',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isSystemActionType: true,
|
||||
supportedFeatureIds: ['generativeAI'],
|
||||
} as ActionType,
|
||||
];
|
||||
const actionTypeRegistry = {
|
||||
...actionTypeRegistryMock.create(),
|
||||
get: jest.fn().mockReturnValue({ iconClass: 'icon-class' }),
|
||||
};
|
||||
const onClose = jest.fn();
|
||||
const onSelect = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
actionTypes,
|
||||
actionTypeRegistry,
|
||||
onClose,
|
||||
onSelect,
|
||||
};
|
||||
|
||||
describe('ActionTypeSelectorModal', () => {
|
||||
it('should render modal with header and body when actionTypes is not empty', () => {
|
||||
const { getByTestId, getAllByTestId } = render(<ActionTypeSelectorModal {...defaultProps} />);
|
||||
|
||||
expect(getByTestId('action-type-selector-modal')).toBeInTheDocument();
|
||||
|
||||
expect(getAllByTestId('action-option')).toHaveLength(actionTypes.length);
|
||||
});
|
||||
|
||||
it('should render null when actionTypes is undefined', () => {
|
||||
const { actionTypes: _, ...rest } = defaultProps;
|
||||
const { queryByTestId } = render(<ActionTypeSelectorModal {...rest} />);
|
||||
|
||||
expect(queryByTestId('action-type-selector-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null when actionTypes is an empty array', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ActionTypeSelectorModal {...defaultProps} actionTypes={[]} />
|
||||
);
|
||||
|
||||
expect(queryByTestId('action-type-selector-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSelect with actionType when clicked', () => {
|
||||
const { getByTestId } = render(<ActionTypeSelectorModal {...defaultProps} />);
|
||||
|
||||
fireEvent.click(getByTestId(`action-option-${actionTypes[1].name}`));
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(actionTypes[1]);
|
||||
});
|
||||
});
|
|
@ -34,7 +34,7 @@ export const ActionTypeSelectorModal = ({
|
|||
onSelect,
|
||||
}: Props) =>
|
||||
actionTypes && actionTypes.length > 0 ? (
|
||||
<EuiModal onClose={onClose}>
|
||||
<EuiModal onClose={onClose} data-test-subj="action-type-selector-modal">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.INLINE_CONNECTOR_PLACEHOLDER}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
@ -44,11 +44,12 @@ export const ActionTypeSelectorModal = ({
|
|||
{actionTypes.map((actionType: ActionType) => {
|
||||
const fullAction = actionTypeRegistry.get(actionType.id);
|
||||
return (
|
||||
<EuiFlexItem key={actionType.id} grow={false}>
|
||||
<EuiFlexItem data-test-subj="action-option" key={actionType.id} grow={false}>
|
||||
<EuiKeyPadMenuItem
|
||||
key={actionType.id}
|
||||
isDisabled={!actionType.enabled}
|
||||
label={actionType.name}
|
||||
data-test-subj={`action-option-${actionType.name}`}
|
||||
onClick={() => onSelect(actionType)}
|
||||
>
|
||||
<EuiIcon size="xl" type={fullAction.iconClass} />
|
||||
|
|
|
@ -6,14 +6,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { mockConnectors } from '../../mock/connectors';
|
||||
import { ConnectorSelectorInline } from './connector_selector_inline';
|
||||
import * as i18n from '../translations';
|
||||
import { Conversation } from '../../..';
|
||||
import { useLoadConnectors } from '../use_load_connectors';
|
||||
|
||||
const setApiConfig = jest.fn();
|
||||
const mockConversation = {
|
||||
setApiConfig,
|
||||
};
|
||||
|
||||
jest.mock('../../assistant/use_conversation', () => ({
|
||||
useConversation: () => mockConversation,
|
||||
}));
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/constants', () => ({
|
||||
loadActionTypes: jest.fn(() => {
|
||||
return Promise.resolve([
|
||||
|
@ -39,18 +48,6 @@ jest.mock('../use_load_connectors', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const mockConnectors = [
|
||||
{
|
||||
id: 'connectorId',
|
||||
name: 'Captain Connector',
|
||||
isMissingSecrets: false,
|
||||
actionTypeId: '.gen-ai',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
(useLoadConnectors as jest.Mock).mockReturnValue({
|
||||
data: mockConnectors,
|
||||
error: null,
|
||||
|
@ -58,6 +55,9 @@ const mockConnectors = [
|
|||
});
|
||||
|
||||
describe('ConnectorSelectorInline', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('renders empty view if no selected conversation is provided', () => {
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
|
@ -88,22 +88,74 @@ describe('ConnectorSelectorInline', () => {
|
|||
);
|
||||
expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders selected connector if selected selectedConnectorId is in list of connectors', () => {
|
||||
it('Clicking add connector button opens the connector selector', () => {
|
||||
const conversation: Conversation = {
|
||||
id: 'conversation_id',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
};
|
||||
const { getByText } = render(
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConnectorSelectorInline
|
||||
isDisabled={false}
|
||||
selectedConnectorId={mockConnectors[0].id}
|
||||
selectedConnectorId={'missing-connector-id'}
|
||||
selectedConversation={conversation}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByText(mockConnectors[0].name)).toBeInTheDocument();
|
||||
expect(queryByTestId('connector-selector')).not.toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
|
||||
expect(getByTestId('connector-selector')).toBeInTheDocument();
|
||||
});
|
||||
it('On connector change, update conversation API config', () => {
|
||||
const connectorTwo = mockConnectors[1];
|
||||
const conversation: Conversation = {
|
||||
id: 'conversation_id',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
};
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConnectorSelectorInline
|
||||
isDisabled={false}
|
||||
selectedConnectorId={'missing-connector-id'}
|
||||
selectedConversation={conversation}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
|
||||
fireEvent.click(getByTestId('connector-selector'));
|
||||
fireEvent.click(getByTestId(connectorTwo.id));
|
||||
expect(queryByTestId('connector-selector')).not.toBeInTheDocument();
|
||||
expect(setApiConfig).toHaveBeenCalledWith({
|
||||
apiConfig: {
|
||||
connectorId: connectorTwo.id,
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
model: undefined,
|
||||
provider: 'OpenAI',
|
||||
},
|
||||
conversationId: 'conversation_id',
|
||||
});
|
||||
});
|
||||
it('On connector change to add new connector, onchange event does nothing', () => {
|
||||
const conversation: Conversation = {
|
||||
id: 'conversation_id',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
};
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConnectorSelectorInline
|
||||
isDisabled={false}
|
||||
selectedConnectorId={'missing-connector-id'}
|
||||
selectedConversation={conversation}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
|
||||
fireEvent.click(getByTestId('connector-selector'));
|
||||
fireEvent.click(getByTestId('addNewConnectorButton'));
|
||||
expect(getByTestId('connector-selector')).toBeInTheDocument();
|
||||
expect(setApiConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useConnectorSetup } from '.';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { alertConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { EuiCommentList } from '@elastic/eui';
|
||||
|
||||
const onSetupComplete = jest.fn();
|
||||
const defaultProps = {
|
||||
conversation: welcomeConvo,
|
||||
onSetupComplete,
|
||||
};
|
||||
const newConnector = { actionTypeId: '.gen-ai', name: 'cool name' };
|
||||
jest.mock('../add_connector_modal', () => ({
|
||||
// @ts-ignore
|
||||
AddConnectorModal: ({ onSaveConnector }) => (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="modal-mock"
|
||||
onClick={() => onSaveConnector(newConnector)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
|
||||
const setConversation = jest.fn();
|
||||
const setApiConfig = jest.fn();
|
||||
const mockConversation = {
|
||||
appendMessage: jest.fn(),
|
||||
appendReplacements: jest.fn(),
|
||||
clearConversation: jest.fn(),
|
||||
createConversation: jest.fn(),
|
||||
deleteConversation: jest.fn(),
|
||||
setApiConfig,
|
||||
setConversation,
|
||||
};
|
||||
|
||||
jest.mock('../../assistant/use_conversation', () => ({
|
||||
useConversation: () => mockConversation,
|
||||
}));
|
||||
|
||||
jest.spyOn(global, 'clearTimeout');
|
||||
describe('useConnectorSetup', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should render comments and prompts', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
expect(
|
||||
result.current.comments.map((c) => ({ username: c.username, timestamp: c.timestamp }))
|
||||
).toEqual([
|
||||
{
|
||||
username: 'You',
|
||||
timestamp: 'at: 7/17/2023, 1:00:36 PM',
|
||||
},
|
||||
{
|
||||
username: 'Elastic AI Assistant',
|
||||
timestamp: 'at: 7/17/2023, 1:00:40 PM',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.current.prompt.props['data-test-subj']).toEqual('prompt');
|
||||
});
|
||||
});
|
||||
it('should set api config for each conversation when new connector is saved', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
const { getByTestId, queryByTestId, rerender } = render(result.current.prompt, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(getByTestId('connectorButton')).toBeInTheDocument();
|
||||
expect(queryByTestId('skip-setup-button')).not.toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('connectorButton'));
|
||||
|
||||
rerender(result.current.prompt);
|
||||
fireEvent.click(getByTestId('modal-mock'));
|
||||
expect(setApiConfig).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show skip button if message has presentation data', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() =>
|
||||
useConnectorSetup({
|
||||
...defaultProps,
|
||||
conversation: {
|
||||
...defaultProps.conversation,
|
||||
messages: [
|
||||
{
|
||||
...defaultProps.conversation.messages[0],
|
||||
presentation: {
|
||||
delay: 0,
|
||||
stream: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
const { getByTestId, queryByTestId } = render(result.current.prompt, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(getByTestId('skip-setup-button')).toBeInTheDocument();
|
||||
expect(queryByTestId('connectorButton')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should call onSetupComplete and setConversation when onHandleMessageStreamingComplete', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
render(<EuiCommentList comments={result.current.comments} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(clearTimeout).toHaveBeenCalled();
|
||||
expect(onSetupComplete).toHaveBeenCalled();
|
||||
expect(setConversation).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,13 +10,10 @@ import type { EuiCommentProps } from '@elastic/eui';
|
|||
import { EuiAvatar, EuiBadge, EuiMarkdownFormat, EuiText, EuiTextAlign } from '@elastic/eui';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
ActionConnector,
|
||||
ConnectorAddModal,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
|
||||
import { ActionType } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { ActionTypeSelectorModal } from '../connector_selector_inline/action_type_selector_modal';
|
||||
import { AddConnectorModal } from '../add_connector_modal';
|
||||
import { WELCOME_CONVERSATION } from '../../assistant/use_conversation/sample_conversations';
|
||||
import { Conversation, Message } from '../../..';
|
||||
import { useLoadActionTypes } from '../use_load_action_types';
|
||||
|
@ -204,7 +201,7 @@ export const useConnectorSetup = ({
|
|||
conversationId: conversation.id,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Connector setup complete!',
|
||||
content: i18n.CONNECTOR_SETUP_COMPLETE,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
},
|
||||
});
|
||||
|
@ -233,6 +230,7 @@ export const useConnectorSetup = ({
|
|||
<EuiTextAlign textAlign="center">
|
||||
<EuiBadge
|
||||
color="hollow"
|
||||
data-test-subj="skip-setup-button"
|
||||
onClick={handleSkipSetup}
|
||||
onClickAriaLabel={i18n.CONNECTOR_SETUP_SKIP}
|
||||
>
|
||||
|
@ -241,20 +239,14 @@ export const useConnectorSetup = ({
|
|||
</EuiTextAlign>
|
||||
</SkipEuiText>
|
||||
)}
|
||||
{isConnectorModalVisible && !selectedActionType && (
|
||||
<ActionTypeSelectorModal
|
||||
{isConnectorModalVisible && (
|
||||
<AddConnectorModal
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionTypes={actionTypes}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onClose={() => setIsConnectorModalVisible(false)}
|
||||
onSelect={(actionType: ActionType) => setSelectedActionType(actionType)}
|
||||
/>
|
||||
)}
|
||||
{isConnectorModalVisible && selectedActionType && (
|
||||
<ConnectorAddModal
|
||||
actionType={selectedActionType}
|
||||
onClose={() => setIsConnectorModalVisible(false)}
|
||||
postSaveEventHandler={onSaveConnector}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onSaveConnector={onSaveConnector}
|
||||
onSelectActionType={(actionType: ActionType) => setSelectedActionType(actionType)}
|
||||
selectedActionType={selectedActionType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MODEL_GPT_3_5_TURBO, MODEL_GPT_4, ModelSelector } from './model_selector';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
describe('ModelSelector', () => {
|
||||
it('should render with correct default selection', () => {
|
||||
const onModelSelectionChange = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<ModelSelector onModelSelectionChange={onModelSelectionChange} />
|
||||
);
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MODEL_GPT_3_5_TURBO);
|
||||
});
|
||||
it('should call onModelSelectionChange when custom option', () => {
|
||||
const onModelSelectionChange = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<ModelSelector onModelSelectionChange={onModelSelectionChange} />
|
||||
);
|
||||
const comboBox = getByTestId('comboBoxSearchInput');
|
||||
const customOption = 'Custom option';
|
||||
fireEvent.change(comboBox, { target: { value: customOption } });
|
||||
fireEvent.keyDown(comboBox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(onModelSelectionChange).toHaveBeenCalledWith(customOption);
|
||||
});
|
||||
it('should call onModelSelectionChange when existing option is selected', () => {
|
||||
const onModelSelectionChange = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<ModelSelector onModelSelectionChange={onModelSelectionChange} />
|
||||
);
|
||||
const comboBox = getByTestId('comboBoxSearchInput');
|
||||
fireEvent.click(comboBox);
|
||||
fireEvent.click(getByTestId(MODEL_GPT_4));
|
||||
expect(onModelSelectionChange).toHaveBeenCalledWith(MODEL_GPT_4);
|
||||
});
|
||||
});
|
|
@ -29,9 +29,7 @@ export const ModelSelector: React.FC<Props> = React.memo(
|
|||
({ models = DEFAULT_MODELS, onModelSelectionChange, selectedModel = DEFAULT_MODELS[0] }) => {
|
||||
// Form options
|
||||
const [options, setOptions] = useState<EuiComboBoxOptionOption[]>(
|
||||
models.map((model) => ({
|
||||
label: model,
|
||||
}))
|
||||
models.map((model) => ({ 'data-test-subj': model, label: model }))
|
||||
);
|
||||
const selectedOptions = useMemo<EuiComboBoxOptionOption[]>(() => {
|
||||
return selectedModel ? [{ label: selectedModel }] : [];
|
||||
|
@ -92,6 +90,7 @@ export const ModelSelector: React.FC<Props> = React.memo(
|
|||
<EuiComboBox
|
||||
aria-label={i18n.HELP_LABEL}
|
||||
compressed
|
||||
data-test-subj="model-selector"
|
||||
isClearable={false}
|
||||
placeholder={i18n.PLACEHOLDER_TEXT}
|
||||
customOptionText={`${i18n.CUSTOM_OPTION_TEXT} {searchValue}`}
|
||||
|
|
|
@ -122,6 +122,13 @@ export const CONNECTOR_SETUP_SKIP = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CONNECTOR_SETUP_COMPLETE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.connectors.setup.complete',
|
||||
{
|
||||
defaultMessage: 'Connector setup complete!',
|
||||
}
|
||||
);
|
||||
|
||||
export const MISSING_CONNECTOR_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutTitle',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useLoadActionTypes, Props } from '.';
|
||||
import { mockActionTypes } from '../../mock/connectors';
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn();
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
const http = {
|
||||
get: jest.fn().mockResolvedValue(mockActionTypes),
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
const defaultProps = { http, toasts } as unknown as Props;
|
||||
describe('useLoadActionTypes', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should call api to load action types', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useLoadActionTypes(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.get).toHaveBeenCalledWith('/api/actions/connector_types', {
|
||||
query: { feature_id: 'generativeAI' },
|
||||
});
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return sorted action types', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLoadActionTypes(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
await expect(result.current).resolves.toStrictEqual(
|
||||
mockActionTypes.sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should display error toast when api throws error', async () => {
|
||||
await act(async () => {
|
||||
const mockHttp = {
|
||||
get: jest.fn().mockRejectedValue(new Error('this is an error')),
|
||||
} as unknown as Props['http'];
|
||||
const { waitForNextUpdate } = renderHook(() =>
|
||||
useLoadActionTypes({ ...defaultProps, http: mockHttp })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -22,7 +22,7 @@ import * as i18n from '../translations';
|
|||
* Cache expiration in ms -- 1 minute, useful if connector is deleted/access removed
|
||||
*/
|
||||
const STALE_TIME = 1000 * 60;
|
||||
const QUERY_KEY = ['elastic-assistant, load-action-types'];
|
||||
export const QUERY_KEY = ['elastic-assistant, load-action-types'];
|
||||
|
||||
export interface Props {
|
||||
http: HttpSetup;
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useLoadConnectors, Props } from '.';
|
||||
import { mockConnectors } from '../../mock/connectors';
|
||||
|
||||
const mockConnectorsAndExtras = [
|
||||
...mockConnectors,
|
||||
{
|
||||
...mockConnectors[0],
|
||||
id: 'connector-missing-secrets',
|
||||
name: 'Connector Missing Secrets',
|
||||
isMissingSecrets: true,
|
||||
},
|
||||
{
|
||||
...mockConnectors[0],
|
||||
|
||||
id: 'connector-wrong-action-type',
|
||||
name: 'Connector Wrong Action Type',
|
||||
isMissingSecrets: true,
|
||||
actionTypeId: '.d3',
|
||||
},
|
||||
];
|
||||
|
||||
const connectorsApiResponse = mockConnectorsAndExtras.map((c) => ({
|
||||
...c,
|
||||
connector_type_id: c.actionTypeId,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
referenced_by_count: 0,
|
||||
is_missing_secrets: c.isMissingSecrets,
|
||||
is_system_action: false,
|
||||
}));
|
||||
|
||||
const loadConnectorsResult = mockConnectors.map((c) => ({
|
||||
...c,
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 0,
|
||||
isSystemAction: false,
|
||||
}));
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn();
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
const http = {
|
||||
get: jest.fn().mockResolvedValue(connectorsApiResponse),
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
const defaultProps = { http, toasts } as unknown as Props;
|
||||
describe('useLoadConnectors', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should call api to load action types', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useLoadConnectors(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.get).toHaveBeenCalledWith('/api/actions/connectors');
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return sorted action types, removing isMissingSecrets and wrong action type ids', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLoadConnectors(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
await expect(result.current).resolves.toStrictEqual(loadConnectorsResult);
|
||||
});
|
||||
});
|
||||
it('should display error toast when api throws error', async () => {
|
||||
await act(async () => {
|
||||
const mockHttp = {
|
||||
get: jest.fn().mockRejectedValue(new Error('this is an error')),
|
||||
} as unknown as Props['http'];
|
||||
const { waitForNextUpdate } = renderHook(() =>
|
||||
useLoadConnectors({ ...defaultProps, http: mockHttp })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -42,7 +42,6 @@ export const useLoadConnectors = ({
|
|||
{
|
||||
retry: false,
|
||||
keepPreviousData: true,
|
||||
// staleTime: STALE_TIME,
|
||||
onError: (error: ServerError) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
toasts?.addError(
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { KnowledgeBaseSettings } from './knowledge_base_settings';
|
||||
import { TestProviders } from '../mock/test_providers/test_providers';
|
||||
import { useKnowledgeBaseStatus } from './use_knowledge_base_status';
|
||||
|
||||
const setUpdatedKnowledgeBaseSettings = jest.fn();
|
||||
const defaultProps = {
|
||||
knowledgeBase: {
|
||||
assistantLangChain: true,
|
||||
},
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
};
|
||||
const mockDelete = jest.fn();
|
||||
jest.mock('./use_delete_knowledge_base', () => ({
|
||||
useDeleteKnowledgeBase: jest.fn(() => {
|
||||
return {
|
||||
mutate: mockDelete,
|
||||
isLoading: false,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockSetup = jest.fn();
|
||||
jest.mock('./use_setup_knowledge_base', () => ({
|
||||
useSetupKnowledgeBase: jest.fn(() => {
|
||||
return {
|
||||
mutate: mockSetup,
|
||||
isLoading: false,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./use_knowledge_base_status', () => ({
|
||||
useKnowledgeBaseStatus: jest.fn(() => {
|
||||
return {
|
||||
data: {
|
||||
elser_exists: true,
|
||||
esql_exists: true,
|
||||
index_exists: true,
|
||||
pipeline_exists: true,
|
||||
},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Knowledge base settings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Shows correct description when esql is installed', () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<KnowledgeBaseSettings {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('esql-installed')).toBeInTheDocument();
|
||||
expect(queryByTestId('install-esql')).not.toBeInTheDocument();
|
||||
expect(getByTestId('esqlEnableButton')).toBeInTheDocument();
|
||||
});
|
||||
it('On click enable esql button, esql is enabled', () => {
|
||||
(useKnowledgeBaseStatus as jest.Mock).mockImplementation(() => {
|
||||
return {
|
||||
data: {
|
||||
elser_exists: true,
|
||||
esql_exists: false,
|
||||
index_exists: true,
|
||||
pipeline_exists: true,
|
||||
},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
};
|
||||
});
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<KnowledgeBaseSettings {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId('esql-installed')).not.toBeInTheDocument();
|
||||
expect(getByTestId('install-esql')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('esqlEnableButton'));
|
||||
expect(mockSetup).toHaveBeenCalledWith('esql');
|
||||
});
|
||||
it('On disable lang chain, set assistantLangChain to false', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<KnowledgeBaseSettings {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('assistantLangChainSwitch'));
|
||||
expect(setUpdatedKnowledgeBaseSettings).toHaveBeenCalledWith({
|
||||
assistantLangChain: false,
|
||||
});
|
||||
|
||||
expect(mockSetup).not.toHaveBeenCalled();
|
||||
});
|
||||
it('On enable lang chain, set up with esql by default if ELSER exists', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<KnowledgeBaseSettings
|
||||
{...defaultProps}
|
||||
knowledgeBase={{
|
||||
assistantLangChain: false,
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('assistantLangChainSwitch'));
|
||||
expect(setUpdatedKnowledgeBaseSettings).toHaveBeenCalledWith({
|
||||
assistantLangChain: true,
|
||||
});
|
||||
|
||||
expect(mockSetup).toHaveBeenCalledWith('esql');
|
||||
});
|
||||
it('On disable knowledge base, call delete knowledge base setup', () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<KnowledgeBaseSettings {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId('install-kb')).not.toBeInTheDocument();
|
||||
expect(getByTestId('kb-installed')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('knowledgeBaseActionButton'));
|
||||
expect(mockDelete).toHaveBeenCalledWith();
|
||||
});
|
||||
it('On enable knowledge base, call setup knowledge base setup', () => {
|
||||
(useKnowledgeBaseStatus as jest.Mock).mockImplementation(() => {
|
||||
return {
|
||||
data: {
|
||||
elser_exists: true,
|
||||
esql_exists: false,
|
||||
index_exists: false,
|
||||
pipeline_exists: false,
|
||||
},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
};
|
||||
});
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<KnowledgeBaseSettings {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId('kb-installed')).not.toBeInTheDocument();
|
||||
expect(getByTestId('install-kb')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('knowledgeBaseActionButton'));
|
||||
expect(mockSetup).toHaveBeenCalledWith();
|
||||
});
|
||||
it('If elser does not exist, do not offer knowledge base', () => {
|
||||
(useKnowledgeBaseStatus as jest.Mock).mockImplementation(() => {
|
||||
return {
|
||||
data: {
|
||||
elser_exists: false,
|
||||
esql_exists: false,
|
||||
index_exists: false,
|
||||
pipeline_exists: false,
|
||||
},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
};
|
||||
});
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<KnowledgeBaseSettings {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId('knowledgeBaseActionButton')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -26,12 +26,12 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { useDeleteKnowledgeBase } from '../use_delete_knowledge_base/use_delete_knowledge_base';
|
||||
import { useKnowledgeBaseStatus } from '../use_knowledge_base_status/use_knowledge_base_status';
|
||||
import { useSetupKnowledgeBase } from '../use_setup_knowledge_base/use_setup_knowledge_base';
|
||||
import { useAssistantContext } from '../assistant_context';
|
||||
import { useDeleteKnowledgeBase } from './use_delete_knowledge_base';
|
||||
import { useKnowledgeBaseStatus } from './use_knowledge_base_status';
|
||||
import { useSetupKnowledgeBase } from './use_setup_knowledge_base';
|
||||
|
||||
import type { KnowledgeBaseConfig } from '../../assistant/types';
|
||||
import type { KnowledgeBaseConfig } from '../assistant/types';
|
||||
|
||||
const ESQL_RESOURCE = 'esql';
|
||||
const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-kb';
|
||||
|
@ -95,6 +95,7 @@ export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
|
|||
) : (
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
data-test-subj="assistantLangChainSwitch"
|
||||
checked={knowledgeBase.assistantLangChain}
|
||||
onChange={onEnableAssistantLangChainChange}
|
||||
label={i18n.KNOWLEDGE_BASE_LABEL}
|
||||
|
@ -123,6 +124,7 @@ export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
|
|||
<EuiButtonEmpty
|
||||
color={isKnowledgeBaseEnabled ? 'danger' : 'primary'}
|
||||
flush="left"
|
||||
data-test-subj={'knowledgeBaseActionButton'}
|
||||
onClick={() => onEnableKB(!isKnowledgeBaseEnabled)}
|
||||
size="xs"
|
||||
>
|
||||
|
@ -135,14 +137,14 @@ export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
|
|||
|
||||
const knowledgeBaseDescription = useMemo(() => {
|
||||
return isKnowledgeBaseEnabled ? (
|
||||
<>
|
||||
<span data-test-subj="kb-installed">
|
||||
{i18n.KNOWLEDGE_BASE_DESCRIPTION_INSTALLED(KNOWLEDGE_BASE_INDEX_PATTERN)}{' '}
|
||||
{knowledgeBaseActionButton}
|
||||
</>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span data-test-subj="install-kb">
|
||||
{i18n.KNOWLEDGE_BASE_DESCRIPTION} {knowledgeBaseActionButton}
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}, [isKnowledgeBaseEnabled, knowledgeBaseActionButton]);
|
||||
|
||||
|
@ -166,6 +168,7 @@ export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
|
|||
<EuiButtonEmpty
|
||||
color={isESQLEnabled ? 'danger' : 'primary'}
|
||||
flush="left"
|
||||
data-test-subj="esqlEnableButton"
|
||||
onClick={() => onEnableESQL(!isESQLEnabled)}
|
||||
size="xs"
|
||||
>
|
||||
|
@ -176,13 +179,13 @@ export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
|
|||
|
||||
const esqlDescription = useMemo(() => {
|
||||
return isESQLEnabled ? (
|
||||
<>
|
||||
<span data-test-subj="esql-installed">
|
||||
{i18n.ESQL_DESCRIPTION_INSTALLED} {esqlActionButton}
|
||||
</>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span data-test-subj="install-esql">
|
||||
{i18n.ESQL_DESCRIPTION} {esqlActionButton}
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}, [esqlActionButton, isESQLEnabled]);
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useDeleteKnowledgeBase, UseDeleteKnowledgeBaseParams } from './use_delete_knowledge_base';
|
||||
import { deleteKnowledgeBase as _deleteKnowledgeBase } from '../assistant/api';
|
||||
import { useMutation as _useMutation } from '@tanstack/react-query';
|
||||
|
||||
const useMutationMock = _useMutation as jest.Mock;
|
||||
const deleteKnowledgeBaseMock = _deleteKnowledgeBase as jest.Mock;
|
||||
|
||||
jest.mock('../assistant/api', () => {
|
||||
const actual = jest.requireActual('../assistant/api');
|
||||
return {
|
||||
...actual,
|
||||
deleteKnowledgeBase: jest.fn((...args) => actual.deleteKnowledgeBase(...args)),
|
||||
};
|
||||
});
|
||||
jest.mock('./use_knowledge_base_status');
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useMutation: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn();
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
const statusResponse = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
const http = {
|
||||
fetch: jest.fn().mockResolvedValue(statusResponse),
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
const defaultProps = { http, toasts } as unknown as UseDeleteKnowledgeBaseParams;
|
||||
|
||||
describe('useDeleteKnowledgeBase', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should call api to delete knowledge base', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/',
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('should call api to delete knowledge base with resource arg', async () => {
|
||||
useMutationMock.mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn('something');
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
});
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/something',
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return delete response', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
await expect(result.current).resolves.toStrictEqual(statusResponse);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast when api throws error', async () => {
|
||||
deleteKnowledgeBaseMock.mockRejectedValue(new Error('this is an error'));
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,8 +9,8 @@ import { useMutation } from '@tanstack/react-query';
|
|||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { deleteKnowledgeBase } from '../../assistant/api';
|
||||
import { useInvalidateKnowledgeBaseStatus } from '../use_knowledge_base_status/use_knowledge_base_status';
|
||||
import { deleteKnowledgeBase } from '../assistant/api';
|
||||
import { useInvalidateKnowledgeBaseStatus } from './use_knowledge_base_status';
|
||||
|
||||
const DELETE_KNOWLEDGE_BASE_MUTATION_KEY = ['elastic-assistant', 'delete-knowledge-base'];
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useKnowledgeBaseStatus, UseKnowledgeBaseStatusParams } from './use_knowledge_base_status';
|
||||
import { getKnowledgeBaseStatus as _getKnowledgeBaseStatus } from '../assistant/api';
|
||||
|
||||
const getKnowledgeBaseStatusMock = _getKnowledgeBaseStatus as jest.Mock;
|
||||
|
||||
jest.mock('../assistant/api', () => {
|
||||
const actual = jest.requireActual('../assistant/api');
|
||||
return {
|
||||
...actual,
|
||||
getKnowledgeBaseStatus: jest.fn((...args) => actual.getKnowledgeBaseStatus(...args)),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn({});
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
const statusResponse = {
|
||||
elser_exists: true,
|
||||
esql_exists: true,
|
||||
index_exists: true,
|
||||
pipeline_exists: true,
|
||||
};
|
||||
|
||||
const http = {
|
||||
fetch: jest.fn().mockResolvedValue(statusResponse),
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
const defaultProps = { http, toasts } as unknown as UseKnowledgeBaseStatusParams;
|
||||
describe('useKnowledgeBaseStatus', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should call api to get knowledge base status without resource arg', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useKnowledgeBaseStatus(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/',
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('should call api to get knowledge base status with resource arg', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() =>
|
||||
useKnowledgeBaseStatus({ ...defaultProps, resource: 'something' })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/something',
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return status response', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useKnowledgeBaseStatus(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
await expect(result.current).resolves.toStrictEqual(statusResponse);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast when api throws error', async () => {
|
||||
getKnowledgeBaseStatusMock.mockRejectedValue(new Error('this is an error'));
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useKnowledgeBaseStatus(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,7 +11,7 @@ import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-ht
|
|||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useCallback } from 'react';
|
||||
import { getKnowledgeBaseStatus } from '../../assistant/api';
|
||||
import { getKnowledgeBaseStatus } from '../assistant/api';
|
||||
|
||||
const KNOWLEDGE_BASE_STATUS_QUERY_KEY = ['elastic-assistant', 'knowledge-base-status'];
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useSetupKnowledgeBase, UseSetupKnowledgeBaseParams } from './use_setup_knowledge_base';
|
||||
import { postKnowledgeBase as _postKnowledgeBase } from '../assistant/api';
|
||||
import { useMutation as _useMutation } from '@tanstack/react-query';
|
||||
|
||||
const postKnowledgeBaseMock = _postKnowledgeBase as jest.Mock;
|
||||
const useMutationMock = _useMutation as jest.Mock;
|
||||
jest.mock('../assistant/api', () => {
|
||||
const actual = jest.requireActual('../assistant/api');
|
||||
return {
|
||||
...actual,
|
||||
postKnowledgeBase: jest.fn((...args) => actual.postKnowledgeBase(...args)),
|
||||
};
|
||||
});
|
||||
jest.mock('./use_knowledge_base_status');
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useMutation: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn();
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
const statusResponse = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
const http = {
|
||||
fetch: jest.fn().mockResolvedValue(statusResponse),
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
const defaultProps = { http, toasts } as unknown as UseSetupKnowledgeBaseParams;
|
||||
|
||||
describe('useSetupKnowledgeBase', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should call api to post knowledge base setup', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useSetupKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/',
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('should call api to post knowledge base setup with resource arg', async () => {
|
||||
useMutationMock.mockImplementation(async (queryKey, fn, opts) => {
|
||||
try {
|
||||
const res = await fn('something');
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
opts.onError(e);
|
||||
}
|
||||
});
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useSetupKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/knowledge_base/something',
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return setup response', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSetupKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
await expect(result.current).resolves.toStrictEqual(statusResponse);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast when api throws error', async () => {
|
||||
postKnowledgeBaseMock.mockRejectedValue(new Error('this is an error'));
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useSetupKnowledgeBase(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,8 +9,8 @@ import { useMutation } from '@tanstack/react-query';
|
|||
import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
|
||||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { postKnowledgeBase } from '../../assistant/api';
|
||||
import { useInvalidateKnowledgeBaseStatus } from '../use_knowledge_base_status/use_knowledge_base_status';
|
||||
import { postKnowledgeBase } from '../assistant/api';
|
||||
import { useInvalidateKnowledgeBaseStatus } from './use_knowledge_base_status';
|
||||
|
||||
const SETUP_KNOWLEDGE_BASE_MUTATION_KEY = ['elastic-assistant', 'post-knowledge-base'];
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ActionType } from '@kbn/actions-plugin/common';
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
export const mockActionTypes = [
|
||||
{
|
||||
id: '.gen-ai',
|
||||
name: 'OpenAI',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isSystemActionType: true,
|
||||
supportedFeatureIds: ['generativeAI'],
|
||||
} as ActionType,
|
||||
{
|
||||
id: '.bedrock',
|
||||
name: 'Bedrock',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isSystemActionType: true,
|
||||
supportedFeatureIds: ['generativeAI'],
|
||||
} as ActionType,
|
||||
];
|
||||
|
||||
export const mockConnectors: ActionConnector[] = [
|
||||
{
|
||||
id: 'connectorId',
|
||||
name: 'Captain Connector',
|
||||
isMissingSecrets: false,
|
||||
actionTypeId: '.gen-ai',
|
||||
secrets: {},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
name: 'Professor Connector',
|
||||
isMissingSecrets: false,
|
||||
actionTypeId: '.gen-ai',
|
||||
secrets: {},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
},
|
||||
},
|
||||
];
|
|
@ -69,3 +69,13 @@ export const welcomeConvo: Conversation = {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const customConvo: Conversation = {
|
||||
id: 'Custom option',
|
||||
isDefault: false,
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -22,3 +22,5 @@ export const mockEventPromptContext: PromptContext = {
|
|||
id: 'mock-event-prompt-context-1',
|
||||
tooltip: 'Add this event as context',
|
||||
};
|
||||
|
||||
export const mockPromptContexts: PromptContext[] = [mockAlertPromptContext, mockEventPromptContext];
|
||||
|
|
|
@ -31,3 +31,9 @@ export const defaultSystemPrompt: Prompt = {
|
|||
isDefault: true,
|
||||
isNewConversationDefault: true,
|
||||
};
|
||||
|
||||
export const mockSystemPrompts: Prompt[] = [
|
||||
mockSystemPrompt,
|
||||
mockSuperheroSystemPrompt,
|
||||
defaultSystemPrompt,
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue