From da739d45d24b8f0f32e24d90aa5e52270728062a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Caplette?= <fcaplette@gitlab.com>
Date: Fri, 26 Jul 2024 15:35:21 +0000
Subject: [PATCH] Fix Explain vulnerabilities with AI loading state

When using the explain vulnerability quick action with
DuoChat, it would not trigger the loading. To fix this issue,
we use the new utility `sendDuoChatCommand` which opens
DuoChat and trigger loading and streaming properly.

Changelog: fixed
---
 .../explain_vulnerability.vue                 | 14 +++-------
 .../vulnerabilities/components/header.vue     | 14 +++-------
 .../explain_vulnerability_spec.js             | 27 +++++--------------
 .../frontend/vulnerabilities/header_spec.js   | 25 +++++------------
 4 files changed, 21 insertions(+), 59 deletions(-)

diff --git a/ee/app/assets/javascripts/vulnerabilities/components/explain_vulnerability/explain_vulnerability.vue b/ee/app/assets/javascripts/vulnerabilities/components/explain_vulnerability/explain_vulnerability.vue
index 0682c3bc99235..476fec6947b2c 100644
--- a/ee/app/assets/javascripts/vulnerabilities/components/explain_vulnerability/explain_vulnerability.vue
+++ b/ee/app/assets/javascripts/vulnerabilities/components/explain_vulnerability/explain_vulnerability.vue
@@ -4,8 +4,7 @@ import { s__ } from '~/locale';
 import { helpPagePath } from '~/helpers/help_page_helper';
 import { convertToGraphQLId } from '~/graphql_shared/utils';
 import { TYPENAME_VULNERABILITY } from '~/graphql_shared/constants';
