mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# 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:
parent
a1f2e91676
commit
99b36e82d4
18 changed files with 716 additions and 56 deletions
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ export function registerRoutes({
|
|||
request,
|
||||
context,
|
||||
params: validatedParams,
|
||||
logger,
|
||||
...dependencies,
|
||||
}).then((value) => {
|
||||
return {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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/**/*"]
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export const observabilityAIAssistantDebugLogger = {
|
|||
appenders: ['console'],
|
||||
};
|
||||
|
||||
const observabilityAIAssistantFtrConfigs = {
|
||||
export const observabilityAIAssistantFtrConfigs = {
|
||||
basic: {
|
||||
license: 'basic' as const,
|
||||
kibanaConfig: {
|
||||
|
|
|
@ -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`,
|
||||
})
|
||||
)
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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;
|
24
x-pack/test/observability_ai_assistant_functional/ftr_provider_context.d.ts
vendored
Normal file
24
x-pack/test/observability_ai_assistant_functional/ftr_provider_context.d.ts
vendored
Normal 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
|
||||
>;
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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}`));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -150,5 +150,6 @@
|
|||
"@kbn/reporting-export-types-png-common",
|
||||
"@kbn/reporting-common",
|
||||
"@kbn/security-plugin-types-common",
|
||||
"@kbn/typed-react-router-config",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue