diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/conversations/conversations.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/conversations/conversations.spec.ts new file mode 100644 index 0000000000000..5040285a66753 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/conversations/conversations.spec.ts @@ -0,0 +1,263 @@ +/* + * 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 { merge, omit } from 'lodash'; +import { + type ConversationCreateRequest, + type ConversationUpdateRequest, + MessageRole, +} from '@kbn/observability-ai-assistant-plugin/common/types'; +import type { SupertestReturnType } from '../../../../services/observability_ai_assistant_api'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi'); + + const conversationCreate: ConversationCreateRequest = { + '@timestamp': new Date().toISOString(), + conversation: { + title: 'My title', + }, + labels: {}, + numeric_labels: {}, + messages: [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + content: 'My message', + }, + }, + ], + public: false, + }; + + const conversationUpdate: ConversationUpdateRequest = merge({}, conversationCreate, { + conversation: { + id: '', + title: 'My updated title', + }, + }); + + describe('Conversations', () => { + describe('without conversations', () => { + it('returns no conversations when listing', async () => { + const { status, body } = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'POST /internal/observability_ai_assistant/conversations', + }); + + expect(status).to.be(200); + + expect(body).to.eql({ conversations: [] }); + }); + + it('returns a 404 for updating conversations', async () => { + try { + await observabilityAIAssistantAPIClient.editor({ + endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: 'non-existing-conversation-id', + }, + body: { + conversation: conversationUpdate, + }, + }, + }); + } catch (error) { + expect(error.status).to.be(404); + } + }); + + it('returns a 404 for retrieving a conversation', async () => { + try { + await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: 'my-conversation-id', + }, + }, + }); + } catch (error) { + expect(error.status).to.be(404); + } + }); + }); + + describe('when creating a conversation with the write user', () => { + let createResponse: Awaited< + SupertestReturnType<'POST /internal/observability_ai_assistant/conversation'> + >; + before(async () => { + createResponse = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'POST /internal/observability_ai_assistant/conversation', + params: { + body: { + conversation: conversationCreate, + }, + }, + }); + expect(createResponse.status).to.be(200); + }); + + after(async () => { + const { status } = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + }, + }); + expect(status).to.be(200); + + try { + await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + }, + }); + } catch (error) { + expect(error.status).to.be(404); + } + }); + + it('returns the conversation', () => { + // delete user from response to avoid comparing it as it will be different in MKI + // delete createResponse.body.user; + expect(createResponse.body).to.eql({ + '@timestamp': createResponse.body['@timestamp'], + conversation: { + id: createResponse.body.conversation.id, + last_updated: createResponse.body.conversation.last_updated, + title: conversationCreate.conversation.title, + }, + labels: conversationCreate.labels, + numeric_labels: conversationCreate.numeric_labels, + messages: conversationCreate.messages, + namespace: 'default', + public: conversationCreate.public, + user: { + id: 'u_gf3TRV5WWjD0PQCcTzkUyRE8By8uUt90gK-rT9ZPhA4_0', + name: 'elastic_editor', + }, + }); + }); + + it('returns a 404 for updating a non-existing conversation', async () => { + try { + await observabilityAIAssistantAPIClient.editor({ + endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: 'non-existing-conversation-id', + }, + body: { + conversation: conversationUpdate, + }, + }, + }); + } catch (error) { + expect(error.status).to.be(404); + } + }); + + it('returns a 404 for retrieving a non-existing conversation', async () => { + try { + await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: 'non-existing-conversation-id', + }, + }, + }); + } catch (error) { + expect(error.status).to.be(404); + } + }); + + it('returns the conversation that was created', async () => { + const response = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + }, + }); + + expect(response.status).to.be(200); + + expect(response.body).to.eql(createResponse.body); + }); + + it('returns the created conversation when listing', async () => { + const response = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'POST /internal/observability_ai_assistant/conversations', + }); + + expect(response.status).to.be(200); + + expect(response.body.conversations[0]).to.eql(createResponse.body); + }); + // TODO + it.skip('returns a 404 when reading it with another user', () => {}); + + describe('after updating', () => { + let updateResponse: Awaited< + SupertestReturnType<'PUT /internal/observability_ai_assistant/conversation/{conversationId}'> + >; + + before(async () => { + updateResponse = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + body: { + conversation: merge(omit(conversationUpdate, 'conversation.id'), { + conversation: { id: createResponse.body.conversation.id }, + }), + }, + }, + }); + expect(updateResponse.status).to.be(200); + }); + + it('returns the updated conversation as response', async () => { + expect(updateResponse.body.conversation.title).to.eql( + conversationUpdate.conversation.title + ); + }); + + it('returns the updated conversation after get', async () => { + const updateAfterCreateResponse = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + }, + }); + + expect(updateAfterCreateResponse.status).to.be(200); + + expect(updateAfterCreateResponse.body.conversation.title).to.eql( + conversationUpdate.conversation.title + ); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/conversations/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/conversations/index.ts new file mode 100644 index 0000000000000..95d26566af6b5 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/conversations/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('conversations', () => { + loadTestFile(require.resolve('./conversations.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index.ts new file mode 100644 index 0000000000000..37f3899436c82 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; + +export default function aiAssistantApiIntegrationTests({ + loadTestFile, +}: DeploymentAgnosticFtrProviderContext) { + describe('observability AI Assistant', function () { + loadTestFile(require.resolve('./conversations')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts index 7544d7d90f1d5..edd82fc62dca6 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts @@ -21,5 +21,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('../../apis/saved_objects_management')); loadTestFile(require.resolve('../../apis/observability/slo')); loadTestFile(require.resolve('../../apis/observability/synthetics')); + loadTestFile(require.resolve('../../apis/observability/ai_assistant')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts index 4f666fc5b3ebe..4f0c42e12b1fb 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts @@ -15,5 +15,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('../../apis/observability/slo')); loadTestFile(require.resolve('../../apis/observability/synthetics')); loadTestFile(require.resolve('../../apis/observability/infra')); + loadTestFile(require.resolve('../../apis/observability/ai_assistant')); }); }