From a656e36e93e960c88d87634597c37426b0ba5637 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 7 Oct 2025 19:19:38 +0200 Subject: [PATCH 01/17] chore(compass-e2e-tests): add assistant end to end tests COMPASS-9384 --- .../helpers/assistant-service.ts | 248 +++++++++++ .../helpers/commands/open-settings-modal.ts | 4 +- .../compass-e2e-tests/helpers/selectors.ts | 18 +- .../compass-e2e-tests/tests/assistant.test.ts | 398 ++++++++++++++++++ .../src/components/ai-optin-modal.tsx | 1 + 5 files changed, 666 insertions(+), 3 deletions(-) create mode 100644 packages/compass-e2e-tests/helpers/assistant-service.ts create mode 100644 packages/compass-e2e-tests/tests/assistant.test.ts diff --git a/packages/compass-e2e-tests/helpers/assistant-service.ts b/packages/compass-e2e-tests/helpers/assistant-service.ts new file mode 100644 index 00000000000..d5a16f14824 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/assistant-service.ts @@ -0,0 +1,248 @@ +import http from 'http'; +import { once } from 'events'; +import type { AddressInfo } from 'net'; + +export type MockAssistantResponse = { + status: number; + body: string; +}; + +function sendStreamingResponse(res: http.ServerResponse, content: string) { + // OpenAI Responses API streaming response format using Server-Sent Events + res.writeHead(200, { + 'Content-Type': 'text/event-stream; charset=utf-8', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Transfer-Encoding': 'chunked', + }); + + const responseId = `resp_${Date.now()}`; + const itemId = `item_${Date.now()}`; + let sequenceNumber = 0; + + // Send response.created event + res.write( + `data: ${JSON.stringify({ + type: 'response.created', + response: { + id: responseId, + object: 'realtime.response', + status: 'in_progress', + output: [], + usage: { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + }, + }, + sequence_number: sequenceNumber++, + })}\n\n` + ); + + // Send output_item.added event + res.write( + `data: ${JSON.stringify({ + type: 'response.output_item.added', + response_id: responseId, + output_index: 0, + item: { + id: itemId, + object: 'realtime.item', + type: 'message', + role: 'assistant', + content: [], + }, + sequence_number: sequenceNumber++, + })}\n\n` + ); + + // Send the content in chunks + const words = content.split(' '); + let index = 0; + + const sendChunk = () => { + if (index < words.length) { + const word = words[index] + (index < words.length - 1 ? ' ' : ''); + // Send output_text.delta event + res.write( + `data: ${JSON.stringify({ + type: 'response.output_text.delta', + response_id: responseId, + item_id: itemId, + output_index: 0, + delta: word, + sequence_number: sequenceNumber++, + })}\n\n` + ); + index++; + setTimeout(sendChunk, 10); + } else { + // Send output_item.done event + res.write( + `data: ${JSON.stringify({ + type: 'response.output_item.done', + response_id: responseId, + output_index: 0, + item: { + id: itemId, + object: 'realtime.item', + type: 'message', + role: 'assistant', + content: [ + { + type: 'text', + text: content, + }, + ], + }, + sequence_number: sequenceNumber++, + })}\n\n` + ); + + // Send response.completed event + const tokenCount = Math.ceil(content.split(' ').length * 1.3); + res.write( + `data: ${JSON.stringify({ + type: 'response.completed', + response: { + id: responseId, + object: 'realtime.response', + status: 'completed', + output: [ + { + id: itemId, + object: 'realtime.item', + type: 'message', + role: 'assistant', + content: [ + { + type: 'text', + text: content, + }, + ], + }, + ], + usage: { + input_tokens: 10, + output_tokens: tokenCount, + total_tokens: 10 + tokenCount, + }, + }, + sequence_number: sequenceNumber++, + })}\n\n` + ); + + res.write('data: [DONE]\n\n'); + res.end(); + } + }; + + sendChunk(); +} + +export async function startMockAssistantServer( + { + response: _response, + }: { + response: MockAssistantResponse; + } = { + response: { + status: 200, + body: 'This is a test response from the AI assistant.', + }, + } +): Promise<{ + clearRequests: () => void; + getResponse: () => MockAssistantResponse; + setResponse: (response: MockAssistantResponse) => void; + getRequests: () => { + content: any; + req: any; + }[]; + endpoint: string; + server: http.Server; + stop: () => Promise; +}> { + let requests: { + content: any; + req: any; + }[] = []; + let response = _response; + const server = http + .createServer((req, res) => { + // Only handle POST requests for chat completions + if (req.method !== 'POST') { + res.writeHead(404); + return res.end('Not Found'); + } + + let body = ''; + req + .setEncoding('utf8') + .on('data', (chunk) => { + body += chunk; + }) + .on('end', () => { + let jsonObject; + try { + jsonObject = JSON.parse(body); + } catch { + res.writeHead(400); + res.setHeader('Content-Type', 'application/json'); + return res.end(JSON.stringify({ error: 'Invalid JSON' })); + } + + requests.push({ + req, + content: jsonObject, + }); + + if (response.status !== 200) { + res.writeHead(response.status); + res.setHeader('Content-Type', 'application/json'); + return res.end(JSON.stringify({ error: response.body })); + } + + // Send streaming response + return sendStreamingResponse(res, response.body); + }); + }) + .listen(0); + await once(server, 'listening'); + + // address() returns either a string or AddressInfo. + const address = server.address() as AddressInfo; + + const endpoint = `http://localhost:${address.port}`; + + async function stop() { + server.close(); + await once(server, 'close'); + } + + function clearRequests() { + requests = []; + } + + function getRequests() { + return requests; + } + + function getResponse() { + return response; + } + + function setResponse(newResponse: MockAssistantResponse) { + response = newResponse; + } + + return { + clearRequests, + getRequests, + endpoint, + server, + getResponse, + setResponse, + stop, + }; +} diff --git a/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts b/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts index bee67004904..89672b0a5d3 100644 --- a/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts +++ b/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts @@ -1,10 +1,11 @@ +import type { ChainablePromiseElement } from 'webdriverio'; import type { CompassBrowser } from '../compass-browser'; import * as Selectors from '../selectors'; export async function openSettingsModal( browser: CompassBrowser, tab?: string -): Promise { +): Promise { await browser.execute(() => { // eslint-disable-next-line @typescript-eslint/no-require-imports require('electron').ipcRenderer.emit('window:show-settings'); @@ -15,4 +16,5 @@ export async function openSettingsModal( if (tab) { await browser.clickVisible(Selectors.SettingsModalTabSelector(tab)); } + return settingsModalElement; } diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 5da7673a9f1..2dc1b9251ba 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -16,6 +16,9 @@ export const SettingsModalTabSelector = (name: string) => `${SettingsModal} [data-testid="sidebar-${name}-item"]`; export const GeneralSettingsButton = SettingsModalTabSelector('general'); export const GeneralSettingsContent = `${SettingsModal} [data-testid="general-settings"]`; +export const ArtificialIntelligenceSettingsButton = + SettingsModalTabSelector('ai'); +export const ArtificialIntelligenceSettingsContent = `${SettingsModal} [data-testid="gen-ai-settings"]`; export const SettingsInputElement = (settingName: string): string => { return `${SettingsModal} [data-testid="${settingName}"]`; @@ -1510,8 +1513,19 @@ export const SideDrawerCloseButton = `[data-testid="${ }"]`; // Assistant +export const AssistantDrawerButton = 'button[aria-label="MongoDB Assistant"]'; +export const AssistantDrawerCloseButton = `[data-testid="lg-drawer-close_button"]`; export const AssistantChatMessages = '[data-testid="assistant-chat-messages"]'; +export const AssistantChatMessage = '[data-testid^="assistant-message-"]'; +export const AssistantChatInput = '[data-testid="assistant-chat-input"]'; +export const AssistantChatInputTextArea = `${AssistantChatInput} textarea`; +export const AssistantChatSubmitButton = `${AssistantChatInput} button[aria-label="Send message"]`; export const AssistantClearChatButton = '[data-testid="assistant-clear-chat"]'; -export const ConfirmClearChatModal = +export const AssistantConfirmClearChatModal = '[data-testid="assistant-confirm-clear-chat-modal"]'; -export const ConfirmClearChatModalConfirmButton = `${ConfirmClearChatModal} [data-testid="lg-confirmation_modal-footer-confirm_button"]`; +export const AssistantConfirmClearChatModalConfirmButton = `${AssistantConfirmClearChatModal} [data-testid="lg-confirmation_modal-footer-confirm_button"]`; + +// AI Opt-in Modal +export const AIOptInModal = '[data-testid="ai-optin-modal"]'; +export const AIOptInModalAcceptButton = 'button=Use AI Features'; +export const AIOptInModalDeclineLink = 'span=Not now'; diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts new file mode 100644 index 00000000000..9e14f06e1e9 --- /dev/null +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -0,0 +1,398 @@ +import { expect } from 'chai'; + +import type { CompassBrowser } from '../helpers/compass-browser'; +import { startTelemetryServer } from '../helpers/telemetry'; +import type { Telemetry } from '../helpers/telemetry'; +import { + init, + cleanup, + screenshotIfFailed, + skipForWeb, + TEST_COMPASS_WEB, +} from '../helpers/compass'; +import type { Compass } from '../helpers/compass'; +import * as Selectors from '../helpers/selectors'; +import { startMockAtlasServiceServer } from '../helpers/atlas-service'; +import { startMockAssistantServer } from '../helpers/assistant-service'; +import type { MockAssistantResponse } from '../helpers/assistant-service'; +import { openSettingsModal } from '../helpers/commands'; + +async function setAIFeatures(browser: CompassBrowser, newValue: boolean) { + await openSettingsModal(browser, 'ai'); + + // Wait for AI settings content to be visible + const aiSettingsContent = browser.$( + Selectors.ArtificialIntelligenceSettingsContent + ); + await aiSettingsContent.waitForDisplayed(); + + const currentValue = + (await browser + .$(Selectors.SettingsInputElement('enableGenAIFeatures')) + .getAttribute('aria-checked')) === 'true'; + + if (currentValue !== newValue) { + await browser.clickParent( + Selectors.SettingsInputElement('enableGenAIFeatures') + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + } + + await browser.clickVisible(Selectors.CloseSettingsModalButton); +} + +async function setAIOptIn(browser: CompassBrowser, enabled: boolean) { + // Reset the opt-in preference by using the execute command + await browser.execute((value: boolean) => { + // Access the preferences API to reset the opt-in + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { ipcRenderer } = require('electron'); + ipcRenderer.invoke('compass:save-preferences', { + optInGenAIFeatures: value, + }); + }, enabled); + + await browser.pause(100); +} + +describe('MongoDB Assistant', function () { + let compass: Compass; + let browser: CompassBrowser; + let telemetry: Telemetry; + let openAssistantDrawer: () => Promise; + + let mockAtlasServer: Awaited>; + let mockAssistantServer: Awaited>; + let clearChat: () => Promise; + let sendMessage: ( + text: string, + options?: { + response?: MockAssistantResponse; + } + ) => Promise; + let getDisplayedMessages: () => Promise< + { + text: string; + role: 'user' | 'assistant'; + }[] + >; + + const testMessage = 'What is MongoDB?'; + const testResponse = 'MongoDB is a database.'; + + before(async function () { + skipForWeb(this, 'ai assistant not yet available in compass-web'); + + process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN = 'true'; + + // Start a mock Atlas service for feature flag checks + mockAtlasServer = await startMockAtlasServiceServer(); + + // Start a mock Assistant server for AI chat responses + mockAssistantServer = await startMockAssistantServer(); + + // Set env vars: one for feature flag checks, one for assistant API + process.env.COMPASS_ATLAS_SERVICE_UNAUTH_BASE_URL_OVERRIDE = + mockAtlasServer.endpoint; + process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE = + mockAssistantServer.endpoint; + + telemetry = await startTelemetryServer(); + compass = await init(this.test?.fullTitle()); + browser = compass.browser; + + openAssistantDrawer = async () => { + const drawerButton = browser.$(Selectors.AssistantDrawerButton); + await drawerButton.waitForDisplayed(); + await drawerButton.waitForClickable(); + await drawerButton.click(); + }; + + clearChat = async () => { + const clearChatButton = browser.$(Selectors.AssistantClearChatButton); + if (await clearChatButton.isDisplayed()) { + await clearChatButton.click(); + const confirmButton = browser.$( + Selectors.AssistantConfirmClearChatModalConfirmButton + ); + await confirmButton.waitForClickable(); + await confirmButton.click(); + } + }; + + sendMessage = async ( + text: string, + { + response = { + status: 200, + body: testResponse, + }, + }: { response?: MockAssistantResponse } = {} + ) => { + mockAssistantServer.setResponse(response); + + const chatInput = browser.$(Selectors.AssistantChatInputTextArea); + await chatInput.waitForDisplayed(); + await chatInput.setValue(text); + const submitButton = browser.$(Selectors.AssistantChatSubmitButton); + await submitButton.click(); + }; + + getDisplayedMessages = async () => { + // Wait for the messages container to be visible + const chatMessages = browser.$(Selectors.AssistantChatMessages); + await chatMessages.waitForDisplayed(); + + // Get all individual message elements + const messageElements = await browser + .$$(Selectors.AssistantChatMessage) + .getElements(); + + const displayedMessages = []; + for (const messageElement of messageElements) { + const textElements = await messageElement.$$('p').getElements(); + const isAssistantMessage = + textElements.length !== 1 && + (await textElements[0].getText()) === 'MongoDB Assistant'; + // Get the message text content. + // In case of Assistant messages, skip the MongoDB Assistant text. + const text = isAssistantMessage + ? await textElements[1].getText() + : await textElements[0].getText(); + + displayedMessages.push({ + text: text, + role: isAssistantMessage ? ('assistant' as const) : ('user' as const), + }); + } + + return displayedMessages; + }; + }); + + after(async function () { + if (TEST_COMPASS_WEB) { + return; + } + + await mockAtlasServer.stop(); + await mockAssistantServer.stop(); + + delete process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN; + delete process.env.COMPASS_ATLAS_SERVICE_UNAUTH_BASE_URL_OVERRIDE; + delete process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE; + + await cleanup(compass); + await telemetry.stop(); + }); + + afterEach(async function () { + mockAssistantServer.clearRequests(); + await clearChat(); + + await screenshotIfFailed(compass, this.currentTest); + }); + + describe('drawer visibility', function () { + it('shows the assistant drawer button when AI features are enabled', async function () { + await setAIFeatures(browser, true); + + // AI Features are enabled by default so the drawer button should be visible. + const drawerButton = browser.$(Selectors.AssistantDrawerButton); + await drawerButton.waitForDisplayed(); + expect(await drawerButton.isDisplayed()).to.be.true; + }); + + it('does not show the assistant drawer button when AI features are disabled', async function () { + await setAIFeatures(browser, false); + + // Assistant drawer button should not be visible + const drawerButton = browser.$(Selectors.AssistantDrawerButton); + await drawerButton.waitForDisplayed({ reverse: true }); + expect(await drawerButton.isDisplayed()).to.be.false; + + await setAIFeatures(browser, true); + }); + + it('can close and open the assistant drawer', async function () { + await openAssistantDrawer(); + + await browser.$(Selectors.AssistantDrawerCloseButton).waitForDisplayed(); + + await browser.clickVisible(Selectors.AssistantDrawerCloseButton); + + await browser.$(Selectors.AssistantDrawerCloseButton).waitForDisplayed({ + reverse: true, + }); + + await browser.clickVisible(Selectors.AssistantDrawerButton); + + await browser.$(Selectors.AssistantDrawerCloseButton).waitForDisplayed(); + }); + }); + + describe('before opt-in', function () { + beforeEach(async function () { + await setAIOptIn(browser, false); + }); + + it('does not send the message if the user declines the opt-in', async function () { + await openAssistantDrawer(); + + await sendMessage(testMessage); + + // Wait for opt-in modal and decline it + const declineLink = browser.$(Selectors.AIOptInModalDeclineLink); + await declineLink.waitForDisplayed(); + await declineLink.click(); + + // Wait for the modal to close + const optInModal = browser.$(Selectors.AIOptInModal); + await optInModal.waitForDisplayed({ reverse: true }); + + // Verify the input was not cleared after sending + const chatInput = browser.$(Selectors.AssistantChatInputTextArea); + expect(await chatInput.getValue()).not.to.equal(testMessage); + + // Verify the message is not displayed in the chat + const chatMessages = browser.$(Selectors.AssistantChatMessages); + expect(await chatMessages.getText()).to.not.include(testMessage); + + expect(mockAssistantServer.getRequests()).to.be.empty; + }); + + it('sends the message if the user opts in', async function () { + await openAssistantDrawer(); + + await sendMessage(testMessage); + + const optInModal = browser.$(Selectors.AIOptInModal); + await optInModal.waitForDisplayed(); + expect(await optInModal.isDisplayed()).to.be.true; + + const acceptButton = browser.$(Selectors.AIOptInModalAcceptButton); + await acceptButton.waitForClickable(); + await acceptButton.click(); + + await optInModal.waitForDisplayed({ reverse: true }); + + const chatInput = browser.$(Selectors.AssistantChatInputTextArea); + expect(await chatInput.getValue()).to.equal(''); + + expect(await getDisplayedMessages()).to.deep.equal([ + { text: testMessage, role: 'user' }, + { text: testResponse, role: 'assistant' }, + ]); + }); + }); + + describe('after opt-in', function () { + beforeEach(async function () { + await setAIOptIn(browser, true); + + await openAssistantDrawer(); + }); + + describe('clear chat button', function () { + it('appears only after a message is sent', async function () { + const clearChatButton = browser.$(Selectors.AssistantClearChatButton); + expect(await clearChatButton.isDisplayed()).to.be.false; + }); + + it('should clear the chat when the user clicks the clear chat button', async function () { + await openAssistantDrawer(); + await sendMessage(testMessage); + await sendMessage(testMessage); + expect(await getDisplayedMessages()).to.deep.equal([ + { text: testMessage, role: 'user' }, + { text: testResponse, role: 'assistant' }, + { text: testMessage, role: 'user' }, + { text: testResponse, role: 'assistant' }, + ]); + + await clearChat(); + + expect(await getDisplayedMessages()).to.deep.equal([]); + }); + }); + + it('displays multiple messages correctly', async function () { + await sendMessage(testMessage); + + await sendMessage('This is a different message', { + response: { + status: 200, + body: 'This is a different response', + }, + }); + + expect(await getDisplayedMessages()).to.deep.equal([ + { text: testMessage, role: 'user' }, + { text: testResponse, role: 'assistant' }, + { text: 'This is a different message', role: 'user' }, + { text: 'This is a different response', role: 'assistant' }, + ]); + }); + + it('can copy assistant message to clipboard', async function () { + await sendMessage(testMessage); + + await browser.pause(100); + + const messageElements = await browser + .$$(Selectors.AssistantChatMessage) + .getElements(); + + const assistantMessage = messageElements[1]; + + const copyButton = assistantMessage.$('[aria-label="Copy message"]'); + await copyButton.waitForDisplayed(); + await copyButton.click(); + + await browser.pause(100); + + const clipboardText = await browser.execute(() => { + return navigator.clipboard.readText(); + }); + + expect(clipboardText).to.equal(testResponse); + }); + + it('can submit feedback with text', async function () { + await sendMessage(testMessage); + + await browser.pause(100); + + // Get all message elements + const messageElements = await browser + .$$(Selectors.AssistantChatMessage) + .getElements(); + + const assistantMessage = messageElements[1]; + + const thumbsDownButton = assistantMessage.$( + '[aria-label="Dislike this message"]' + ); + await thumbsDownButton.waitForDisplayed(); + await thumbsDownButton.click(); + + const feedbackTextarea = assistantMessage.$('textarea'); + await feedbackTextarea.waitForDisplayed(); + + await feedbackTextarea.setValue('This is a test feedback'); + + const submitButton = browser.$('button*=Submit'); + await submitButton.waitForClickable(); + await submitButton.click(); + + await feedbackTextarea.waitForDisplayed({ reverse: true }); + + const thumbsDownButtonAfter = assistantMessage.$( + '[aria-label="Dislike this message"]' + ); + expect(await thumbsDownButtonAfter.getAttribute('aria-checked')).to.equal( + 'true' + ); + }); + }); +}); diff --git a/packages/compass-generative-ai/src/components/ai-optin-modal.tsx b/packages/compass-generative-ai/src/components/ai-optin-modal.tsx index 175be53c93d..2d2989d4628 100644 --- a/packages/compass-generative-ai/src/components/ai-optin-modal.tsx +++ b/packages/compass-generative-ai/src/components/ai-optin-modal.tsx @@ -206,6 +206,7 @@ export const AIOptInModal: React.FunctionComponent = ({ title={`Use AI Features in ${isCloudOptIn ? 'Data Explorer' : 'Compass'}`} open={isOptInModalVisible} onClose={handleModalClose} + data-testid="ai-optin-modal" // TODO Button Disabling className={!isProjectAIEnabled ? currentDisabledButtonStyles : undefined} buttonText="Use AI Features" From 4d1cddcf7af4af5926d89f586225c83d91feaaf0 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 7 Oct 2025 21:01:33 +0200 Subject: [PATCH 02/17] chore: add entry points --- .../compass-e2e-tests/helpers/selectors.ts | 3 + .../compass-e2e-tests/tests/assistant.test.ts | 177 ++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 2dc1b9251ba..54ec3a6db90 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -897,6 +897,9 @@ export const AggregationSavedPipelineCardDeleteButton = ( export const AggregationExplainButton = '[data-testid="pipeline-toolbar-explain-aggregation-button"]'; export const AggregationExplainModal = '[data-testid="explain-plan-modal"]'; +export const ExplainPlanInterpretButton = + '[data-testid="interpret-for-me-button"]'; +export const ExplainPlanCloseButton = '[data-testid="explain-close-button"]'; export const AggregationExplainModalCloseButton = `${AggregationExplainModal} [aria-label*="Close"]`; // Create view from pipeline modal diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 9e14f06e1e9..c54a6e3d7f5 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -9,6 +9,7 @@ import { screenshotIfFailed, skipForWeb, TEST_COMPASS_WEB, + DEFAULT_CONNECTION_NAME_1, } from '../helpers/compass'; import type { Compass } from '../helpers/compass'; import * as Selectors from '../helpers/selectors'; @@ -395,4 +396,180 @@ describe('MongoDB Assistant', function () { ); }); }); + + describe('entry points', function () { + const dbName = 'test'; + const collectionName = 'entryPoints'; + before(async function () { + await browser.setupDefaultConnections(); + await browser.connectToDefaults(); + await browser.selectConnectionMenuItem( + DEFAULT_CONNECTION_NAME_1, + Selectors.CreateDatabaseButton, + false + ); + await browser.addDatabase(dbName, collectionName); + }); + + describe('explain plan entry point', function () { + let useExplainPlanEntryPoint: () => Promise; + + before(async function () { + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAME_1, + dbName, + collectionName, + 'Aggregations' + ); + await setAIOptIn(browser, true); + await setAIFeatures(browser, true); + + mockAssistantServer.setResponse({ + status: 200, + body: 'You should create an index.', + }); + }); + + beforeEach(function () { + useExplainPlanEntryPoint = async () => { + // Open explain plan modal by clicking the explain button + const explainButton = browser.$(Selectors.AggregationExplainButton); + await explainButton.waitForDisplayed(); + await explainButton.click(); + + // Wait for the explain plan modal to open and finish loading + const explainModal = browser.$(Selectors.AggregationExplainModal); + await explainModal.waitForDisplayed(); + + // Wait for the explain plan to be ready (loader should disappear) + const explainLoader = browser.$(Selectors.ExplainLoader); + await explainLoader.waitForDisplayed({ + reverse: true, + timeout: 10000, + }); + + // Click the "Interpret for me" button + const interpretButton = browser.$( + Selectors.ExplainPlanInterpretButton + ); + await interpretButton.waitForDisplayed(); + await interpretButton.click(); + + // The modal should close + await explainModal.waitForDisplayed({ reverse: true }); + + // The assistant drawer should open + const assistantDrawer = browser.$(Selectors.SideDrawer); + await assistantDrawer.waitForDisplayed(); + }; + }); + + it('opens assistant with explain plan prompt when clicking "Interpret for me"', async function () { + await useExplainPlanEntryPoint(); + + const confirmButton = browser.$('button*=Confirm'); + await confirmButton.waitForDisplayed(); + await confirmButton.click(); + + await browser.pause(100); + + const messages = await getDisplayedMessages(); + expect(messages).deep.equal([ + { text: 'Interpret this explain plan output for me.', role: 'user' }, + { text: 'You should create an index.', role: 'assistant' }, + ]); + + expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); + }); + + it('does not send request when user cancels confirmation', async function () { + // Navigate to collection Aggregations tab + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAME_1, + dbName, + collectionName, + 'Aggregations' + ); + + await useExplainPlanEntryPoint(); + + const chatMessages = browser.$(Selectors.AssistantChatMessages); + await chatMessages.waitForDisplayed(); + expect(await chatMessages.getText()).to.include( + 'Please confirm your request' + ); + + // Click Cancel button + const cancelButton = browser.$('button*=Cancel'); + await cancelButton.waitForDisplayed(); + await cancelButton.click(); + + // Wait a bit to ensure no request is sent + await browser.pause(300); + + const finalMessages = await getDisplayedMessages(); + expect(finalMessages.length).to.equal(0); + + expect(await chatMessages.getText()).to.include( + 'Please confirm your request' + ); + expect(await chatMessages.getText()).to.include('Request cancelled'); + + // Verify no assistant request was made + expect(mockAssistantServer.getRequests()).to.be.empty; + }); + }); + + describe('error message view entry point', function () { + let useErrorViewEntryPoint: () => Promise; + before(async function () { + await setAIOptIn(browser, true); + + mockAssistantServer.setResponse({ + status: 200, + body: 'You should review the connection string.', + }); + useErrorViewEntryPoint = async () => { + const connectionToastErrorDebugButton = browser.$( + Selectors.ConnectionToastErrorDebugButton + ); + await connectionToastErrorDebugButton.waitForDisplayed(); + await connectionToastErrorDebugButton.click(); + }; + }); + + it('opens assistant with error message view prompt when clicking "Debug for me"', async function () { + void (await browser.connectWithConnectionString( + 'mongodb-invalid://localhost:27017' + )); + await useErrorViewEntryPoint(); + + const messages = await getDisplayedMessages(); + expect(messages).deep.equal([ + { + text: 'Diagnose why my Compass connection is failing and help me debug it.', + role: 'user', + }, + { + text: 'You should review the connection string.', + role: 'assistant', + }, + ]); + + expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); + }); + + it('should display opt-in modal when clicking "Debug for me" without opt-in ', async function () { + await setAIOptIn(browser, false); + void (await browser.connectWithConnectionString( + 'mongodb-invalid://localhost:27017' + )); + await useErrorViewEntryPoint(); + + const optInModal = browser.$(Selectors.AIOptInModal); + await optInModal.waitForDisplayed(); + expect(await optInModal.isDisplayed()).to.be.true; + }); + }); + }); }); From bd6fc2fe7255a1088a72f6a45438b96affe18883 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 7 Oct 2025 21:03:50 +0200 Subject: [PATCH 03/17] chore: fixup --- .../compass-e2e-tests/helpers/commands/open-settings-modal.ts | 4 +--- packages/compass-e2e-tests/tests/assistant.test.ts | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts b/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts index 89672b0a5d3..bee67004904 100644 --- a/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts +++ b/packages/compass-e2e-tests/helpers/commands/open-settings-modal.ts @@ -1,11 +1,10 @@ -import type { ChainablePromiseElement } from 'webdriverio'; import type { CompassBrowser } from '../compass-browser'; import * as Selectors from '../selectors'; export async function openSettingsModal( browser: CompassBrowser, tab?: string -): Promise { +): Promise { await browser.execute(() => { // eslint-disable-next-line @typescript-eslint/no-require-imports require('electron').ipcRenderer.emit('window:show-settings'); @@ -16,5 +15,4 @@ export async function openSettingsModal( if (tab) { await browser.clickVisible(Selectors.SettingsModalTabSelector(tab)); } - return settingsModalElement; } diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index c54a6e3d7f5..6ba1644c0ce 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -16,10 +16,9 @@ import * as Selectors from '../helpers/selectors'; import { startMockAtlasServiceServer } from '../helpers/atlas-service'; import { startMockAssistantServer } from '../helpers/assistant-service'; import type { MockAssistantResponse } from '../helpers/assistant-service'; -import { openSettingsModal } from '../helpers/commands'; async function setAIFeatures(browser: CompassBrowser, newValue: boolean) { - await openSettingsModal(browser, 'ai'); + await browser.openSettingsModal('ai'); // Wait for AI settings content to be visible const aiSettingsContent = browser.$( From dccb3542545a70f41c8c1d862f4e590dc7425c90 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 10:25:44 +0200 Subject: [PATCH 04/17] chore: cleanup and fix --- .../compass-e2e-tests/tests/assistant.test.ts | 549 +++++++++--------- 1 file changed, 284 insertions(+), 265 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 6ba1644c0ce..332d8b1738f 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -17,68 +17,24 @@ import { startMockAtlasServiceServer } from '../helpers/atlas-service'; import { startMockAssistantServer } from '../helpers/assistant-service'; import type { MockAssistantResponse } from '../helpers/assistant-service'; -async function setAIFeatures(browser: CompassBrowser, newValue: boolean) { - await browser.openSettingsModal('ai'); - - // Wait for AI settings content to be visible - const aiSettingsContent = browser.$( - Selectors.ArtificialIntelligenceSettingsContent - ); - await aiSettingsContent.waitForDisplayed(); - - const currentValue = - (await browser - .$(Selectors.SettingsInputElement('enableGenAIFeatures')) - .getAttribute('aria-checked')) === 'true'; - - if (currentValue !== newValue) { - await browser.clickParent( - Selectors.SettingsInputElement('enableGenAIFeatures') - ); - await browser.clickVisible(Selectors.SaveSettingsButton); - } - - await browser.clickVisible(Selectors.CloseSettingsModalButton); -} - -async function setAIOptIn(browser: CompassBrowser, enabled: boolean) { - // Reset the opt-in preference by using the execute command - await browser.execute((value: boolean) => { - // Access the preferences API to reset the opt-in - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { ipcRenderer } = require('electron'); - ipcRenderer.invoke('compass:save-preferences', { - optInGenAIFeatures: value, - }); - }, enabled); - - await browser.pause(100); -} - describe('MongoDB Assistant', function () { let compass: Compass; let browser: CompassBrowser; let telemetry: Telemetry; - let openAssistantDrawer: () => Promise; let mockAtlasServer: Awaited>; let mockAssistantServer: Awaited>; - let clearChat: () => Promise; let sendMessage: ( text: string, options?: { response?: MockAssistantResponse; } ) => Promise; - let getDisplayedMessages: () => Promise< - { - text: string; - role: 'user' | 'assistant'; - }[] - >; const testMessage = 'What is MongoDB?'; const testResponse = 'MongoDB is a database.'; + const dbName = 'test'; + const collectionName = 'entryPoints'; before(async function () { skipForWeb(this, 'ai assistant not yet available in compass-web'); @@ -101,25 +57,6 @@ describe('MongoDB Assistant', function () { compass = await init(this.test?.fullTitle()); browser = compass.browser; - openAssistantDrawer = async () => { - const drawerButton = browser.$(Selectors.AssistantDrawerButton); - await drawerButton.waitForDisplayed(); - await drawerButton.waitForClickable(); - await drawerButton.click(); - }; - - clearChat = async () => { - const clearChatButton = browser.$(Selectors.AssistantClearChatButton); - if (await clearChatButton.isDisplayed()) { - await clearChatButton.click(); - const confirmButton = browser.$( - Selectors.AssistantConfirmClearChatModalConfirmButton - ); - await confirmButton.waitForClickable(); - await confirmButton.click(); - } - }; - sendMessage = async ( text: string, { @@ -138,36 +75,21 @@ describe('MongoDB Assistant', function () { await submitButton.click(); }; - getDisplayedMessages = async () => { - // Wait for the messages container to be visible - const chatMessages = browser.$(Selectors.AssistantChatMessages); - await chatMessages.waitForDisplayed(); - - // Get all individual message elements - const messageElements = await browser - .$$(Selectors.AssistantChatMessage) - .getElements(); - - const displayedMessages = []; - for (const messageElement of messageElements) { - const textElements = await messageElement.$$('p').getElements(); - const isAssistantMessage = - textElements.length !== 1 && - (await textElements[0].getText()) === 'MongoDB Assistant'; - // Get the message text content. - // In case of Assistant messages, skip the MongoDB Assistant text. - const text = isAssistantMessage - ? await textElements[1].getText() - : await textElements[0].getText(); - - displayedMessages.push({ - text: text, - role: isAssistantMessage ? ('assistant' as const) : ('user' as const), - }); - } + await browser.setupDefaultConnections(); + await browser.connectToDefaults(); + await browser.selectConnectionMenuItem( + DEFAULT_CONNECTION_NAME_1, + Selectors.CreateDatabaseButton, + false + ); + await browser.addDatabase(dbName, collectionName); - return displayedMessages; - }; + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAME_1, + dbName, + collectionName, + 'Aggregations' + ); }); after(async function () { @@ -188,7 +110,7 @@ describe('MongoDB Assistant', function () { afterEach(async function () { mockAssistantServer.clearRequests(); - await clearChat(); + await clearChat(browser); await screenshotIfFailed(compass, this.currentTest); }); @@ -215,7 +137,7 @@ describe('MongoDB Assistant', function () { }); it('can close and open the assistant drawer', async function () { - await openAssistantDrawer(); + await openAssistantDrawer(browser); await browser.$(Selectors.AssistantDrawerCloseButton).waitForDisplayed(); @@ -237,7 +159,7 @@ describe('MongoDB Assistant', function () { }); it('does not send the message if the user declines the opt-in', async function () { - await openAssistantDrawer(); + await openAssistantDrawer(browser); await sendMessage(testMessage); @@ -261,28 +183,76 @@ describe('MongoDB Assistant', function () { expect(mockAssistantServer.getRequests()).to.be.empty; }); - it('sends the message if the user opts in', async function () { - await openAssistantDrawer(); + describe('entry points', function () { + beforeEach(async function () { + await setAIOptIn(browser, false); + }); - await sendMessage(testMessage); + it('should display opt-in modal for connection error entry point', async function () { + await browser.connectWithConnectionString( + 'mongodb-invalid://localhost:27017', + { connectionStatus: 'failure' } + ); + await useErrorViewEntryPoint(browser); - const optInModal = browser.$(Selectors.AIOptInModal); - await optInModal.waitForDisplayed(); - expect(await optInModal.isDisplayed()).to.be.true; + const optInModal = browser.$(Selectors.AIOptInModal); + await optInModal.waitForDisplayed(); + expect(await optInModal.isDisplayed()).to.be.true; - const acceptButton = browser.$(Selectors.AIOptInModalAcceptButton); - await acceptButton.waitForClickable(); - await acceptButton.click(); + const declineLink = browser.$(Selectors.AIOptInModalDeclineLink); + await declineLink.waitForDisplayed(); + await declineLink.click(); - await optInModal.waitForDisplayed({ reverse: true }); + await optInModal.waitForDisplayed({ reverse: true }); - const chatInput = browser.$(Selectors.AssistantChatInputTextArea); - expect(await chatInput.getValue()).to.equal(''); + expect(await optInModal.isDisplayed()).to.be.false; - expect(await getDisplayedMessages()).to.deep.equal([ - { text: testMessage, role: 'user' }, - { text: testResponse, role: 'assistant' }, - ]); + expect(await getDisplayedMessages(browser)).to.deep.equal([]); + }); + + it('should display opt-in modal for explain plan entry point', async function () { + await useExplainPlanEntryPoint(browser); + + const optInModal = browser.$(Selectors.AIOptInModal); + await optInModal.waitForDisplayed(); + expect(await optInModal.isDisplayed()).to.be.true; + + const declineLink = browser.$(Selectors.AIOptInModalDeclineLink); + await declineLink.waitForDisplayed(); + await declineLink.click(); + + await optInModal.waitForDisplayed({ reverse: true }); + + expect(await optInModal.isDisplayed()).to.be.false; + + expect(await getDisplayedMessages(browser)).to.deep.equal([]); + }); + }); + + describe('opting in', function () { + it('sends the message if the user opts in', async function () { + await openAssistantDrawer(browser); + + await sendMessage(testMessage); + + const optInModal = browser.$(Selectors.AIOptInModal); + await optInModal.waitForDisplayed(); + expect(await optInModal.isDisplayed()).to.be.true; + + const acceptButton = browser.$(Selectors.AIOptInModalAcceptButton); + await acceptButton.waitForClickable(); + await acceptButton.click(); + + await optInModal.waitForDisplayed({ reverse: true }); + + const chatInput = browser.$(Selectors.AssistantChatInputTextArea); + expect(await chatInput.getValue()).to.equal(''); + + expect(await getDisplayedMessages(browser)).to.deep.equal([ + { text: testMessage, role: 'user' }, + { text: testResponse, role: 'assistant' }, + ]); + }); }); }); @@ -290,7 +260,7 @@ describe('MongoDB Assistant', function () { beforeEach(async function () { await setAIOptIn(browser, true); - await openAssistantDrawer(); + await openAssistantDrawer(browser); }); describe('clear chat button', function () { @@ -300,19 +270,19 @@ describe('MongoDB Assistant', function () { }); it('should clear the chat when the user clicks the clear chat button', async function () { - await openAssistantDrawer(); + await openAssistantDrawer(browser); await sendMessage(testMessage); await sendMessage(testMessage); - expect(await getDisplayedMessages()).to.deep.equal([ + expect(await getDisplayedMessages(browser)).to.deep.equal([ { text: testMessage, role: 'user' }, { text: testResponse, role: 'assistant' }, { text: testMessage, role: 'user' }, { text: testResponse, role: 'assistant' }, ]); - await clearChat(); + await clearChat(browser); - expect(await getDisplayedMessages()).to.deep.equal([]); + expect(await getDisplayedMessages(browser)).to.deep.equal([]); }); }); @@ -326,7 +296,7 @@ describe('MongoDB Assistant', function () { }, }); - expect(await getDisplayedMessages()).to.deep.equal([ + expect(await getDisplayedMessages(browser)).to.deep.equal([ { text: testMessage, role: 'user' }, { text: testResponse, role: 'assistant' }, { text: 'This is a different message', role: 'user' }, @@ -394,181 +364,230 @@ describe('MongoDB Assistant', function () { 'true' ); }); - }); - describe('entry points', function () { - const dbName = 'test'; - const collectionName = 'entryPoints'; - before(async function () { - await browser.setupDefaultConnections(); - await browser.connectToDefaults(); - await browser.selectConnectionMenuItem( - DEFAULT_CONNECTION_NAME_1, - Selectors.CreateDatabaseButton, - false - ); - await browser.addDatabase(dbName, collectionName); - }); + describe('entry points', function () { + describe('explain plan entry point', function () { + before(async function () { + await setAIOptIn(browser, true); + await setAIFeatures(browser, true); - describe('explain plan entry point', function () { - let useExplainPlanEntryPoint: () => Promise; + mockAssistantServer.setResponse({ + status: 200, + body: 'You should create an index.', + }); + }); - before(async function () { - await browser.navigateToCollectionTab( - DEFAULT_CONNECTION_NAME_1, - dbName, - collectionName, - 'Aggregations' - ); - await setAIOptIn(browser, true); - await setAIFeatures(browser, true); + it('opens assistant with explain plan prompt when clicking "Interpret for me"', async function () { + await useExplainPlanEntryPoint(browser); - mockAssistantServer.setResponse({ - status: 200, - body: 'You should create an index.', + const confirmButton = browser.$('button*=Confirm'); + await confirmButton.waitForDisplayed(); + await confirmButton.click(); + + await browser.pause(100); + + const messages = await getDisplayedMessages(browser); + expect(messages).deep.equal([ + { + text: 'Interpret this explain plan output for me.', + role: 'user', + }, + { text: 'You should create an index.', role: 'assistant' }, + ]); + + expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); }); - }); - beforeEach(function () { - useExplainPlanEntryPoint = async () => { - // Open explain plan modal by clicking the explain button - const explainButton = browser.$(Selectors.AggregationExplainButton); - await explainButton.waitForDisplayed(); - await explainButton.click(); - - // Wait for the explain plan modal to open and finish loading - const explainModal = browser.$(Selectors.AggregationExplainModal); - await explainModal.waitForDisplayed(); - - // Wait for the explain plan to be ready (loader should disappear) - const explainLoader = browser.$(Selectors.ExplainLoader); - await explainLoader.waitForDisplayed({ - reverse: true, - timeout: 10000, - }); + it('does not send request when user cancels confirmation', async function () { + await useExplainPlanEntryPoint(browser); - // Click the "Interpret for me" button - const interpretButton = browser.$( - Selectors.ExplainPlanInterpretButton + const chatMessages = browser.$(Selectors.AssistantChatMessages); + await chatMessages.waitForDisplayed(); + expect(await chatMessages.getText()).to.include( + 'Please confirm your request' ); - await interpretButton.waitForDisplayed(); - await interpretButton.click(); - // The modal should close - await explainModal.waitForDisplayed({ reverse: true }); + // Click Cancel button + const cancelButton = browser.$('button*=Cancel'); + await cancelButton.waitForDisplayed(); + await cancelButton.click(); - // The assistant drawer should open - const assistantDrawer = browser.$(Selectors.SideDrawer); - await assistantDrawer.waitForDisplayed(); - }; - }); + // Wait a bit to ensure no request is sent + await browser.pause(300); - it('opens assistant with explain plan prompt when clicking "Interpret for me"', async function () { - await useExplainPlanEntryPoint(); + const finalMessages = await getDisplayedMessages(browser); + expect(finalMessages.length).to.equal(0); - const confirmButton = browser.$('button*=Confirm'); - await confirmButton.waitForDisplayed(); - await confirmButton.click(); + expect(await chatMessages.getText()).to.include( + 'Please confirm your request' + ); + expect(await chatMessages.getText()).to.include('Request cancelled'); - await browser.pause(100); + // Verify no assistant request was made + expect(mockAssistantServer.getRequests()).to.be.empty; + }); + }); - const messages = await getDisplayedMessages(); - expect(messages).deep.equal([ - { text: 'Interpret this explain plan output for me.', role: 'user' }, - { text: 'You should create an index.', role: 'assistant' }, - ]); + describe('error message entry point', function () { + before(async function () { + await setAIOptIn(browser, true); - expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); + mockAssistantServer.setResponse({ + status: 200, + body: 'You should review the connection string.', + }); + }); + + it('opens assistant with error message view prompt when clicking "Debug for me"', async function () { + await browser.connectWithConnectionString( + 'mongodb-invalid://localhost:27017', + { connectionStatus: 'failure' } + ); + await useErrorViewEntryPoint(browser); + + const messages = await getDisplayedMessages(browser); + expect(messages).deep.equal([ + { + text: 'Diagnose why my Compass connection is failing and help me debug it.', + role: 'user', + }, + { + text: 'You should review the connection string.', + role: 'assistant', + }, + ]); + + expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); + }); }); + }); + }); +}); - it('does not send request when user cancels confirmation', async function () { - // Navigate to collection Aggregations tab - await browser.navigateToCollectionTab( - DEFAULT_CONNECTION_NAME_1, - dbName, - collectionName, - 'Aggregations' - ); +async function setAIFeatures(browser: CompassBrowser, newValue: boolean) { + await browser.openSettingsModal('ai'); - await useExplainPlanEntryPoint(); + // Wait for AI settings content to be visible + const aiSettingsContent = browser.$( + Selectors.ArtificialIntelligenceSettingsContent + ); + await aiSettingsContent.waitForDisplayed(); - const chatMessages = browser.$(Selectors.AssistantChatMessages); - await chatMessages.waitForDisplayed(); - expect(await chatMessages.getText()).to.include( - 'Please confirm your request' - ); + const currentValue = + (await browser + .$(Selectors.SettingsInputElement('enableGenAIFeatures')) + .getAttribute('aria-checked')) === 'true'; + + if (currentValue !== newValue) { + await browser.clickParent( + Selectors.SettingsInputElement('enableGenAIFeatures') + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + } - // Click Cancel button - const cancelButton = browser.$('button*=Cancel'); - await cancelButton.waitForDisplayed(); - await cancelButton.click(); + const closeButton = browser.$(Selectors.CloseSettingsModalButton); + await closeButton.waitForClickable(); + await closeButton.click(); + await closeButton.waitForDisplayed({ + reverse: true, + }); +} - // Wait a bit to ensure no request is sent - await browser.pause(300); +async function setAIOptIn(browser: CompassBrowser, enabled: boolean) { + // Reset the opt-in preference by using the execute command + await browser.setFeature('optInGenAIFeatures', enabled); - const finalMessages = await getDisplayedMessages(); - expect(finalMessages.length).to.equal(0); + // Wait for the IPC to be processed + await browser.pause(500); +} - expect(await chatMessages.getText()).to.include( - 'Please confirm your request' - ); - expect(await chatMessages.getText()).to.include('Request cancelled'); +async function openAssistantDrawer(browser: CompassBrowser) { + const drawerButton = browser.$(Selectors.AssistantDrawerButton); + await drawerButton.waitForDisplayed(); + await drawerButton.waitForClickable(); + await drawerButton.click(); +} - // Verify no assistant request was made - expect(mockAssistantServer.getRequests()).to.be.empty; - }); +async function clearChat(browser: CompassBrowser) { + const clearChatButton = browser.$(Selectors.AssistantClearChatButton); + if (await clearChatButton.isDisplayed()) { + await clearChatButton.click(); + const confirmButton = browser.$( + Selectors.AssistantConfirmClearChatModalConfirmButton + ); + await confirmButton.waitForClickable(); + await confirmButton.click(); + } +} + +async function getDisplayedMessages(browser: CompassBrowser) { + // Wait for the messages container to be visible + const chatMessages = browser.$(Selectors.AssistantChatMessages); + await chatMessages.waitForDisplayed(); + + // Get all individual message elements + const messageElements = await browser + .$$(Selectors.AssistantChatMessage) + .getElements(); + + const displayedMessages = []; + for (const messageElement of messageElements) { + const textElements = await messageElement.$$('p').getElements(); + const isAssistantMessage = + textElements.length !== 1 && + (await textElements[0].getText()) === 'MongoDB Assistant'; + // Get the message text content. + // In case of Assistant messages, skip the MongoDB Assistant text. + const text = isAssistantMessage + ? await textElements[1].getText() + : await textElements[0].getText(); + + displayedMessages.push({ + text: text, + role: isAssistantMessage ? ('assistant' as const) : ('user' as const), }); + } - describe('error message view entry point', function () { - let useErrorViewEntryPoint: () => Promise; - before(async function () { - await setAIOptIn(browser, true); + return displayedMessages; +} - mockAssistantServer.setResponse({ - status: 200, - body: 'You should review the connection string.', - }); - useErrorViewEntryPoint = async () => { - const connectionToastErrorDebugButton = browser.$( - Selectors.ConnectionToastErrorDebugButton - ); - await connectionToastErrorDebugButton.waitForDisplayed(); - await connectionToastErrorDebugButton.click(); - }; - }); +async function useExplainPlanEntryPoint(browser: CompassBrowser) { + // Open explain plan modal by clicking the explain button + const explainButton = browser.$(Selectors.AggregationExplainButton); + await explainButton.waitForDisplayed(); + await explainButton.click(); + + // Wait for the explain plan modal to open and finish loading + const explainModal = browser.$(Selectors.AggregationExplainModal); + await explainModal.waitForDisplayed(); + + // Wait for the explain plan to be ready (loader should disappear) + const explainLoader = browser.$(Selectors.ExplainLoader); + await explainLoader.waitForDisplayed({ + reverse: true, + timeout: 10000, + }); - it('opens assistant with error message view prompt when clicking "Debug for me"', async function () { - void (await browser.connectWithConnectionString( - 'mongodb-invalid://localhost:27017' - )); - await useErrorViewEntryPoint(); - - const messages = await getDisplayedMessages(); - expect(messages).deep.equal([ - { - text: 'Diagnose why my Compass connection is failing and help me debug it.', - role: 'user', - }, - { - text: 'You should review the connection string.', - role: 'assistant', - }, - ]); + // Click the "Interpret for me" button + const interpretButton = browser.$(Selectors.ExplainPlanInterpretButton); + await interpretButton.waitForDisplayed(); + await interpretButton.click(); - expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); - }); + // The modal should close + await explainModal.waitForDisplayed({ reverse: true }); - it('should display opt-in modal when clicking "Debug for me" without opt-in ', async function () { - await setAIOptIn(browser, false); - void (await browser.connectWithConnectionString( - 'mongodb-invalid://localhost:27017' - )); - await useErrorViewEntryPoint(); + // The assistant drawer should open + const assistantDrawer = browser.$(Selectors.SideDrawer); + await assistantDrawer.waitForDisplayed(); +} - const optInModal = browser.$(Selectors.AIOptInModal); - await optInModal.waitForDisplayed(); - expect(await optInModal.isDisplayed()).to.be.true; - }); - }); +async function useErrorViewEntryPoint(browser: CompassBrowser) { + const connectionToastErrorDebugButton = browser.$( + Selectors.ConnectionToastErrorDebugButton + ); + await connectionToastErrorDebugButton.waitForDisplayed(); + await connectionToastErrorDebugButton.click(); + await connectionToastErrorDebugButton.waitForDisplayed({ + reverse: true, }); -}); +} From 10101612a1e854a47195e8b9c24aa8f862b711ef Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 12:47:17 +0200 Subject: [PATCH 05/17] chore: remove redundant comments --- packages/compass-e2e-tests/tests/assistant.test.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 332d8b1738f..00e22f1182b 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -47,7 +47,6 @@ describe('MongoDB Assistant', function () { // Start a mock Assistant server for AI chat responses mockAssistantServer = await startMockAssistantServer(); - // Set env vars: one for feature flag checks, one for assistant API process.env.COMPASS_ATLAS_SERVICE_UNAUTH_BASE_URL_OVERRIDE = mockAtlasServer.endpoint; process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE = @@ -119,7 +118,6 @@ describe('MongoDB Assistant', function () { it('shows the assistant drawer button when AI features are enabled', async function () { await setAIFeatures(browser, true); - // AI Features are enabled by default so the drawer button should be visible. const drawerButton = browser.$(Selectors.AssistantDrawerButton); await drawerButton.waitForDisplayed(); expect(await drawerButton.isDisplayed()).to.be.true; @@ -128,7 +126,6 @@ describe('MongoDB Assistant', function () { it('does not show the assistant drawer button when AI features are disabled', async function () { await setAIFeatures(browser, false); - // Assistant drawer button should not be visible const drawerButton = browser.$(Selectors.AssistantDrawerButton); await drawerButton.waitForDisplayed({ reverse: true }); expect(await drawerButton.isDisplayed()).to.be.false; @@ -163,22 +160,17 @@ describe('MongoDB Assistant', function () { await sendMessage(testMessage); - // Wait for opt-in modal and decline it const declineLink = browser.$(Selectors.AIOptInModalDeclineLink); await declineLink.waitForDisplayed(); await declineLink.click(); - // Wait for the modal to close const optInModal = browser.$(Selectors.AIOptInModal); await optInModal.waitForDisplayed({ reverse: true }); - // Verify the input was not cleared after sending const chatInput = browser.$(Selectors.AssistantChatInputTextArea); expect(await chatInput.getValue()).not.to.equal(testMessage); - // Verify the message is not displayed in the chat - const chatMessages = browser.$(Selectors.AssistantChatMessages); - expect(await chatMessages.getText()).to.not.include(testMessage); + expect(await getDisplayedMessages(browser)).to.deep.equal([]); expect(mockAssistantServer.getRequests()).to.be.empty; }); @@ -205,8 +197,6 @@ describe('MongoDB Assistant', function () { await optInModal.waitForDisplayed({ reverse: true }); - expect(await optInModal.isDisplayed()).to.be.false; - expect(await getDisplayedMessages(browser)).to.deep.equal([]); }); @@ -223,8 +213,6 @@ describe('MongoDB Assistant', function () { await optInModal.waitForDisplayed({ reverse: true }); - expect(await optInModal.isDisplayed()).to.be.false; - expect(await getDisplayedMessages(browser)).to.deep.equal([]); }); }); From 5f57459731979697bc46ef0a48cce40498d9a72a Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 16:35:23 +0200 Subject: [PATCH 06/17] chore: changes from feedback --- .../compass-e2e-tests/tests/assistant.test.ts | 160 +++++++----------- 1 file changed, 65 insertions(+), 95 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 00e22f1182b..dcc3a99938c 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -30,6 +30,7 @@ describe('MongoDB Assistant', function () { response?: MockAssistantResponse; } ) => Promise; + let setAIOptIn: (newValue: boolean) => Promise; const testMessage = 'What is MongoDB?'; const testResponse = 'MongoDB is a database.'; @@ -74,6 +75,19 @@ describe('MongoDB Assistant', function () { await submitButton.click(); }; + setAIOptIn = async (newValue: boolean) => { + if ( + (await browser.getFeature('optInGenAIFeatures')) === true && + newValue === false + ) { + // Reseting the opt-in to false can be tricky so it's best to start over in this case. + compass = await init(this.test?.fullTitle(), { firstRun: false }); + return; + } + + await browser.setFeature('optInGenAIFeatures', newValue); + }; + await browser.setupDefaultConnections(); await browser.connectToDefaults(); await browser.selectConnectionMenuItem( @@ -151,8 +165,8 @@ describe('MongoDB Assistant', function () { }); describe('before opt-in', function () { - beforeEach(async function () { - await setAIOptIn(browser, false); + before(async function () { + await setAIOptIn(false); }); it('does not send the message if the user declines the opt-in', async function () { @@ -176,16 +190,14 @@ describe('MongoDB Assistant', function () { }); describe('entry points', function () { - beforeEach(async function () { - await setAIOptIn(browser, false); - }); - it('should display opt-in modal for connection error entry point', async function () { await browser.connectWithConnectionString( 'mongodb-invalid://localhost:27017', { connectionStatus: 'failure' } ); - await useErrorViewEntryPoint(browser); + await browser.clickVisible( + browser.$(Selectors.ConnectionToastErrorDebugButton) + ); const optInModal = browser.$(Selectors.AIOptInModal); await optInModal.waitForDisplayed(); @@ -216,38 +228,40 @@ describe('MongoDB Assistant', function () { expect(await getDisplayedMessages(browser)).to.deep.equal([]); }); }); + }); - describe('opting in', function () { - it('sends the message if the user opts in', async function () { - await openAssistantDrawer(browser); + describe('opting in', function () { + before(async function () { + await setAIOptIn(false); + await openAssistantDrawer(browser); + }); - await sendMessage(testMessage); + it('sends the message if the user opts in', async function () { + await sendMessage(testMessage); - const optInModal = browser.$(Selectors.AIOptInModal); - await optInModal.waitForDisplayed(); - expect(await optInModal.isDisplayed()).to.be.true; + const optInModal = browser.$(Selectors.AIOptInModal); + await optInModal.waitForDisplayed(); + expect(await optInModal.isDisplayed()).to.be.true; - const acceptButton = browser.$(Selectors.AIOptInModalAcceptButton); - await acceptButton.waitForClickable(); - await acceptButton.click(); + const acceptButton = browser.$(Selectors.AIOptInModalAcceptButton); + await acceptButton.waitForClickable(); + await acceptButton.click(); - await optInModal.waitForDisplayed({ reverse: true }); + await optInModal.waitForDisplayed({ reverse: true }); - const chatInput = browser.$(Selectors.AssistantChatInputTextArea); - expect(await chatInput.getValue()).to.equal(''); + const chatInput = browser.$(Selectors.AssistantChatInputTextArea); + expect(await chatInput.getValue()).to.equal(''); - expect(await getDisplayedMessages(browser)).to.deep.equal([ - { text: testMessage, role: 'user' }, - { text: testResponse, role: 'assistant' }, - ]); - }); + expect(await getDisplayedMessages(browser)).to.deep.equal([ + { text: testMessage, role: 'user' }, + { text: testResponse, role: 'assistant' }, + ]); }); }); describe('after opt-in', function () { - beforeEach(async function () { - await setAIOptIn(browser, true); - + before(async function () { + await setAIOptIn(true); await openAssistantDrawer(browser); }); @@ -275,7 +289,12 @@ describe('MongoDB Assistant', function () { }); it('displays multiple messages correctly', async function () { - await sendMessage(testMessage); + await sendMessage(testMessage, { + response: { + status: 200, + body: testResponse, + }, + }); await sendMessage('This is a different message', { response: { @@ -331,17 +350,13 @@ describe('MongoDB Assistant', function () { const thumbsDownButton = assistantMessage.$( '[aria-label="Dislike this message"]' ); - await thumbsDownButton.waitForDisplayed(); - await thumbsDownButton.click(); + await browser.clickVisible(thumbsDownButton); const feedbackTextarea = assistantMessage.$('textarea'); await feedbackTextarea.waitForDisplayed(); - await feedbackTextarea.setValue('This is a test feedback'); - const submitButton = browser.$('button*=Submit'); - await submitButton.waitForClickable(); - await submitButton.click(); + await browser.clickVisible(browser.$('button*=Submit')); await feedbackTextarea.waitForDisplayed({ reverse: true }); @@ -356,7 +371,7 @@ describe('MongoDB Assistant', function () { describe('entry points', function () { describe('explain plan entry point', function () { before(async function () { - await setAIOptIn(browser, true); + await setAIOptIn(true); await setAIFeatures(browser, true); mockAssistantServer.setResponse({ @@ -417,9 +432,7 @@ describe('MongoDB Assistant', function () { }); describe('error message entry point', function () { - before(async function () { - await setAIOptIn(browser, true); - + before(function () { mockAssistantServer.setResponse({ status: 200, body: 'You should review the connection string.', @@ -431,7 +444,9 @@ describe('MongoDB Assistant', function () { 'mongodb-invalid://localhost:27017', { connectionStatus: 'failure' } ); - await useErrorViewEntryPoint(browser); + await browser.clickVisible( + browser.$(Selectors.ConnectionToastErrorDebugButton) + ); const messages = await getDisplayedMessages(browser); expect(messages).deep.equal([ @@ -455,11 +470,9 @@ describe('MongoDB Assistant', function () { async function setAIFeatures(browser: CompassBrowser, newValue: boolean) { await browser.openSettingsModal('ai'); - // Wait for AI settings content to be visible - const aiSettingsContent = browser.$( - Selectors.ArtificialIntelligenceSettingsContent - ); - await aiSettingsContent.waitForDisplayed(); + await browser + .$(Selectors.ArtificialIntelligenceSettingsContent) + .waitForDisplayed(); const currentValue = (await browser @@ -481,19 +494,8 @@ async function setAIFeatures(browser: CompassBrowser, newValue: boolean) { }); } -async function setAIOptIn(browser: CompassBrowser, enabled: boolean) { - // Reset the opt-in preference by using the execute command - await browser.setFeature('optInGenAIFeatures', enabled); - - // Wait for the IPC to be processed - await browser.pause(500); -} - async function openAssistantDrawer(browser: CompassBrowser) { - const drawerButton = browser.$(Selectors.AssistantDrawerButton); - await drawerButton.waitForDisplayed(); - await drawerButton.waitForClickable(); - await drawerButton.click(); + await browser.clickVisible(Selectors.AssistantDrawerButton); } async function clearChat(browser: CompassBrowser) { @@ -509,11 +511,8 @@ async function clearChat(browser: CompassBrowser) { } async function getDisplayedMessages(browser: CompassBrowser) { - // Wait for the messages container to be visible - const chatMessages = browser.$(Selectors.AssistantChatMessages); - await chatMessages.waitForDisplayed(); + await browser.$(Selectors.AssistantChatMessages).waitForDisplayed(); - // Get all individual message elements const messageElements = await browser .$$(Selectors.AssistantChatMessage) .getElements(); @@ -540,42 +539,13 @@ async function getDisplayedMessages(browser: CompassBrowser) { } async function useExplainPlanEntryPoint(browser: CompassBrowser) { - // Open explain plan modal by clicking the explain button - const explainButton = browser.$(Selectors.AggregationExplainButton); - await explainButton.waitForDisplayed(); - await explainButton.click(); - - // Wait for the explain plan modal to open and finish loading - const explainModal = browser.$(Selectors.AggregationExplainModal); - await explainModal.waitForDisplayed(); - - // Wait for the explain plan to be ready (loader should disappear) - const explainLoader = browser.$(Selectors.ExplainLoader); - await explainLoader.waitForDisplayed({ - reverse: true, - timeout: 10000, - }); - - // Click the "Interpret for me" button - const interpretButton = browser.$(Selectors.ExplainPlanInterpretButton); - await interpretButton.waitForDisplayed(); - await interpretButton.click(); + await browser.clickVisible(Selectors.AggregationExplainButton); - // The modal should close - await explainModal.waitForDisplayed({ reverse: true }); + await browser.clickVisible(Selectors.ExplainPlanInterpretButton); - // The assistant drawer should open - const assistantDrawer = browser.$(Selectors.SideDrawer); - await assistantDrawer.waitForDisplayed(); -} - -async function useErrorViewEntryPoint(browser: CompassBrowser) { - const connectionToastErrorDebugButton = browser.$( - Selectors.ConnectionToastErrorDebugButton - ); - await connectionToastErrorDebugButton.waitForDisplayed(); - await connectionToastErrorDebugButton.click(); - await connectionToastErrorDebugButton.waitForDisplayed({ + await browser.$(Selectors.AggregationExplainModal).waitForDisplayed({ reverse: true, }); + + await browser.$(Selectors.AssistantChatMessages).waitForDisplayed(); } From b0084fc0bd311258f7b74ae37497d8e30412ea18 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 16:48:20 +0200 Subject: [PATCH 07/17] chore: fixups --- .../compass-e2e-tests/tests/assistant.test.ts | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index dcc3a99938c..d195afa9cb5 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -38,8 +38,6 @@ describe('MongoDB Assistant', function () { const collectionName = 'entryPoints'; before(async function () { - skipForWeb(this, 'ai assistant not yet available in compass-web'); - process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN = 'true'; // Start a mock Atlas service for feature flag checks @@ -55,7 +53,6 @@ describe('MongoDB Assistant', function () { telemetry = await startTelemetryServer(); compass = await init(this.test?.fullTitle()); - browser = compass.browser; sendMessage = async ( text: string, @@ -75,34 +72,41 @@ describe('MongoDB Assistant', function () { await submitButton.click(); }; + const setup = async () => { + browser = compass.browser; + await browser.setupDefaultConnections(); + await browser.connectToDefaults(); + await browser.selectConnectionMenuItem( + DEFAULT_CONNECTION_NAME_1, + Selectors.CreateDatabaseButton, + false + ); + await browser.addDatabase(dbName, collectionName); + + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAME_1, + dbName, + collectionName, + 'Aggregations' + ); + }; + setAIOptIn = async (newValue: boolean) => { if ( (await browser.getFeature('optInGenAIFeatures')) === true && newValue === false ) { + await cleanup(compass); // Reseting the opt-in to false can be tricky so it's best to start over in this case. - compass = await init(this.test?.fullTitle(), { firstRun: false }); + compass = await init(this.test?.fullTitle(), { firstRun: true }); + await setup(); return; } await browser.setFeature('optInGenAIFeatures', newValue); }; - await browser.setupDefaultConnections(); - await browser.connectToDefaults(); - await browser.selectConnectionMenuItem( - DEFAULT_CONNECTION_NAME_1, - Selectors.CreateDatabaseButton, - false - ); - await browser.addDatabase(dbName, collectionName); - - await browser.navigateToCollectionTab( - DEFAULT_CONNECTION_NAME_1, - dbName, - collectionName, - 'Aggregations' - ); + await setup(); }); after(async function () { From 0297c45211bd2f5bac843403890988928a237db8 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 17:08:15 +0200 Subject: [PATCH 08/17] chore: fixups --- packages/compass-e2e-tests/tests/assistant.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index d195afa9cb5..31dcf7b4d96 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -7,7 +7,6 @@ import { init, cleanup, screenshotIfFailed, - skipForWeb, TEST_COMPASS_WEB, DEFAULT_CONNECTION_NAME_1, } from '../helpers/compass'; @@ -146,7 +145,6 @@ describe('MongoDB Assistant', function () { const drawerButton = browser.$(Selectors.AssistantDrawerButton); await drawerButton.waitForDisplayed({ reverse: true }); - expect(await drawerButton.isDisplayed()).to.be.false; await setAIFeatures(browser, true); }); @@ -154,8 +152,6 @@ describe('MongoDB Assistant', function () { it('can close and open the assistant drawer', async function () { await openAssistantDrawer(browser); - await browser.$(Selectors.AssistantDrawerCloseButton).waitForDisplayed(); - await browser.clickVisible(Selectors.AssistantDrawerCloseButton); await browser.$(Selectors.AssistantDrawerCloseButton).waitForDisplayed({ From b995d47e7d0fc4375c5ef42a08b2df5f9e4d1c81 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 17:16:08 +0200 Subject: [PATCH 09/17] chore: waituntil --- .../compass-e2e-tests/tests/assistant.test.ts | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 31dcf7b4d96..796554170ff 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -69,6 +69,13 @@ describe('MongoDB Assistant', function () { await chatInput.setValue(text); const submitButton = browser.$(Selectors.AssistantChatSubmitButton); await submitButton.click(); + + await browser.waitUntil(async () => { + return (await getDisplayedMessages(browser)).includes({ + text: response?.body, + role: 'user', + }); + }); }; const setup = async () => { @@ -314,8 +321,6 @@ describe('MongoDB Assistant', function () { it('can copy assistant message to clipboard', async function () { await sendMessage(testMessage); - await browser.pause(100); - const messageElements = await browser .$$(Selectors.AssistantChatMessage) .getElements(); @@ -326,8 +331,6 @@ describe('MongoDB Assistant', function () { await copyButton.waitForDisplayed(); await copyButton.click(); - await browser.pause(100); - const clipboardText = await browser.execute(() => { return navigator.clipboard.readText(); }); @@ -338,8 +341,6 @@ describe('MongoDB Assistant', function () { it('can submit feedback with text', async function () { await sendMessage(testMessage); - await browser.pause(100); - // Get all message elements const messageElements = await browser .$$(Selectors.AssistantChatMessage) @@ -387,16 +388,16 @@ describe('MongoDB Assistant', function () { await confirmButton.waitForDisplayed(); await confirmButton.click(); - await browser.pause(100); - - const messages = await getDisplayedMessages(browser); - expect(messages).deep.equal([ - { - text: 'Interpret this explain plan output for me.', - role: 'user', - }, - { text: 'You should create an index.', role: 'assistant' }, - ]); + await browser.waitUntil(async () => { + expect(await getDisplayedMessages(browser)).deep.equal([ + { + text: 'Interpret this explain plan output for me.', + role: 'user', + }, + { text: 'You should create an index.', role: 'assistant' }, + ]); + return true; + }); expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); }); From a9d07ed95818c5c9656752b31ca5df95d84d8028 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 17:17:17 +0200 Subject: [PATCH 10/17] chore: add some extra check --- packages/compass-e2e-tests/tests/assistant.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 796554170ff..8b29781c636 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -62,6 +62,7 @@ describe('MongoDB Assistant', function () { }, }: { response?: MockAssistantResponse } = {} ) => { + const existingMessages = await getDisplayedMessages(browser); mockAssistantServer.setResponse(response); const chatInput = browser.$(Selectors.AssistantChatInputTextArea); @@ -71,10 +72,14 @@ describe('MongoDB Assistant', function () { await submitButton.click(); await browser.waitUntil(async () => { - return (await getDisplayedMessages(browser)).includes({ - text: response?.body, - role: 'user', - }); + const newMessages = await getDisplayedMessages(browser); + return ( + newMessages.length > existingMessages.length && + newMessages.includes({ + text: response?.body, + role: 'user', + }) + ); }); }; From a086d07838a581ad263d4222eb29571c888fa622 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 17:22:45 +0200 Subject: [PATCH 11/17] chore: add expected result option to sendMessage --- .../compass-e2e-tests/tests/assistant.test.ts | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 8b29781c636..dd46f4146d3 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -27,6 +27,7 @@ describe('MongoDB Assistant', function () { text: string, options?: { response?: MockAssistantResponse; + expectedResult?: 'success' | 'opt-in'; } ) => Promise; let setAIOptIn: (newValue: boolean) => Promise; @@ -60,27 +61,39 @@ describe('MongoDB Assistant', function () { status: 200, body: testResponse, }, - }: { response?: MockAssistantResponse } = {} + expectedResult = 'success', + }: { + response?: MockAssistantResponse; + expectedResult?: 'success' | 'opt-in'; + } = {} ) => { const existingMessages = await getDisplayedMessages(browser); - mockAssistantServer.setResponse(response); + mockAssistantServer.setResponse(response); const chatInput = browser.$(Selectors.AssistantChatInputTextArea); await chatInput.waitForDisplayed(); await chatInput.setValue(text); const submitButton = browser.$(Selectors.AssistantChatSubmitButton); await submitButton.click(); - await browser.waitUntil(async () => { - const newMessages = await getDisplayedMessages(browser); - return ( - newMessages.length > existingMessages.length && - newMessages.includes({ - text: response?.body, - role: 'user', - }) - ); - }); + switch (expectedResult) { + case 'success': + await browser.waitUntil(async () => { + const newMessages = await getDisplayedMessages(browser); + return ( + newMessages.length > existingMessages.length && + newMessages.includes({ + text: response?.body, + role: 'user', + }) + ); + }); + break; + case 'opt-in': + await browser + .$(Selectors.AIOptInModalAcceptButton) + .waitForDisplayed(); + } }; const setup = async () => { @@ -184,11 +197,9 @@ describe('MongoDB Assistant', function () { it('does not send the message if the user declines the opt-in', async function () { await openAssistantDrawer(browser); - await sendMessage(testMessage); + await sendMessage(testMessage, { expectedResult: 'opt-in' }); - const declineLink = browser.$(Selectors.AIOptInModalDeclineLink); - await declineLink.waitForDisplayed(); - await declineLink.click(); + await browser.clickVisible(Selectors.AIOptInModalDeclineLink); const optInModal = browser.$(Selectors.AIOptInModal); await optInModal.waitForDisplayed({ reverse: true }); @@ -394,16 +405,17 @@ describe('MongoDB Assistant', function () { await confirmButton.click(); await browser.waitUntil(async () => { - expect(await getDisplayedMessages(browser)).deep.equal([ - { - text: 'Interpret this explain plan output for me.', - role: 'user', - }, - { text: 'You should create an index.', role: 'assistant' }, - ]); - return true; + return (await getDisplayedMessages(browser)).length === 2; }); + expect(await getDisplayedMessages(browser)).deep.equal([ + { + text: 'Interpret this explain plan output for me.', + role: 'user', + }, + { text: 'You should create an index.', role: 'assistant' }, + ]); + expect(mockAssistantServer.getRequests()).to.have.lengthOf(1); }); From c81c712fc9f7f3fd661625bbc5a850eb7d6ba4ce Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 17:23:19 +0200 Subject: [PATCH 12/17] chore: remove stop --- packages/compass-e2e-tests/tests/assistant.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index dd46f4146d3..dbcd1b523de 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -134,10 +134,6 @@ describe('MongoDB Assistant', function () { }); after(async function () { - if (TEST_COMPASS_WEB) { - return; - } - await mockAtlasServer.stop(); await mockAssistantServer.stop(); From 65dd4525bc128cd86ffdc787ba31d9500c232c30 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 17:32:08 +0200 Subject: [PATCH 13/17] chore: fixup --- .../compass-e2e-tests/tests/assistant.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index dbcd1b523de..9a6efd29431 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -7,7 +7,6 @@ import { init, cleanup, screenshotIfFailed, - TEST_COMPASS_WEB, DEFAULT_CONNECTION_NAME_1, } from '../helpers/compass'; import type { Compass } from '../helpers/compass'; @@ -40,10 +39,7 @@ describe('MongoDB Assistant', function () { before(async function () { process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN = 'true'; - // Start a mock Atlas service for feature flag checks mockAtlasServer = await startMockAtlasServiceServer(); - - // Start a mock Assistant server for AI chat responses mockAssistantServer = await startMockAssistantServer(); process.env.COMPASS_ATLAS_SERVICE_UNAUTH_BASE_URL_OVERRIDE = @@ -82,10 +78,14 @@ describe('MongoDB Assistant', function () { const newMessages = await getDisplayedMessages(browser); return ( newMessages.length > existingMessages.length && - newMessages.includes({ - text: response?.body, - role: 'user', - }) + newMessages.some( + (message) => + message.text === response?.body && + message.role === 'assistant' + ) && + newMessages.some( + (message) => message.text === text && message.role === 'user' + ) ); }); break; @@ -256,7 +256,7 @@ describe('MongoDB Assistant', function () { }); it('sends the message if the user opts in', async function () { - await sendMessage(testMessage); + await sendMessage(testMessage, { expectedResult: 'opt-in' }); const optInModal = browser.$(Selectors.AIOptInModal); await optInModal.waitForDisplayed(); @@ -291,9 +291,9 @@ describe('MongoDB Assistant', function () { }); it('should clear the chat when the user clicks the clear chat button', async function () { - await openAssistantDrawer(browser); await sendMessage(testMessage); await sendMessage(testMessage); + expect(await getDisplayedMessages(browser)).to.deep.equal([ { text: testMessage, role: 'user' }, { text: testResponse, role: 'assistant' }, From 2adb17fa71aae3e9810513cabeebd539b733b21d Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 21:10:40 +0200 Subject: [PATCH 14/17] chore: use set feature to support web --- .../compass-e2e-tests/tests/assistant.test.ts | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 9a6efd29431..f6c17965e61 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -30,6 +30,7 @@ describe('MongoDB Assistant', function () { } ) => Promise; let setAIOptIn: (newValue: boolean) => Promise; + let setAIFeatures: (newValue: boolean) => Promise; const testMessage = 'What is MongoDB?'; const testResponse = 'MongoDB is a database.'; @@ -115,6 +116,23 @@ describe('MongoDB Assistant', function () { ); }; + setAIFeatures = async (newValue: boolean) => { + const currentValue = await browser.getFeature('enableGenAIFeatures'); + if (currentValue === newValue) { + return; + } + + await browser.setFeature('enableGenAIFeatures', newValue); + + if (newValue) { + await browser.$(Selectors.AssistantDrawerButton).waitForDisplayed(); + } else { + await browser.$(Selectors.AssistantDrawerButton).waitForDisplayed({ + reverse: true, + }); + } + }; + setAIOptIn = async (newValue: boolean) => { if ( (await browser.getFeature('optInGenAIFeatures')) === true && @@ -154,7 +172,7 @@ describe('MongoDB Assistant', function () { describe('drawer visibility', function () { it('shows the assistant drawer button when AI features are enabled', async function () { - await setAIFeatures(browser, true); + await setAIFeatures(true); const drawerButton = browser.$(Selectors.AssistantDrawerButton); await drawerButton.waitForDisplayed(); @@ -162,12 +180,12 @@ describe('MongoDB Assistant', function () { }); it('does not show the assistant drawer button when AI features are disabled', async function () { - await setAIFeatures(browser, false); + await setAIFeatures(false); const drawerButton = browser.$(Selectors.AssistantDrawerButton); await drawerButton.waitForDisplayed({ reverse: true }); - await setAIFeatures(browser, true); + await setAIFeatures(true); }); it('can close and open the assistant drawer', async function () { @@ -385,7 +403,7 @@ describe('MongoDB Assistant', function () { describe('explain plan entry point', function () { before(async function () { await setAIOptIn(true); - await setAIFeatures(browser, true); + await setAIFeatures(true); mockAssistantServer.setResponse({ status: 200, @@ -481,33 +499,6 @@ describe('MongoDB Assistant', function () { }); }); -async function setAIFeatures(browser: CompassBrowser, newValue: boolean) { - await browser.openSettingsModal('ai'); - - await browser - .$(Selectors.ArtificialIntelligenceSettingsContent) - .waitForDisplayed(); - - const currentValue = - (await browser - .$(Selectors.SettingsInputElement('enableGenAIFeatures')) - .getAttribute('aria-checked')) === 'true'; - - if (currentValue !== newValue) { - await browser.clickParent( - Selectors.SettingsInputElement('enableGenAIFeatures') - ); - await browser.clickVisible(Selectors.SaveSettingsButton); - } - - const closeButton = browser.$(Selectors.CloseSettingsModalButton); - await closeButton.waitForClickable(); - await closeButton.click(); - await closeButton.waitForDisplayed({ - reverse: true, - }); -} - async function openAssistantDrawer(browser: CompassBrowser) { await browser.clickVisible(Selectors.AssistantDrawerButton); } From 167f968a69e4c9fe5d30667176d1a084be3e1d5e Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 22:02:45 +0200 Subject: [PATCH 15/17] chore: avoid get on web --- .../compass-e2e-tests/tests/assistant.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index f6c17965e61..adc5d19245f 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -14,6 +14,7 @@ import * as Selectors from '../helpers/selectors'; import { startMockAtlasServiceServer } from '../helpers/atlas-service'; import { startMockAssistantServer } from '../helpers/assistant-service'; import type { MockAssistantResponse } from '../helpers/assistant-service'; +import { isTestingWeb } from '../helpers/test-runner-context'; describe('MongoDB Assistant', function () { let compass: Compass; @@ -117,9 +118,11 @@ describe('MongoDB Assistant', function () { }; setAIFeatures = async (newValue: boolean) => { - const currentValue = await browser.getFeature('enableGenAIFeatures'); - if (currentValue === newValue) { - return; + if (!isTestingWeb()) { + const currentValue = await browser.getFeature('enableGenAIFeatures'); + if (currentValue === newValue) { + return; + } } await browser.setFeature('enableGenAIFeatures', newValue); @@ -135,12 +138,13 @@ describe('MongoDB Assistant', function () { setAIOptIn = async (newValue: boolean) => { if ( - (await browser.getFeature('optInGenAIFeatures')) === true && - newValue === false + isTestingWeb() || + ((await browser.getFeature('optInGenAIFeatures')) === true && + newValue === false) ) { await cleanup(compass); // Reseting the opt-in to false can be tricky so it's best to start over in this case. - compass = await init(this.test?.fullTitle(), { firstRun: true }); + compass = await init(this.test?.fullTitle(), { firstRun: false }); await setup(); return; } From 5ab9fea9c7e30aac8e59d781fbb5afa11244d651 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 8 Oct 2025 22:31:13 +0200 Subject: [PATCH 16/17] chore: enable additional features --- packages/compass-e2e-tests/tests/assistant.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index adc5d19245f..1829008a071 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -127,6 +127,14 @@ describe('MongoDB Assistant', function () { await browser.setFeature('enableGenAIFeatures', newValue); + // On compass-web, we need to set additional settings for the assistant to work + if (isTestingWeb()) { + await browser.setFeature('enableGenAIFeaturesAtlasOrg', newValue); + await browser.setFeature('cloudFeatureRolloutAccess', { + GEN_AI_COMPASS: newValue, + }); + } + if (newValue) { await browser.$(Selectors.AssistantDrawerButton).waitForDisplayed(); } else { From ebfa9768271e5eb38ab64c8175b8500d3154a0ca Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 9 Oct 2025 00:29:10 +0200 Subject: [PATCH 17/17] chore: add force enable AI Assistant in Compass Web, skip optin --- .../helpers/assistant-service.ts | 17 ++++++++- .../compass-e2e-tests/tests/assistant.test.ts | 35 ++++++++++--------- packages/compass-web/sandbox/index.tsx | 16 ++++++--- .../sandbox/sandbox-atlas-sign-in.tsx | 16 ++++++--- packages/compass-web/webpack.config.js | 10 ++++++ 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/compass-e2e-tests/helpers/assistant-service.ts b/packages/compass-e2e-tests/helpers/assistant-service.ts index d5a16f14824..4504b78f1c1 100644 --- a/packages/compass-e2e-tests/helpers/assistant-service.ts +++ b/packages/compass-e2e-tests/helpers/assistant-service.ts @@ -140,6 +140,7 @@ function sendStreamingResponse(res: http.ServerResponse, content: string) { sendChunk(); } +export const MOCK_ASSISTANT_SERVER_PORT = 27097; export async function startMockAssistantServer( { response: _response, @@ -170,6 +171,20 @@ export async function startMockAssistantServer( let response = _response; const server = http .createServer((req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Request-Origin, User-Agent' + ); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + // Only handle POST requests for chat completions if (req.method !== 'POST') { res.writeHead(404); @@ -207,7 +222,7 @@ export async function startMockAssistantServer( return sendStreamingResponse(res, response.body); }); }) - .listen(0); + .listen(MOCK_ASSISTANT_SERVER_PORT); await once(server, 'listening'); // address() returns either a string or AddressInfo. diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index 1829008a071..816e13f4f7f 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -8,14 +8,23 @@ import { cleanup, screenshotIfFailed, DEFAULT_CONNECTION_NAME_1, + skipForWeb, } from '../helpers/compass'; import type { Compass } from '../helpers/compass'; import * as Selectors from '../helpers/selectors'; import { startMockAtlasServiceServer } from '../helpers/atlas-service'; -import { startMockAssistantServer } from '../helpers/assistant-service'; +import { + MOCK_ASSISTANT_SERVER_PORT, + startMockAssistantServer, +} from '../helpers/assistant-service'; import type { MockAssistantResponse } from '../helpers/assistant-service'; import { isTestingWeb } from '../helpers/test-runner-context'; +if (isTestingWeb()) { + process.env.COMPASS_WEB_FORCE_ENABLE_AI = 'true'; + process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE = `http://localhost:${MOCK_ASSISTANT_SERVER_PORT}`; +} + describe('MongoDB Assistant', function () { let compass: Compass; let browser: CompassBrowser; @@ -39,15 +48,11 @@ describe('MongoDB Assistant', function () { const collectionName = 'entryPoints'; before(async function () { - process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN = 'true'; - mockAtlasServer = await startMockAtlasServiceServer(); mockAssistantServer = await startMockAssistantServer(); process.env.COMPASS_ATLAS_SERVICE_UNAUTH_BASE_URL_OVERRIDE = mockAtlasServer.endpoint; - process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE = - mockAssistantServer.endpoint; telemetry = await startTelemetryServer(); compass = await init(this.test?.fullTitle()); @@ -127,14 +132,6 @@ describe('MongoDB Assistant', function () { await browser.setFeature('enableGenAIFeatures', newValue); - // On compass-web, we need to set additional settings for the assistant to work - if (isTestingWeb()) { - await browser.setFeature('enableGenAIFeaturesAtlasOrg', newValue); - await browser.setFeature('cloudFeatureRolloutAccess', { - GEN_AI_COMPASS: newValue, - }); - } - if (newValue) { await browser.$(Selectors.AssistantDrawerButton).waitForDisplayed(); } else { @@ -152,7 +149,7 @@ describe('MongoDB Assistant', function () { ) { await cleanup(compass); // Reseting the opt-in to false can be tricky so it's best to start over in this case. - compass = await init(this.test?.fullTitle(), { firstRun: false }); + compass = await init(this.test?.fullTitle(), { firstRun: true }); await setup(); return; } @@ -167,9 +164,7 @@ describe('MongoDB Assistant', function () { await mockAtlasServer.stop(); await mockAssistantServer.stop(); - delete process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN; delete process.env.COMPASS_ATLAS_SERVICE_UNAUTH_BASE_URL_OVERRIDE; - delete process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE; await cleanup(compass); await telemetry.stop(); @@ -217,6 +212,10 @@ describe('MongoDB Assistant', function () { describe('before opt-in', function () { before(async function () { + skipForWeb( + this, + 'E2E testing for opt-in on compass-web is not yet implemented' + ); await setAIOptIn(false); }); @@ -281,6 +280,10 @@ describe('MongoDB Assistant', function () { describe('opting in', function () { before(async function () { + skipForWeb( + this, + 'E2E testing for opt-in on compass-web is not yet implemented' + ); await setAIOptIn(false); await openAssistantDrawer(browser); }); diff --git a/packages/compass-web/sandbox/index.tsx b/packages/compass-web/sandbox/index.tsx index 897d284d6c4..a5a6848272f 100644 --- a/packages/compass-web/sandbox/index.tsx +++ b/packages/compass-web/sandbox/index.tsx @@ -37,6 +37,9 @@ function getMetaEl(name: string) { const App = () => { const [currentTab, updateCurrentTab] = useWorkspaceTabRouter(); const { status, projectParams } = useAtlasProxySignIn(); + + const forceEnableAI = process.env.COMPASS_WEB_FORCE_ENABLE_AI === 'true'; + const { projectId, csrfToken, @@ -111,12 +114,17 @@ const App = () => { enableRollingIndexes: isAtlas, showDisabledConnections: true, enableGenAIFeaturesAtlasProject: - isAtlas && !!enableGenAIFeaturesAtlasProject, + forceEnableAI || (isAtlas && !!enableGenAIFeaturesAtlasProject), enableGenAISampleDocumentPassing: - isAtlas && !!enableGenAISampleDocumentPassing, + forceEnableAI || + (isAtlas && !!enableGenAISampleDocumentPassing), enableGenAIFeaturesAtlasOrg: - isAtlas && !!enableGenAIFeaturesAtlasOrg, - optInGenAIFeatures: isAtlas && !!optInGenAIFeatures, + forceEnableAI || (isAtlas && !!enableGenAIFeaturesAtlasOrg), + optInGenAIFeatures: + forceEnableAI || (isAtlas && !!optInGenAIFeatures), + cloudFeatureRolloutAccess: forceEnableAI + ? { GEN_AI_COMPASS: true } + : undefined, enableDataModeling: true, enableMyQueries: false, ...groupRolePreferences, diff --git a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx index 54547da6688..1b04e6e1bc4 100644 --- a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx +++ b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx @@ -128,19 +128,25 @@ export function useAtlasProxySignIn(): AtlasLoginReturnValue { featureFlags: { groupEnabledFeatureFlags }, userRoles, } = params; + const forceEnableAI = + process.env.COMPASS_WEB_FORCE_ENABLE_AI === 'true'; setProjectParams({ projectId, csrfToken, csrfTime, - optInGenAIFeatures: isOptedIntoDataExplorerGenAIFeatures, - enableGenAIFeaturesAtlasOrg: genAIFeaturesEnabled, + optInGenAIFeatures: + forceEnableAI || isOptedIntoDataExplorerGenAIFeatures, + enableGenAIFeaturesAtlasOrg: forceEnableAI || genAIFeaturesEnabled, enableGenAISampleDocumentPassing: + forceEnableAI || !groupEnabledFeatureFlags.includes( 'DISABLE_DATA_EXPLORER_GEN_AI_SAMPLE_DOCUMENT_PASSING' ), - enableGenAIFeaturesAtlasProject: groupEnabledFeatureFlags.includes( - 'ENABLE_DATA_EXPLORER_GEN_AI_FEATURES' - ), + enableGenAIFeaturesAtlasProject: + forceEnableAI || + groupEnabledFeatureFlags.includes( + 'ENABLE_DATA_EXPLORER_GEN_AI_FEATURES' + ), userRoles, }); setStatus('signed-in'); diff --git a/packages/compass-web/webpack.config.js b/packages/compass-web/webpack.config.js index d4b43ca855d..2ae15bdc4b3 100644 --- a/packages/compass-web/webpack.config.js +++ b/packages/compass-web/webpack.config.js @@ -212,6 +212,16 @@ module.exports = (env, args) => { // Can be either `web` or `webdriverio`, helpful if we need special // behavior for tests in sandbox 'process.env.APP_ENV': JSON.stringify(process.env.APP_ENV ?? 'web'), + 'process.env.COMPASS_WEB_FORCE_ENABLE_AI': JSON.stringify( + process.env.COMPASS_WEB_FORCE_ENABLE_AI ?? 'false' + ), + ...(process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE + ? { + 'process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE': JSON.stringify( + process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE + ), + } + : {}), }), new webpack.ProvidePlugin({