-import chatMutation from 'ee/ai/graphql/chat.mutation.graphql';
-import { duoChatGlobalState } from '~/super_sidebar/constants';
+import { sendDuoChatCommand } from 'ee/ai/utils';
 
 export default {
   components: {
@@ -28,14 +27,9 @@ export default {
   },
   methods: {
     triggerDuoChat() {
-      duoChatGlobalState.isShown = true;
-
-      this.$apollo.mutate({
-        mutation: chatMutation,
-        variables: {
-          question: '/vulnerability_explain',
-          resourceId: this.vulnerabilityGraphqlId,
-        },
+      sendDuoChatCommand({
+        question: '/vulnerability_explain',
+        resourceId: this.vulnerabilityGraphqlId,
       });
     },
   },
diff --git a/ee/app/assets/javascripts/vulnerabilities/components/header.vue b/ee/app/assets/javascripts/vulnerabilities/components/header.vue
index 36d415fe45b86..63d719c98fc01 100644
--- a/ee/app/assets/javascripts/vulnerabilities/components/header.vue
+++ b/ee/app/assets/javascripts/vulnerabilities/components/header.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
 import { v4 as uuidv4 } from 'uuid';
 import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';
+import { sendDuoChatCommand } from 'ee/ai/utils';
 import vulnerabilityStateMutations from 'ee/security_dashboard/graphql/mutate_vulnerability_state';
 import StatusBadge from 'ee/vue_shared/security_reports/components/status_badge.vue';
 import { createAlert } from '~/alert';
@@ -14,8 +15,6 @@ import download from '~/lib/utils/downloader';
 import { visitUrl } from '~/lib/utils/url_utility';
 import UsersCache from '~/lib/utils/users_cache';
 import { __, s__ } from '~/locale';
-import { duoChatGlobalState } from '~/super_sidebar/constants';
-import chatMutation from 'ee/ai/graphql/chat.mutation.graphql';
 import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql';
 import aiResolveVulnerability from '../graphql/ai_resolve_vulnerability.mutation.graphql';
 import {
@@ -247,14 +246,9 @@ export default {
       }
     },
     explainVulnerability() {
-      duoChatGlobalState.isShown = true;
-
-      this.$apollo.mutate({
-        mutation: chatMutation,
-        variables: {
-          question: '/vulnerability_explain',
-          resourceId: this.vulnerabilityGraphqlId,
-        },
+      sendDuoChatCommand({
+        question: '/vulnerability_explain',
+        resourceId: this.vulnerabilityGraphqlId,
       });
     },
     resolveVulnerability() {
diff --git a/ee/spec/frontend/vulnerabilities/explain_vulnerability/explain_vulnerability_spec.js b/ee/spec/frontend/vulnerabilities/explain_vulnerability/explain_vulnerability_spec.js
index b70692826329c..0a741804615c1 100644
--- a/ee/spec/frontend/vulnerabilities/explain_vulnerability/explain_vulnerability_spec.js
+++ b/ee/spec/frontend/vulnerabilities/explain_vulnerability/explain_vulnerability_spec.js
@@ -2,11 +2,11 @@ import Vue from 'vue';
 import VueApollo from 'vue-apollo';
 import { GlLink, GlSprintf } from '@gitlab/ui';
 import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import * as utils from 'ee/ai/utils';
 import ExplainVulnerability from 'ee/vulnerabilities/components/explain_vulnerability/explain_vulnerability.vue';
-import { duoChatGlobalState } from '~/super_sidebar/constants';
-import chatMutation from 'ee/ai/graphql/chat.mutation.graphql';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { MOCK_TANUKI_BOT_MUTATATION_RES } from 'ee_jest/ai/tanuki_bot/mock_data';
+
+jest.mock('ee/ai/utils');
+jest.spyOn(utils, 'sendDuoChatCommand');
 
 Vue.use(VueApollo);
 
@@ -15,17 +15,12 @@ const MOCK_VULNERABILITY_ID = 1;
 describe('Explain Vulnerability component', () => {
   let wrapper;
 
-  const chatMutationHandlerMock = jest.fn().mockResolvedValue(MOCK_TANUKI_BOT_MUTATATION_RES);
-
   const createWrapper = () => {
-    const apolloProvider = createMockApollo([[chatMutation, chatMutationHandlerMock]]);
-
     wrapper = shallowMountExtended(ExplainVulnerability, {
       propsData: { vulnerabilityId: MOCK_VULNERABILITY_ID },
       stubs: {
         GlSprintf,
       },
-      apolloProvider,
     });
   };
 
@@ -65,20 +60,12 @@ describe('Explain Vulnerability component', () => {
   });
 
   describe('when the "Explain vulnerability" button is clicked', () => {
-    it('opens the global DuoChat drawer', () => {
-      expect(duoChatGlobalState.isShown).toBe(false);
-
-      clickExplainVulnerabilityButton();
-
-      expect(duoChatGlobalState.isShown).toBe(true);
-    });
-
-    it('calls a custom chat mutation with the correct prompt and resource-id', () => {
-      expect(chatMutationHandlerMock).not.toHaveBeenCalled();
+    it('calls sendDuoChatCommand with correct prompt and resource-id', () => {
+      expect(utils.sendDuoChatCommand).not.toHaveBeenCalled();
 
       clickExplainVulnerabilityButton();
 
-      expect(chatMutationHandlerMock).toHaveBeenCalledWith({
+      expect(utils.sendDuoChatCommand).toHaveBeenCalledWith({
         question: '/vulnerability_explain',
         resourceId: `gid://gitlab/Vulnerability/${MOCK_VULNERABILITY_ID}`,
       });
diff --git a/ee/spec/frontend/vulnerabilities/header_spec.js b/ee/spec/frontend/vulnerabilities/header_spec.js
index 8426b7f980d64..a9a417e1ab8ff 100644
--- a/ee/spec/frontend/vulnerabilities/header_spec.js
+++ b/ee/spec/frontend/vulnerabilities/header_spec.js
@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
 import VueApollo from 'vue-apollo';
 import { createMockSubscription } from 'mock-apollo-client';
 import { createMockDirective } from 'helpers/vue_mock_directive';
+import * as aiUtils from 'ee/ai/utils';
 import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql';
 import aiResolveVulnerability from 'ee/vulnerabilities/graphql/ai_resolve_vulnerability.mutation.graphql';
 import Api from 'ee/api';
@@ -25,9 +26,6 @@ import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
 import download from '~/lib/utils/downloader';
 import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
 import { visitUrl } from '~/lib/utils/url_utility';
-import chatMutation from 'ee/ai/graphql/chat.mutation.graphql';
-import { MOCK_TANUKI_BOT_MUTATATION_RES } from 'ee_jest/ai/tanuki_bot/mock_data';
-import { duoChatGlobalState } from '~/super_sidebar/constants';
 import {
   getVulnerabilityStatusMutationResponse,
   dismissalDescriptions,
@@ -51,6 +49,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
   ...jest.requireActual('~/lib/utils/url_utility'),
   visitUrl: jest.fn(),
 }));
+jest.mock('ee/ai/utils');
+jest.spyOn(aiUtils, 'sendDuoChatCommand');
 
 describe('Vulnerability Header', () => {
   let wrapper;
@@ -637,18 +637,11 @@ describe('Vulnerability Header', () => {
     });
 
     describe('explain with AI button', () => {
-      let chatMutationHandlerMock;
-
       const findExplainWithAIButton = () => findSplitButton().props('buttons')[1];
 
       beforeEach(() => {
-        chatMutationHandlerMock = jest.fn().mockResolvedValue(MOCK_TANUKI_BOT_MUTATATION_RES);
-
-        const apolloProvider = createApolloProvider([chatMutation, chatMutationHandlerMock]);
-
         createWrapper({
           vulnerability: getVulnerability(),
-          apolloProvider,
         });
       });
 
@@ -662,19 +655,13 @@ describe('Vulnerability Header', () => {
         });
       });
 
-      it('opens the global DuoChat drawer when clicked', async () => {
-        expect(duoChatGlobalState.isShown).toBe(false);
-
-        await clickButton('explain-vulnerability');
-
-        expect(duoChatGlobalState.isShown).toBe(true);
-      });
+      it('calls sendDuoChatCommand with the correct parameters when clicked', async () => {
+        expect(aiUtils.sendDuoChatCommand).not.toHaveBeenCalled();
 
-      it('calls the correct mutation with the correct parameters when clicked', async () => {
         await clickButton('explain-vulnerability');
         await waitForPromises();
 
-        expect(chatMutationHandlerMock).toHaveBeenCalledWith({
+        expect(aiUtils.sendDuoChatCommand).toHaveBeenCalledWith({
           question: '/vulnerability_explain',
           resourceId: `gid://gitlab/Vulnerability/${defaultVulnerability.id}`,
         });
-- 
GitLab