diff --git a/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue b/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue index 3c1542682bf0c30d746950039393a93b9b75a4b7..382c0c846c217401862dc58bb1d51af1bd6f7c79 100644 --- a/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue +++ b/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue @@ -87,7 +87,7 @@ export default { query: getAiMessages, variables() { return { - conversationType: this.glFeatures.duoChatMultiThread ? 'DUO_CHAT' : null, + conversationType: 'DUO_CHAT', }; }, skip() { @@ -106,7 +106,7 @@ export default { aiConversationThreads: { query: getAiConversationThreads, skip() { - return !this.duoChatGlobalState.isShown || !this.glFeatures.duoChatMultiThread; + return !this.duoChatGlobalState.isShown; }, update(data) { return data?.aiConversationThreads?.nodes || []; @@ -264,8 +264,10 @@ export default { onWindowResize() { this.updateDimensions(); }, - isClearOrResetMessage(question) { - return [GENIE_CHAT_CLEAR_MESSAGE, GENIE_CHAT_RESET_MESSAGE].includes(question); + shouldStartNewChat(question) { + return [GENIE_CHAT_NEW_MESSAGE, GENIE_CHAT_CLEAR_MESSAGE, GENIE_CHAT_RESET_MESSAGE].includes( + question, + ); }, findPredefinedPrompt(question) { return this.formattedContextPresets.find(({ text }) => text === question); @@ -332,25 +334,16 @@ export default { this.isResponseTracked = true; }, onSendChatPrompt(question, variables = {}, resourceId = this.computedResourceId) { - const CHAT_RESET_COMMANDS = [ - GENIE_CHAT_NEW_MESSAGE, - GENIE_CHAT_RESET_MESSAGE, - GENIE_CHAT_CLEAR_MESSAGE, - ]; - - // Create a new thread if chat is reset - if (this.glFeatures.duoChatMultiThread && CHAT_RESET_COMMANDS.includes(question)) { + if (this.shouldStartNewChat(question)) { this.onNewChat(); - - if (CHAT_RESET_COMMANDS.includes(question)) { - return; - } + return; } + performance.mark('prompt-sent'); this.completedRequestId = null; this.isResponseTracked = false; - if (!this.loading && !this.isClearOrResetMessage(question)) { + if (!this.loading) { this.setLoading(true); } @@ -359,10 +352,8 @@ export default { resourceId, clientSubscriptionId: this.clientSubscriptionId, projectId: this.projectId, - threadId: this.glFeatures.duoChatMultiThread ? this.activeThread : null, - conversationType: this.glFeatures.duoChatMultiThread - ? MULTI_THREADED_CONVERSATION_TYPE - : null, + threadId: this.activeThread, + conversationType: MULTI_THREADED_CONVERSATION_TYPE, ...variables, }; @@ -378,27 +369,21 @@ export default { }, }) .then(({ data: { aiAction = {} } = {} }) => { - if (!this.isClearOrResetMessage(question)) { - const trackingOptions = { - property: aiAction.requestId, - label: this.findPredefinedPrompt(question)?.eventLabel, - }; + const trackingOptions = { + property: aiAction.requestId, + label: this.findPredefinedPrompt(question)?.eventLabel, + }; - this.trackEvent('submit_gitlab_duo_question', trackingOptions); - } + this.trackEvent('submit_gitlab_duo_question', trackingOptions); if (aiAction.threadId && !this.activeThread) { this.activeThread = aiAction.threadId; } - if ([GENIE_CHAT_CLEAR_MESSAGE].includes(question)) { - this.$apollo.queries.aiMessages.refetch(); - } else { - this.addDuoChatMessage({ - ...aiAction, - content: question, - }); - } + this.addDuoChatMessage({ + ...aiAction, + content: question, + }); }) .catch((err) => { this.addDuoChatMessage({ @@ -498,7 +483,7 @@ export default { :thread-list="aiConversationThreads" :multi-threaded-view="multithreadedView" :active-thread-id="activeThread" - :is-multithreaded="glFeatures.duoChatMultiThread" + :is-multithreaded="true" :slash-commands="aiSlashCommands" :title="chatTitle" :dimensions="dimensions" diff --git a/ee/app/assets/javascripts/ai/tanuki_bot/components/tanuki_bot_subscriptions.vue b/ee/app/assets/javascripts/ai/tanuki_bot/components/tanuki_bot_subscriptions.vue index b12c203bf1ff00094ae4be8ff59a8d6536b36635..0e163f1388731d3f074442ee23496992e1c5a7b1 100644 --- a/ee/app/assets/javascripts/ai/tanuki_bot/components/tanuki_bot_subscriptions.vue +++ b/ee/app/assets/javascripts/ai/tanuki_bot/components/tanuki_bot_subscriptions.vue @@ -32,13 +32,7 @@ export default { return false; } - // In single-threaded mode, we ignore threadId checks since the API still includes threadIds - // and they can change (e.g., on /clear). Only check threadId matching in multi-threaded mode. - if (!this.glFeatures?.duoChatMultiThread) { - return true; - } - - // if we're running multi-threaded, check if the threadId is the same as the active thread + // check if the threadId is the same as the active thread return !threadId || threadId === this.activeThreadId; }, }, diff --git a/ee/app/finders/ai/conversations/thread_finder.rb b/ee/app/finders/ai/conversations/thread_finder.rb index 1ddfca2efaa37ff3c5d711300fb34b79d86ae175..ac3b7bdc9fc34ad5b6f452f3d779157dbd12b539 100644 --- a/ee/app/finders/ai/conversations/thread_finder.rb +++ b/ee/app/finders/ai/conversations/thread_finder.rb @@ -12,17 +12,7 @@ def execute relation = current_user.ai_conversation_threads relation = by_id(relation) relation = by_conversation_type(relation) - relation = relation.ordered - - # When requesting duo_chat threads but none exist, - # attempt to copy latest duo_chat_legacy type thread as a duo_chat thread. - if copy_legacy_thread?(relation) - legacy_thread = current_user.ai_conversation_threads.duo_chat_legacy.last - legacy_thread.dup_as_duo_chat_thread! if legacy_thread - ::Ai::Conversation::LegacyDuoChatCopiedUser.add(current_user.id) - end - - relation + relation.ordered end private @@ -40,13 +30,6 @@ def by_conversation_type(relation) relation.for_conversation_type(params[:conversation_type]) end - - def copy_legacy_thread?(relation) - params[:conversation_type] == 'duo_chat' && - params[:id].nil? && - relation.empty? && - !::Ai::Conversation::LegacyDuoChatCopiedUser.include?(current_user.id) # rubocop:disable Rails/NegateInclude -- no exclude method - end end end end diff --git a/ee/app/models/ai/conversation/thread.rb b/ee/app/models/ai/conversation/thread.rb index e149a922a351d6ae45bdb46a39b5e058c55847f4..0fa48446a1a4dbd374e8d5b8eac93643c6e3c706 100644 --- a/ee/app/models/ai/conversation/thread.rb +++ b/ee/app/models/ai/conversation/thread.rb @@ -40,18 +40,6 @@ def to_new_thread! ) end - def dup_as_duo_chat_thread! - transaction do - new_thread = self.class.new(attributes.except('id')) - new_thread.conversation_type = :duo_chat - new_thread.save! - messages.recent(::Gitlab::Llm::ChatStorage::Postgresql::MAX_MESSAGES).each do |message| - new_thread.messages.create(message.attributes.except('id', 'thread_id')) - end - new_thread - end - end - private def populate_organization diff --git a/ee/app/services/ai/slash_commands_service.rb b/ee/app/services/ai/slash_commands_service.rb index 73c5144bf7ed383e048ebd2cdd8063a16e851d1f..5871b6c52447fdf5c3e465651897a4b45971c392 100644 --- a/ee/app/services/ai/slash_commands_service.rb +++ b/ee/app/services/ai/slash_commands_service.rb @@ -46,18 +46,9 @@ def available_commands private def new_thread_commands - if Feature.enabled?(:duo_chat_multi_thread, @user) - [ - { name: '/new', description: _('New chat conversation.'), should_submit: false } - ] - else - [ - { name: '/reset', description: _('Reset conversation and ignore previous messages.'), - should_submit: true }, - { name: '/clear', description: _('Delete all messages in the current conversation.'), - should_submit: true } - ] - end + [ + { name: '/new', description: _('New chat conversation.'), should_submit: false } + ] end def context_commands diff --git a/ee/config/feature_flags/beta/duo_chat_multi_thread.yml b/ee/config/feature_flags/beta/duo_chat_multi_thread.yml deleted file mode 100644 index 1e8d65e39e00f2bbc79a7f551d9fe4f9a5b156cf..0000000000000000000000000000000000000000 --- a/ee/config/feature_flags/beta/duo_chat_multi_thread.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: duo_chat_multi_thread -feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/16108 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177977 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/519236 -milestone: '17.9' -group: group::duo chat -type: beta -default_enabled: true diff --git a/ee/lib/ai/conversation/legacy_duo_chat_copied_user.rb b/ee/lib/ai/conversation/legacy_duo_chat_copied_user.rb deleted file mode 100644 index c5f6cb9965016defcfe1af1e1dd93b064cb5f744..0000000000000000000000000000000000000000 --- a/ee/lib/ai/conversation/legacy_duo_chat_copied_user.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -# Record users who have attempted to copy legacy thread (including when none is available to copy) -# This will be removed after we remove `duo_chat_multi_thread` feature flag. -module Ai - module Conversation - module LegacyDuoChatCopiedUser - extend Gitlab::Redis::BackwardsCompatibility - - KEY = 'legacy_duo_chat_copied_user_set' - - class << self - def add(value) - Gitlab::Redis::SharedState.with do |redis| - redis.pipelined do |pipeline| - pipeline.sadd(KEY, value) - pipeline.expire(KEY, ::Ai::Conversation::Thread::MAX_EXPIRATION_PERIOD.to_i) - end - end - end - - def include?(value) - Gitlab::Redis::SharedState.with do |redis| - redis.sismember(KEY, value) - end - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/gon_helper.rb b/ee/lib/ee/gitlab/gon_helper.rb index 940249407c15c75082ea047b02c8746415703caf..4881d1242741a4acabf6c3b3cc05650163d41665 100644 --- a/ee/lib/ee/gitlab/gon_helper.rb +++ b/ee/lib/ee/gitlab/gon_helper.rb @@ -34,7 +34,6 @@ def add_gon_variables def push_frontend_feature_flags push_frontend_feature_flag(:duo_chat_dynamic_dimension, current_user) - push_frontend_feature_flag(:duo_chat_multi_thread, current_user) push_frontend_feature_flag(:advanced_context_resolver, current_user) push_frontend_feature_flag(:vulnerability_report_type_scanner_filter, current_user) end diff --git a/ee/spec/finders/ai/conversations/thread_finder_spec.rb b/ee/spec/finders/ai/conversations/thread_finder_spec.rb index 43ca74e74257663e6fe5ed182c7e70a8967bf61e..80cb2e426fc9a2d5e7b7646e96434584f73e39d0 100644 --- a/ee/spec/finders/ai/conversations/thread_finder_spec.rb +++ b/ee/spec/finders/ai/conversations/thread_finder_spec.rb @@ -34,80 +34,4 @@ expect(threads).to eq([thread_2, thread_1]) end end - - describe 'fallback behavior on absence of duo_chat threads' do - let(:finder) { described_class.new(user, params) } - - context 'when requesting duo chat threads' do - let(:params) { { conversation_type: 'duo_chat' } } - - context 'when there are no existing duo_chat threads' do - before do - user.ai_conversation_threads.duo_chat.delete_all - end - - context 'when there is a legacy thread' do - let_it_be(:legacy_thread) do - create(:ai_conversation_thread, user: user, conversation_type: 'duo_chat_legacy') - end - - it 'copies the last legacy thread as a new duo_chat thread' do - expect(::Ai::Conversation::LegacyDuoChatCopiedUser.include?(user.id)).to be(false) - - expect do - result = finder.execute - - expect(result.size).to eq(1) - - new_thread = result.first - - expect(new_thread.id).to be > legacy_thread.id - expect(new_thread.conversation_type).to eq('duo_chat') - end.to change { Ai::Conversation::Thread.count }.by(1) - - expect(::Ai::Conversation::LegacyDuoChatCopiedUser.include?(user.id)).to be(true) - end - - context 'when user has copied a thread in the past' do - it 'does not copy the legacy thread' do - allow(::Ai::Conversation::LegacyDuoChatCopiedUser).to receive(:include?).with(user.id).and_return(true) - - expect do - expect(finder.execute).to be_empty - end.not_to change { Ai::Conversation::Thread.count } - end - end - end - - context 'when there is no legacy thread' do - it 'returns an empty relation' do - expect do - expect(finder.execute).to be_empty - end.not_to change { Ai::Conversation::Thread.count } - - expect(::Ai::Conversation::LegacyDuoChatCopiedUser.include?(user.id)).to be(true) - end - end - end - - context 'when there are existing duo_chat threads' do - it 'does not copy legacy thread' do - expect do - result = finder.execute - expect(result.count).to eq(2) - end.not_to change { Ai::Conversation::Thread.count } - end - end - - context 'when a inaccessible ID is provided' do - let(:params) { { conversation_type: 'duo_chat', id: thread_3.id } } - - it 'does not create thread' do - expect do - expect(finder.execute).to be_empty - end.not_to change { Ai::Conversation::Thread.count } - end - end - end - end end diff --git a/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js b/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js index b8048ad4eb5ece625cfefedfa803a200fd122460..80e1ef655db97c1a71d81e14219efd6880a44951 100644 --- a/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js +++ b/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js @@ -235,14 +235,9 @@ describeSkipVue3(skipReason, () => { expect(duoChat.props('slashCommands')).toEqual([ { - description: 'Reset conversation and ignore previous messages.', - name: '/reset', - shouldSubmit: true, - }, - { - description: 'Delete all messages in the current conversation.', - name: '/clear', - shouldSubmit: true, + description: 'New chat conversation.', + name: '/new', + shouldSubmit: false, }, { description: 'Learn what Duo Chat can do.', @@ -332,8 +327,8 @@ describeSkipVue3(skipReason, () => { question: '/troubleshoot', resourceId: 'command::1', projectId: null, - conversationType: null, - threadId: null, + conversationType: 'DUO_CHAT', + threadId: undefined, }); }); }); @@ -362,11 +357,9 @@ describeSkipVue3(skipReason, () => { }); it.each([GENIE_CHAT_NEW_MESSAGE, GENIE_CHAT_RESET_MESSAGE, GENIE_CHAT_CLEAR_MESSAGE])( - 'resets chat state when "%s" command is sent and multi-thread is enabled', + 'resets chat state when "%s" command is sent', async (command) => { - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); findDuoChat().vm.$emit('send-chat-prompt', command); await nextTick(); @@ -380,7 +373,7 @@ describeSkipVue3(skipReason, () => { }, ); - it('does set loading to `true` for a message other than the reset or clear messages', () => { + it('does set loading to `true` unless a new chat is requested', () => { findDuoChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.content); expect(actionSpies.setLoading).toHaveBeenCalled(); }); @@ -404,8 +397,8 @@ describeSkipVue3(skipReason, () => { question: MOCK_USER_MESSAGE.content, resourceId: MOCK_USER_ID, projectId: 'project-123', - conversationType: null, - threadId: null, + conversationType: 'DUO_CHAT', + threadId: undefined, }); }); @@ -423,8 +416,8 @@ describeSkipVue3(skipReason, () => { question: MOCK_USER_MESSAGE.content, resourceId: MOCK_RESOURCE_ID, projectId: null, - conversationType: null, - threadId: null, + conversationType: 'DUO_CHAT', + threadId: undefined, }); }); @@ -452,16 +445,6 @@ describeSkipVue3(skipReason, () => { ); }); - it.each([GENIE_CHAT_RESET_MESSAGE, GENIE_CHAT_CLEAR_MESSAGE])( - 'does not set loading to `true` for "%s" message in single-thread mode', - async (msg) => { - actionSpies.setLoading.mockReset(); - findDuoChat().vm.$emit('send-chat-prompt', msg); - await nextTick(); - expect(actionSpies.setLoading).not.toHaveBeenCalled(); - }, - ); - describe.each` resourceId | expectedResourceId ${MOCK_RESOURCE_ID} | ${MOCK_RESOURCE_ID} @@ -480,31 +463,12 @@ describeSkipVue3(skipReason, () => { question: MOCK_USER_MESSAGE.content, clientSubscriptionId: '123', projectId: null, - conversationType: null, - threadId: null, + conversationType: 'DUO_CHAT', + threadId: undefined, }); }); }); - it.each([GENIE_CHAT_CLEAR_MESSAGE])( - 'refetches the `aiMessages` if the prompt is "%s" and does not call addDuoChatMessage', - async (prompt) => { - createComponent(); - - await waitForPromises(); - - queryHandlerMock.mockClear(); - actionSpies.addDuoChatMessage.mockClear(); - - findDuoChat().vm.$emit('send-chat-prompt', prompt); - - await waitForPromises(); - - expect(queryHandlerMock).toHaveBeenCalled(); - expect(actionSpies.addDuoChatMessage).not.toHaveBeenCalled(); - }, - ); - describe('tracking on mutation', () => { const expectedCategory = undefined; const expectedAction = 'submit_gitlab_duo_question'; @@ -858,29 +822,10 @@ describeSkipVue3(skipReason, () => { }); }); - describe('with duoChatMultiThread disabled', () => { - beforeEach(() => { - duoChatGlobalState.isShown = true; - createComponent({ - glFeatures: { duoChatMultiThread: false }, - }); - }); - - it('includes all slash commands', async () => { - await waitForPromises(); - const allCommands = findDuoChat().props().slashCommands; - expect(allCommands.some((cmd) => cmd.name === '/reset')).toBe(true); - expect(allCommands.some((cmd) => cmd.name === '/clear')).toBe(true); - expect(allCommands.some((cmd) => cmd.name === '/help')).toBe(true); - }); - }); - - describe('with duoChatMultiThread enabled', () => { + describe('multi-threaded chat functionality', () => { beforeEach(async () => { duoChatGlobalState.isShown = true; - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); await waitForPromises(); }); @@ -910,9 +855,7 @@ describeSkipVue3(skipReason, () => { it('uses correct conversation type DUO_CHAT when sending a message and active thread exists', async () => { threadQueryHandlerMock.mockResolvedValue(mockMessagesData); - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); findDuoChat().vm.$emit('thread-selected', { id: mockThreadId }); await waitForPromises(); @@ -1008,9 +951,7 @@ describeSkipVue3(skipReason, () => { it('returns to thread list view', async () => { conversationThreadsQueryHandlerMock.mockResolvedValue(MOCK_THREADS_RESPONSE); - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); const duoChat = findDuoChat(); await waitForPromises(); @@ -1066,9 +1007,7 @@ describeSkipVue3(skipReason, () => { }, }, }); - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); }); it('does not auto-select thread when in list view', async () => { @@ -1092,9 +1031,7 @@ describeSkipVue3(skipReason, () => { }, }); - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); const duoChat = findDuoChat(); @@ -1109,9 +1046,7 @@ describeSkipVue3(skipReason, () => { duoChatGlobalState.commands = [{ question: 'Button command' }]; - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); await waitForPromises(); @@ -1123,16 +1058,12 @@ describeSkipVue3(skipReason, () => { describe('aiConversationThreads query', () => { it.each` - isShown | hasMultiThread | shouldSkip | description - ${false} | ${false} | ${true} | ${'when chat is hidden and multi-thread is disabled'} - ${false} | ${true} | ${true} | ${'when chat is hidden and multi-thread is enabled'} - ${true} | ${false} | ${true} | ${'when chat is shown and multi-thread is disabled'} - ${true} | ${true} | ${false} | ${'when chat is shown and multi-thread is enabled'} - `('skips query=$shouldSkip $description', async ({ isShown, hasMultiThread, shouldSkip }) => { + isShown | shouldSkip | description + ${false} | ${true} | ${'when chat is hidden'} + ${true} | ${false} | ${'when chat is shown'} + `('skips query=$shouldSkip $description', async ({ isShown, shouldSkip }) => { duoChatGlobalState.isShown = isShown; - createComponent({ - glFeatures: { duoChatMultiThread: hasMultiThread }, - }); + createComponent(); await waitForPromises(); expect(conversationThreadsQueryHandlerMock).toHaveBeenCalledTimes(shouldSkip ? 0 : 1); @@ -1141,14 +1072,11 @@ describeSkipVue3(skipReason, () => { describe('aiMessages query', () => { it.each` - hasMultiThread | expectedType | description - ${false} | ${null} | ${'uses null conversation type when multi-thread is disabled'} - ${true} | ${'DUO_CHAT'} | ${'uses DUO_CHAT conversation type when multi-thread is enabled'} - `('$description', async ({ hasMultiThread, expectedType }) => { + expectedType | description + ${'DUO_CHAT'} | ${'uses DUO_CHAT conversation type'} + `('$description', async ({ expectedType }) => { duoChatGlobalState.isShown = true; - createComponent({ - glFeatures: { duoChatMultiThread: hasMultiThread }, - }); + createComponent(); await waitForPromises(); expect(queryHandlerMock).toHaveBeenCalledWith( @@ -1196,9 +1124,7 @@ describeSkipVue3(skipReason, () => { describe('duoChatGlobalState.isShown', () => { it('creates a new chat when Duo Chat is closed', async () => { duoChatGlobalState.isShown = true; - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); const onNewChatSpy = jest.spyOn(wrapper.vm, 'onNewChat'); @@ -1211,9 +1137,7 @@ describeSkipVue3(skipReason, () => { it('does not create a new chat when Duo Chat is opened', async () => { duoChatGlobalState.isShown = false; - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); const onNewChatSpy = jest.spyOn(wrapper.vm, 'onNewChat'); @@ -1227,9 +1151,7 @@ describeSkipVue3(skipReason, () => { duoChatGlobalState.isShown = false; duoChatGlobalState.commands = [{ question: 'Button command' }]; - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); const onNewChatSpy = jest.spyOn(wrapper.vm, 'onNewChat'); @@ -1245,9 +1167,7 @@ describeSkipVue3(skipReason, () => { describe('duoChatGlobalState.commands', () => { beforeEach(() => { duoChatGlobalState.isShown = true; - createComponent({ - glFeatures: { duoChatMultiThread: true }, - }); + createComponent(); }); afterEach(() => { diff --git a/ee/spec/frontend/ai/tanuki_bot/components/tanuki_bot_subscriptions_spec.js b/ee/spec/frontend/ai/tanuki_bot/components/tanuki_bot_subscriptions_spec.js index 9f3c90503f8f20fefd42a0be567bee071bb2dd46..2d6621efed08a71ffcb4c8fb32ca6a3308c6664e 100644 --- a/ee/spec/frontend/ai/tanuki_bot/components/tanuki_bot_subscriptions_spec.js +++ b/ee/spec/frontend/ai/tanuki_bot/components/tanuki_bot_subscriptions_spec.js @@ -49,7 +49,6 @@ describe('Ai Response Subscriptions', () => { }, provide: { glFeatures: { - duoChatMultiThread: false, ...provide.glFeatures, }, }, @@ -84,15 +83,12 @@ describe('Ai Response Subscriptions', () => { eventName, isStream = false, threadId = 'thread-1', - multiThread = false, shouldEmit = true, }) => { const requestId = '123'; const response = createResponseData(requestId, threadId, isStream); - createComponent({ - provide: { glFeatures: { duoChatMultiThread: multiThread } }, - }); + createComponent(); await waitForPromises(); const subscription = isStream ? mockSubscriptionStream : mockSubscriptionComplete; @@ -149,23 +145,14 @@ describe('Ai Response Subscriptions', () => { expect(emittedEvents[0]).toEqual([requestId]); }); - it('does not emit message-stream event when in multi-threaded mode and threadId does not match', () => { + it('does not emit message-stream event when threadId does not match', () => { return testMessageEmission({ eventName: 'message-stream', isStream: true, threadId: 'thread-2', - multiThread: true, shouldEmit: false, }); }); - - it('does emit message-stream event when in single-threaded mode and threadId does not match', () => { - return testMessageEmission({ - eventName: 'message-stream', - isStream: true, - threadId: 'thread-2', - }); - }); }); describe('aiCompletionResponse', () => { @@ -173,21 +160,13 @@ describe('Ai Response Subscriptions', () => { return testMessageEmission({ eventName: 'message' }); }); - it('does not emit message event when in multi-threaded mode and threadId does not match', () => { + it('does not emit message event when threadId does not match', () => { return testMessageEmission({ eventName: 'message', threadId: 'thread-2', - multiThread: true, shouldEmit: false, }); }); - - it('does emit message event when in single-threaded mode and threadId does not match', () => { - return testMessageEmission({ - eventName: 'message', - threadId: 'thread-2', - }); - }); }); }); }); diff --git a/ee/spec/frontend/ai/tanuki_bot/mock_data.js b/ee/spec/frontend/ai/tanuki_bot/mock_data.js index ed933d94e47f2e1efc89ed9cc48ff6056721bb4b..7cfb867ca65698dafdf72109f2a3b6617f2ed7fb 100644 --- a/ee/spec/frontend/ai/tanuki_bot/mock_data.js +++ b/ee/spec/frontend/ai/tanuki_bot/mock_data.js @@ -5,14 +5,9 @@ export const MOCK_SLASH_COMMANDS = { data: { aiSlashCommands: [ { - description: 'Reset conversation and ignore previous messages.', - name: '/reset', - shouldSubmit: true, - }, - { - description: 'Delete all messages in the current conversation.', - name: '/clear', - shouldSubmit: true, + description: 'New chat conversation.', + name: '/new', + shouldSubmit: false, }, { description: 'Learn what Duo Chat can do.', diff --git a/ee/spec/lib/ai/conversation/legacy_duo_chat_copied_user_spec.rb b/ee/spec/lib/ai/conversation/legacy_duo_chat_copied_user_spec.rb deleted file mode 100644 index b86fd470d18edb8fbcac378b52a433a99a4c37f2..0000000000000000000000000000000000000000 --- a/ee/spec/lib/ai/conversation/legacy_duo_chat_copied_user_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Ai::Conversation::LegacyDuoChatCopiedUser, :clean_gitlab_redis_shared_state, feature_category: :duo_chat do - let(:redis) { Gitlab::Redis::SharedState.with { |redis| redis } } - let(:user_id) { 123 } - - describe '.add' do - it 'adds a value to the Redis set' do - described_class.add(user_id) - ttl = redis.ttl(described_class::KEY) - - expect(redis.sismember(described_class::KEY, user_id)).to be true - expect(ttl).to be_within(5).of(30.days.to_i) - end - end - - describe '.include?' do - context 'when value exists in the set' do - it 'returns true' do - described_class.add(user_id) - - expect(described_class.include?(user_id)).to be true - end - end - - context 'when value does not exist in the set' do - it 'returns false' do - expect(described_class.include?(user_id)).to be false - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/gon_helper_spec.rb b/ee/spec/lib/ee/gitlab/gon_helper_spec.rb index 8d883d82150051cd6891e468d4b6fc4fb71cb82c..afd7253ecb7fac0cf9f09215ab324c9dc2f40d5f 100644 --- a/ee/spec/lib/ee/gitlab/gon_helper_spec.rb +++ b/ee/spec/lib/ee/gitlab/gon_helper_spec.rb @@ -56,7 +56,6 @@ def current_user end it_behaves_like 'pushes frontend feature flag', :duo_chat_dynamic_dimension - it_behaves_like 'pushes frontend feature flag', :duo_chat_multi_thread it_behaves_like 'pushes frontend feature flag', :advanced_context_resolver it_behaves_like 'pushes frontend feature flag', :vulnerability_report_type_scanner_filter end @@ -67,7 +66,6 @@ def current_user end it_behaves_like 'pushes frontend feature flag', :duo_chat_dynamic_dimension - it_behaves_like 'pushes frontend feature flag', :duo_chat_multi_thread it_behaves_like 'pushes frontend feature flag', :advanced_context_resolver it_behaves_like 'pushes frontend feature flag', :vulnerability_report_type_scanner_filter diff --git a/ee/spec/models/ai/conversation/thread_spec.rb b/ee/spec/models/ai/conversation/thread_spec.rb index 323232840e34000bdbdc68b5f0761ba68540c65b..4c746cc733638c6d4a6cc9fc186a0bdaf1847889 100644 --- a/ee/spec/models/ai/conversation/thread_spec.rb +++ b/ee/spec/models/ai/conversation/thread_spec.rb @@ -98,57 +98,4 @@ it_behaves_like 'it has loose foreign keys' do let(:factory_name) { :ai_conversation_thread } end - - describe '#dup_as_duo_chat_thread!' do - let_it_be(:original_thread) do - create(:ai_conversation_thread, :with_messages, conversation_type: :duo_chat_legacy) - end - - subject(:duplicated_thread) { original_thread.dup_as_duo_chat_thread! } - - it 'clones thread and messages' do - expect do - duplicated_thread - end.to change { described_class.count }.by(1) - .and change { Ai::Conversation::Message.count }.by(2) - - expect(duplicated_thread.conversation_type).to eq('duo_chat') - - # Verify attributes - expect(duplicated_thread).to have_attributes( - conversation_type: 'duo_chat', - user_id: original_thread.user_id, - organization_id: original_thread.organization_id - ) - expect(duplicated_thread.id).not_to eq(original_thread.id) - - original_message = original_thread.messages.first - duplicated_message = duplicated_thread.messages.first - - expect(duplicated_message).to have_attributes( - content: original_message.content, - role: original_message.role, - thread_id: duplicated_thread.id - ) - expect(duplicated_message.id).not_to eq(original_message.id) - expect(duplicated_message.thread_id).not_to eq(original_message.thread_id) - end - - context 'when transaction fails' do - before do - allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) - end - end - - it 'does not create any records' do - expect do - duplicated_thread - rescue ActiveRecord::RecordInvalid - nil - end.to not_change(described_class, :count) - .and not_change(Ai::Conversation::Message, :count) - end - end - end end diff --git a/ee/spec/services/ai/slash_commands_service_spec.rb b/ee/spec/services/ai/slash_commands_service_spec.rb index 296e7a11e7013104d9dd089616f9ca881d023b7e..ffbf55316ae0e4a3054258b5d8fe7c32cea3cdb3 100644 --- a/ee/spec/services/ai/slash_commands_service_spec.rb +++ b/ee/spec/services/ai/slash_commands_service_spec.rb @@ -168,16 +168,5 @@ it_behaves_like 'returns only base commands' end - - context 'when duo_chat_multi_thread is disabled' do - let(:url) { nil } - - it 'returns deprecated commands' do - stub_feature_flags(duo_chat_multi_thread: false) - - expect(available_commands.pluck(:name)).to include('/reset', '/clear') - expect(available_commands.pluck(:name)).not_to include('/new') - end - end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b5f8fcbfed4602678726d8816e7a28a275be66c1..bb464916be47d2f1649289c52442e63cc03f745f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20725,9 +20725,6 @@ msgstr "" msgid "Delete account" msgstr "" -msgid "Delete all messages in the current conversation." -msgstr "" - msgid "Delete asset" msgstr "" @@ -51000,9 +50997,6 @@ msgstr "" msgid "Reset approvals on push" msgstr "" -msgid "Reset conversation and ignore previous messages." -msgstr "" - msgid "Reset error tracking access token" msgstr ""