[8.12] [Obs AI Assistant] E2E tests for conversation view (#173485) (#173598)

# Backport

This will backport the following commits from `main` to `8.12`:
- [[Obs AI Assistant] E2E tests for conversation view
(#173485)](https://github.com/elastic/kibana/pull/173485)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Dario
Gieselaar","email":"dario.gieselaar@elastic.co"},"sourceCommit":{"committedDate":"2023-12-19T08:49:58Z","message":"[Obs
AI Assistant] E2E tests for conversation view (#173485)\n\nAdds some
basic E2E tests for the conversation
view.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f67afe2866014cfbc21755134950d7abaa8cf8c4","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v8.12.0","v8.12.1","v8.13.0"],"number":173485,"url":"https://github.com/elastic/kibana/pull/173485","mergeCommit":{"message":"[Obs
AI Assistant] E2E tests for conversation view (#173485)\n\nAdds some
basic E2E tests for the conversation
view.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f67afe2866014cfbc21755134950d7abaa8cf8c4"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/173485","number":173485,"mergeCommit":{"message":"[Obs
AI Assistant] E2E tests for conversation view (#173485)\n\nAdds some
basic E2E tests for the conversation
view.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f67afe2866014cfbc21755134950d7abaa8cf8c4"}}]}]
BACKPORT-->
This commit is contained in:
Dario Gieselaar 2023-12-19 12:38:04 +01:00 committed by GitHub
parent a1f2e91676
commit 99b36e82d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 716 additions and 56 deletions

View file

@ -342,6 +342,7 @@ enabled:
- x-pack/test/observability_onboarding_api_integration/basic/config.ts
- x-pack/test/observability_onboarding_api_integration/cloud/config.ts
- x-pack/test/observability_ai_assistant_api_integration/enterprise/config.ts
- x-pack/test/observability_ai_assistant_functional/enterprise/config.ts
- x-pack/test/plugin_api_integration/config.ts
- x-pack/test/plugin_functional/config.ts
- x-pack/test/reporting_api_integration/reporting_and_security.config.ts

View file

@ -1,5 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/server-route-repository",
"owner": ["@elastic/obs-knowledge-team", "@elastic/obs-ux-management-team"]
"owner": ["@elastic/obs-knowledge-team"]
}

View file

@ -63,6 +63,7 @@ export function registerRoutes({
request,
context,
params: validatedParams,
logger,
...dependencies,
}).then((value) => {
return {

View file

@ -7,9 +7,18 @@
import React from 'react';
import { css, keyframes } from '@emotion/css';
import { EuiBetaBadge, EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
import {
EuiBetaBadge,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
import { isHttpFetchError } from '@kbn/core-http-browser';
import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
const fadeInAnimation = keyframes`
@ -32,6 +41,34 @@ export function WelcomeMessageConnectors({
connectors: UseGenAIConnectorsResult;
onSetupConnectorClick?: () => void;
}) {
if (connectors.error) {
const isForbiddenError =
isHttpFetchError(connectors.error) &&
(connectors.error.body as { statusCode: number }).statusCode === 403;
return (
<div className={fadeInClassName}>
<EuiFlexGroup direction="row" alignItems="center" justifyContent="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type="alert" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="danger">
{isForbiddenError
? i18n.translate(
'xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel',
{ defaultMessage: 'Required privileges to get connectors are missing' }
)
: i18n.translate(
'xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel',
{ defaultMessage: 'Could not load connectors' }
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}
return !connectors.loading && connectors.connectors?.length === 0 && onSetupConnectorClick ? (
<div className={fadeInClassName}>
<EuiText color="subdued" size="s">

View file

@ -53,7 +53,7 @@ export interface UseChatProps {
export function useChat({
initialMessages,
initialConversationId: initialConversationIdFromProps,
initialConversationId,
chatService,
connectorId,
onConversationUpdate,
@ -68,7 +68,9 @@ export function useChat({
useOnce(initialMessages);
const initialConversationId = useOnce(initialConversationIdFromProps);
useOnce(initialConversationId);
const [conversationId, setConversationId] = useState(initialConversationId);
const [messages, setMessages] = useState<Message[]>(initialMessages);
@ -127,7 +129,7 @@ export function useChat({
messages: getWithSystemMessage(nextMessages, systemMessage),
persist,
signal: abortControllerRef.current.signal,
conversationId: initialConversationId,
conversationId,
});
function getPendingMessages() {
@ -188,6 +190,9 @@ export function useChat({
break;
case StreamingChatResponseEventType.ConversationCreate:
setConversationId(event.conversation.id);
onConversationUpdateRef.current?.(event);
break;
case StreamingChatResponseEventType.ConversationUpdate:
onConversationUpdateRef.current?.(event);
break;
@ -220,7 +225,7 @@ export function useChat({
systemMessage,
handleError,
persist,
initialConversationId,
conversationId,
]
);

View file

@ -312,7 +312,10 @@ export class ObservabilityAIAssistantService {
};
await Promise.all(
this.registrations.map((fn) =>
fn({ signal, registerContext, registerFunction, resources, client })
fn({ signal, registerContext, registerFunction, resources, client }).catch((error) => {
this.logger.error(`Error registering functions`);
this.logger.error(error);
})
)
);

View file

@ -54,6 +54,7 @@
"@kbn/babel-register",
"@kbn/dev-cli-runner",
"@kbn/core-analytics-browser",
"@kbn/core-http-browser",
"@kbn/security-plugin-types-common"
],
"exclude": ["target/**/*"]

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
import { Config, FtrConfigProviderContext } from '@kbn/test';
import supertest from 'supertest';
import { format, UrlObject } from 'url';
import { ObservabilityAIAssistantFtrConfigName } from '../configs';
import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context';
import { InheritedServices } from './ftr_provider_context';
import {
createObservabilityAIAssistantApiClient,
ObservabilityAIAssistantAPIClient,
@ -34,21 +34,71 @@ export type CreateTestConfig = ReturnType<typeof createTestConfig>;
export interface CreateTest {
testFiles: string[];
servers: any;
servicesRequiredForTestAnalysis: string[];
services: InheritedServices & {
observabilityAIAssistantAPIClient: (context: InheritedFtrProviderContext) => Promise<{
observabilityAIAssistantAPIClient: () => Promise<{
readUser: ObservabilityAIAssistantAPIClient;
writeUser: ObservabilityAIAssistantAPIClient;
}>;
observabilityAIAssistantFtrConfig: (
context: InheritedFtrProviderContext
) => ObservabilityAIAssistantFtrConfig;
};
junit: { reportName: string };
esTestCluster: any;
kbnTestServer: any;
}
export function createObservabilityAIAssistantAPIConfig({
config,
license,
name,
kibanaConfig,
}: {
config: Config;
license: 'basic' | 'trial';
name: string;
kibanaConfig?: Record<string, any>;
}): Omit<CreateTest, 'testFiles'> {
const services = config.get('services') as InheritedServices;
const servers = config.get('servers');
const kibanaServer = servers.kibana as UrlObject;
const createTest: Omit<CreateTest, 'testFiles'> = {
...config.getAll(),
servers,
services: {
...services,
observabilityAIAssistantAPIClient: async () => {
return {
readUser: await getObservabilityAIAssistantAPIClient({
kibanaServer,
}),
writeUser: await getObservabilityAIAssistantAPIClient({
kibanaServer,
}),
};
},
},
junit: {
reportName: `Observability AI Assistant API Integration tests (${name})`,
},
esTestCluster: {
...config.get('esTestCluster'),
license,
},
kbnTestServer: {
...config.get('kbnTestServer'),
serverArgs: [
...config.get('kbnTestServer.serverArgs'),
...(kibanaConfig
? Object.entries(kibanaConfig).map(([key, value]) =>
Array.isArray(value) ? `--${key}=${JSON.stringify(value)}` : `--${key}=${value}`
)
: []),
],
},
};
return createTest;
}
export function createTestConfig(
config: ObservabilityAIAssistantFtrConfig
): ({ readConfigFile }: FtrConfigProviderContext) => Promise<CreateTest> {
@ -59,49 +109,15 @@ export function createTestConfig(
require.resolve('../../api_integration/config.ts')
);
const services = xPackAPITestsConfig.get('services') as InheritedServices;
const servers = xPackAPITestsConfig.get('servers');
const kibanaServer = servers.kibana as UrlObject;
const createTest: CreateTest = {
testFiles: [require.resolve('../tests')],
servers,
servicesRequiredForTestAnalysis: ['observabilityAIAssistantFtrConfig', 'registry'],
services: {
...services,
observabilityAIAssistantFtrConfig: () => config,
observabilityAIAssistantAPIClient: async (_: InheritedFtrProviderContext) => {
return {
readUser: await getObservabilityAIAssistantAPIClient({
kibanaServer,
}),
writeUser: await getObservabilityAIAssistantAPIClient({
kibanaServer,
}),
};
},
},
junit: {
reportName: `Observability AI Assistant API Integration tests (${name})`,
},
esTestCluster: {
...xPackAPITestsConfig.get('esTestCluster'),
return {
...createObservabilityAIAssistantAPIConfig({
config: xPackAPITestsConfig,
name,
license,
},
kbnTestServer: {
...xPackAPITestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
...(kibanaConfig
? Object.entries(kibanaConfig).map(([key, value]) =>
Array.isArray(value) ? `--${key}=${JSON.stringify(value)}` : `--${key}=${value}`
)
: []),
],
},
kibanaConfig,
}),
testFiles: [require.resolve('../tests')],
};
return createTest;
};
}

View file

@ -14,7 +14,7 @@ export const observabilityAIAssistantDebugLogger = {
appenders: ['console'],
};
const observabilityAIAssistantFtrConfigs = {
export const observabilityAIAssistantFtrConfigs = {
basic: {
license: 'basic' as const,
kibanaConfig: {

View file

@ -0,0 +1,83 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
import { merge } from 'lodash';
import supertest from 'supertest';
import { format, UrlObject } from 'url';
import {
ObservabilityAIAssistantFtrConfig,
CreateTest as CreateTestAPI,
createObservabilityAIAssistantAPIConfig,
} from '../../observability_ai_assistant_api_integration/common/config';
import {
createObservabilityAIAssistantApiClient,
ObservabilityAIAssistantAPIClient,
} from '../../observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client';
import { InheritedFtrProviderContext, InheritedServices } from '../ftr_provider_context';
import { ObservabilityAIAssistantUIProvider, ObservabilityAIAssistantUIService } from './ui';
export interface TestConfig extends CreateTestAPI {
services: Omit<CreateTestAPI['services'], 'observabilityAIAssistantAPIClient'> &
InheritedServices & {
observabilityAIAssistantUI: (
context: InheritedFtrProviderContext
) => Promise<ObservabilityAIAssistantUIService>;
observabilityAIAssistantAPIClient: () => Promise<
Awaited<ReturnType<CreateTestAPI['services']['observabilityAIAssistantAPIClient']>> & {
testUser: ObservabilityAIAssistantAPIClient;
}
>;
};
}
export type CreateTestConfig = ReturnType<typeof createTestConfig>;
export function createTestConfig(
config: ObservabilityAIAssistantFtrConfig
): ({ readConfigFile }: FtrConfigProviderContext) => Promise<TestConfig> {
const { license, name, kibanaConfig } = config;
return async ({ readConfigFile, log, esVersion }: FtrConfigProviderContext) => {
const testConfig = await readConfigFile(require.resolve('../../functional/config.base.js'));
const baseConfig = createObservabilityAIAssistantAPIConfig({
config: testConfig,
license,
name,
kibanaConfig,
});
return merge(
{
services: testConfig.get('services'),
},
baseConfig,
{
testFiles: [require.resolve('../tests')],
services: {
observabilityAIAssistantUI: (context: InheritedFtrProviderContext) =>
ObservabilityAIAssistantUIProvider(context),
observabilityAIAssistantAPIClient: async (context: InheritedFtrProviderContext) => {
const otherUsers = await baseConfig.services.observabilityAIAssistantAPIClient();
return {
...otherUsers,
testUser: createObservabilityAIAssistantApiClient(
supertest(
format({
...(baseConfig.servers.kibana as UrlObject),
auth: `test_user:changeme`,
})
)
),
};
},
},
}
);
};
}

View file

@ -0,0 +1,60 @@
/*
* 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 { WebDriver } from 'selenium-webdriver';
interface ResponseFactory {
fail: (reason?: string) => ['Fetch.failRequest', { requestId: string }];
}
export async function interceptRequest(
driver: WebDriver,
pattern: string,
onIntercept: (responseFactory: ResponseFactory) => [string, Record<string, any>],
cb: () => Promise<void>
) {
const connection = await driver.createCDPConnection('page');
return new Promise<void>((resolve, reject) => {
connection._wsConnection.on('message', async (data: Buffer) => {
const parsed = JSON.parse(data.toString());
if (parsed.method === 'Fetch.requestPaused') {
await new Promise((innerResolve) =>
connection.execute(
...onIntercept({
fail: () => [
'Fetch.failRequest',
{ requestId: parsed.params.requestId, errorReason: 'Failed' },
],
}),
innerResolve
)
);
await new Promise((innerResolve) => connection.execute('Fetch.disable', {}, innerResolve));
resolve();
}
});
new Promise((innerResolve) =>
connection.execute(
'Fetch.enable',
{
patterns: [{ urlPattern: pattern }],
},
innerResolve
)
)
.then(() => {
return cb();
})
.catch((err: Error) => {
reject(err);
});
});
}

View file

@ -0,0 +1,121 @@
/*
* 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 type { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config';
import type { ObservabilityAIAssistantRoutes } from '@kbn/observability-ai-assistant-plugin/public/routes/config';
import qs from 'query-string';
import type { Role } from '@kbn/security-plugin-types-common';
import { OBSERVABILITY_AI_ASSISTANT_FEATURE_ID } from '@kbn/observability-ai-assistant-plugin/common/feature';
import { APM_SERVER_FEATURE_ID } from '@kbn/apm-plugin/server';
import type { InheritedFtrProviderContext } from '../../ftr_provider_context';
export interface ObservabilityAIAssistantUIService {
pages: typeof pages;
auth: {
login: () => Promise<void>;
logout: () => Promise<void>;
};
router: {
goto<T extends PathsOf<ObservabilityAIAssistantRoutes>>(
path: T,
...params: TypeAsArgs<TypeOf<ObservabilityAIAssistantRoutes, T>>
): Promise<void>;
};
}
const pages = {
conversations: {
setupGenAiConnectorsButtonSelector: `observabilityAiAssistantInitialSetupPanelSetUpGenerativeAiConnectorButton`,
chatInput: 'observabilityAiAssistantChatPromptEditorTextArea',
retryButton: 'observabilityAiAssistantWelcomeMessageSetUpKnowledgeBaseButton',
},
createConnectorFlyout: {
flyout: 'create-connector-flyout',
genAiCard: '.gen-ai-card',
bedrockCard: '.bedrock-card',
nameInput: 'nameInput',
urlInput: 'config.apiUrl-input',
apiKeyInput: 'secrets.apiKey-input',
saveButton: 'create-connector-flyout-save-btn',
},
};
export async function ObservabilityAIAssistantUIProvider({
getPageObjects,
getService,
}: InheritedFtrProviderContext): Promise<ObservabilityAIAssistantUIService> {
const browser = getService('browser');
const deployment = getService('deployment');
const security = getService('security');
const pageObjects = getPageObjects(['common']);
const roleName = 'observability-ai-assistant-functional-test-role';
return {
pages,
auth: {
login: async () => {
await browser.navigateTo(deployment.getHostPort());
const roleDefinition: Role = {
name: roleName,
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
spaces: ['*'],
base: [],
feature: {
actions: ['all'],
[APM_SERVER_FEATURE_ID]: ['all'],
[OBSERVABILITY_AI_ASSISTANT_FEATURE_ID]: ['all'],
},
},
],
};
await security.role.create(roleName, roleDefinition);
await security.testUser.setRoles([roleName, 'apm_user']); // performs a page reload
},
logout: async () => {
await security.role.delete(roleName);
await security.testUser.restoreDefaults();
},
},
router: {
goto: (...args) => {
const [path, params] = args;
const formattedPath = path
.split('/')
.map((part) => {
const match = part.match(/(?:{([a-zA-Z]+)})/);
return match ? (params.path as Record<string, any>)[match[1]] : part;
})
.join('/');
const urlWithQueryParams = qs.stringifyUrl(
{
url: formattedPath,
query: params.query,
},
{ encode: true }
);
return pageObjects.common.navigateToApp('observabilityAIAssistant', {
path: urlWithQueryParams.substring(1),
shouldLoginIfPrompted: true,
insertTimestamp: false,
});
},
},
};
}

View file

@ -0,0 +1,23 @@
/*
* 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 { mapValues } from 'lodash';
import {
ObservabilityAIAssistantFtrConfigName,
observabilityAIAssistantFtrConfigs,
} from '../../observability_ai_assistant_api_integration/configs';
import { createTestConfig, CreateTestConfig } from '../common/config';
export const configs: Record<ObservabilityAIAssistantFtrConfigName, CreateTestConfig> = mapValues(
observabilityAIAssistantFtrConfigs,
(value, key) => {
return createTestConfig({
name: key as ObservabilityAIAssistantFtrConfigName,
...value,
});
}
);

View file

@ -0,0 +1,11 @@
/*
* 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 { configs } from '../configs';
// eslint-disable-next-line import/no-default-export
export default configs.enterprise;

View file

@ -0,0 +1,24 @@
/*
* 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 { GenericFtrProviderContext } from '@kbn/test';
import { pageObjects } from '../functional/page_objects';
import { services } from '../functional/services';
import { TestConfig } from './common/config';
export type InheritedServices = typeof services;
export type InheritedFtrProviderContext = GenericFtrProviderContext<
InheritedServices,
typeof pageObjects
>;
export type FtrProviderContext = GenericFtrProviderContext<
TestConfig['services'],
typeof pageObjects
>;

View file

@ -0,0 +1,245 @@
/*
* 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 expect from '@kbn/expect';
import { CreateChatCompletionRequest } from 'openai';
import {
createLlmProxy,
LlmProxy,
} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy';
import { interceptRequest } from '../../common/intercept_request';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ApiTest({ getService, getPageObjects }: FtrProviderContext) {
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
const ui = getService('observabilityAIAssistantUI');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
const supertest = getService('supertest');
const retry = getService('retry');
const driver = getService('__webdriver__');
const toasts = getService('toasts');
const { header, common } = getPageObjects(['header', 'common']);
const flyoutService = getService('flyout');
async function deleteConversations() {
const response = await observabilityAIAssistantAPIClient.testUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
for (const conversation of response.body.conversations) {
await observabilityAIAssistantAPIClient.testUser({
endpoint: `DELETE /internal/observability_ai_assistant/conversation/{conversationId}`,
params: {
path: {
conversationId: conversation.conversation.id,
},
},
});
}
}
async function deleteConnectors() {
const response = await observabilityAIAssistantAPIClient.testUser({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
});
for (const connector of response.body) {
await supertest
.delete(`/api/actions/connector/${connector.id}`)
.set('kbn-xsrf', 'foo')
.expect(204);
}
}
describe('Conversations', () => {
let proxy: LlmProxy;
before(async () => {
await deleteConnectors();
await deleteConversations();
proxy = await createLlmProxy();
await ui.auth.login();
await ui.router.goto('/conversations/new', { path: {}, query: {} });
});
describe('without a connector', () => {
it('should display the set up connectors button', async () => {
await testSubjects.existOrFail(ui.pages.conversations.setupGenAiConnectorsButtonSelector);
});
describe('after clicking on the setup connectors button', async () => {
before(async () => {
await testSubjects.click(ui.pages.conversations.setupGenAiConnectorsButtonSelector);
});
it('opens a flyout', async () => {
await testSubjects.existOrFail(ui.pages.createConnectorFlyout.flyout);
await testSubjects.existOrFail(ui.pages.createConnectorFlyout.genAiCard);
// TODO: https://github.com/elastic/obs-ai-assistant-team/issues/126
// await testSubjects.missingOrFail(ui.pages.createConnectorFlyout.bedrockCard);
});
describe('after clicking on the Gen AI card and submitting the form', () => {
before(async () => {
await testSubjects.click(ui.pages.createConnectorFlyout.genAiCard);
await testSubjects.setValue(ui.pages.createConnectorFlyout.nameInput, 'myConnector');
await testSubjects.setValue(
ui.pages.createConnectorFlyout.urlInput,
`http://localhost:${proxy.getPort()}`
);
await testSubjects.setValue(ui.pages.createConnectorFlyout.apiKeyInput, 'myApiKey');
// intercept the request to set up the knowledge base,
// so we don't have to wait until it's fully downloaded
await interceptRequest(
driver.driver,
'*kb\\/setup*',
(responseFactory) => {
return responseFactory.fail();
},
async () => {
await testSubjects.clickWhenNotDisabled(ui.pages.createConnectorFlyout.saveButton);
}
);
await retry.waitFor('Connector created toast', async () => {
const count = await toasts.getToastCount();
return count > 0;
});
await toasts.dismissAllToasts();
});
it('creates a connector', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
});
expect(response.body.length).to.eql(1);
});
describe('after refreshing the page', () => {
before(async () => {
await browser.refresh();
});
it('shows a setup kb button', async () => {
await testSubjects.existOrFail(ui.pages.conversations.retryButton);
});
it('has an input field enabled', async () => {
await testSubjects.existOrFail(ui.pages.conversations.chatInput);
await testSubjects.isEnabled(ui.pages.conversations.chatInput);
});
describe('and sending over some text', () => {
before(async () => {
const titleInterceptor = proxy.intercept(
'title',
(body) => (JSON.parse(body) as CreateChatCompletionRequest).messages.length === 1
);
const conversationInterceptor = proxy.intercept(
'conversation',
(body) => (JSON.parse(body) as CreateChatCompletionRequest).messages.length !== 1
);
await testSubjects.setValue(ui.pages.conversations.chatInput, 'hello');
await testSubjects.pressEnter(ui.pages.conversations.chatInput);
const [titleSimulator, conversationSimulator] = await Promise.all([
titleInterceptor.waitForIntercept(),
conversationInterceptor.waitForIntercept(),
]);
await titleSimulator.next('My title');
await titleSimulator.complete();
await conversationSimulator.next('My response');
await conversationSimulator.complete();
await header.waitUntilLoadingHasFinished();
});
it('creates a conversation and updates the URL', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
expect(response.body.conversations.length).to.eql(1);
expect(response.body.conversations[0].messages.length).to.eql(3);
expect(response.body.conversations[0].conversation.title).to.be('My title');
await common.waitUntilUrlIncludes(
`/conversations/${response.body.conversations[0].conversation.id}`
);
});
describe('and adding another prompt', () => {
before(async () => {
const conversationInterceptor = proxy.intercept('conversation', () => true);
await testSubjects.setValue(ui.pages.conversations.chatInput, 'hello');
await testSubjects.pressEnter(ui.pages.conversations.chatInput);
const conversationSimulator = await conversationInterceptor.waitForIntercept();
await conversationSimulator.next('My second response');
await conversationSimulator.complete();
await header.waitUntilLoadingHasFinished();
});
it('does not create another conversation', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
expect(response.body.conversations.length).to.eql(1);
});
it('appends to the existing one', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
expect(response.body.conversations[0].messages.length).to.eql(5);
});
});
});
});
});
after(async () => {
await flyoutService.ensureAllClosed();
});
});
});
after(async () => {
await deleteConnectors();
await deleteConversations();
await ui.auth.logout();
await proxy.close();
});
});
}

View file

@ -0,0 +1,28 @@
/*
* 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 globby from 'globby';
import path from 'path';
import { FtrProviderContext } from '../../observability_ai_assistant_api_integration/common/ftr_provider_context';
const cwd = path.join(__dirname);
export default function observabilityAIAssistantFunctionalTests({
getService,
loadTestFile,
}: FtrProviderContext) {
describe('Observability AI Assistant Functional tests', function () {
const filePattern = '**/*.spec.ts';
const tests = globby.sync(filePattern, { cwd });
tests.forEach((testName) => {
describe(testName, () => {
loadTestFile(require.resolve(`./${testName}`));
});
});
});
}

View file

@ -150,5 +150,6 @@
"@kbn/reporting-export-types-png-common",
"@kbn/reporting-common",
"@kbn/security-plugin-types-common",
"@kbn/typed-react-router-config",
]
}