diff --git a/package.json b/package.json index 0d413f85a..0da8a0b37 100644 --- a/package.json +++ b/package.json @@ -644,6 +644,18 @@ "type": "string", "markdownDescription": "Model ID to override your organization's default model. This setting is only applicable to commercial users with an Ansible Lightspeed seat assignment.", "order": 4 + }, + "ansible.lightspeed.playbookGenerationCustomPrompt": { + "scope": "resource", + "type": "string", + "markdownDescription": "Custom Prompt for Playbook generation. This setting is only applicable to commercial users with an Ansible Lightspeed seat assignment.", + "order": 5 + }, + "ansible.lightspeed.playbookExplanationCustomPrompt": { + "scope": "resource", + "type": "string", + "markdownDescription": "Custom Prompt for Playbook explanation. This setting is only applicable to commercial users with an Ansible Lightspeed seat assignment.", + "order": 6 } } }, diff --git a/src/features/lightspeed/api.ts b/src/features/lightspeed/api.ts index e7352c6f6..fa7f064cf 100644 --- a/src/features/lightspeed/api.ts +++ b/src/features/lightspeed/api.ts @@ -297,6 +297,12 @@ export class LightSpeedAPI { return {} as ExplanationResponseParams; } try { + const customPrompt = + lightSpeedManager.settingsManager.settings.lightSpeedService + .playbookExplanationCustomPrompt; + if (customPrompt && customPrompt.length > 0) { + inputData.customPrompt = customPrompt; + } const requestData = { ...inputData, metadata: { ansibleExtensionVersion: this._extensionVersion }, @@ -336,6 +342,13 @@ export class LightSpeedAPI { return {} as GenerationResponseParams; } try { + const customPrompt = + lightSpeedManager.settingsManager.settings.lightSpeedService + .playbookGenerationCustomPrompt; + if (customPrompt && customPrompt.length > 0) { + inputData.customPrompt = customPrompt; + } + const requestData = { ...inputData, metadata: { ansibleExtensionVersion: this._extensionVersion }, diff --git a/src/features/lightspeed/errors.ts b/src/features/lightspeed/errors.ts index 08eeff0aa..013c9e601 100644 --- a/src/features/lightspeed/errors.ts +++ b/src/features/lightspeed/errors.ts @@ -83,29 +83,52 @@ class Errors { ? responseErrorData.message : "unknown"; } - - let detail: string = ""; - if (typeof responseErrorData.message == "string") { - detail = responseErrorData.message ?? ""; - } else if (Array.isArray(responseErrorData.message)) { - const messages = responseErrorData.message as []; - messages.forEach((value: string, index: number) => { - detail = - detail + - "(" + - (index + 1) + - ") " + - value + - (index < messages.length - 1 ? " " : ""); - }); - } + const items = (err?.response?.data as Record) ?? {}; + const detail = Object.hasOwn(items, "detail") + ? items["detail"] + : undefined; // Clone the Error to preserve the original definition - return new Error(e.code, message, detail, e.check); + return new Error( + e.code, + message, + this.prettyPrintDetail(detail), + e.check, + ); } return undefined; } + + public prettyPrintDetail(detail: unknown): string | undefined { + let pretty: string = ""; + if (detail === undefined) { + return undefined; + } else if (typeof detail == "string") { + pretty = detail ?? ""; + } else if (Array.isArray(detail)) { + const items = detail as []; + items.forEach((value: string, index: number) => { + pretty = + pretty + + "(" + + (index + 1) + + ") " + + value + + (index < items.length - 1 ? " " : ""); + }); + } else if (detail instanceof Object && detail.constructor === Object) { + const items = detail as Record; + const keys: string[] = Object.keys(detail); + keys.forEach((key, index) => { + pretty = + pretty + + `${key}: ${items[key]}` + + (index < keys.length - 1 ? " " : ""); + }); + } + return pretty; + } } export const ERRORS = new Errors(); diff --git a/src/features/lightspeed/handleApiError.ts b/src/features/lightspeed/handleApiError.ts index 4621137a7..6c18dc168 100644 --- a/src/features/lightspeed/handleApiError.ts +++ b/src/features/lightspeed/handleApiError.ts @@ -21,33 +21,38 @@ export function mapError(err: AxiosError): IError { } // If the error is unknown fallback to defaults - const detail = err.response?.data; + const items = (err?.response?.data as Record) ?? {}; + const detail = Object.hasOwn(items, "detail") ? items["detail"] : undefined; const status: number | string = err?.response?.status ?? err?.code ?? 500; if (err instanceof CanceledError) { return ERRORS_CONNECTION_CANCELED_TIMEOUT; } if (status === 400) { - return ERRORS_BAD_REQUEST.withDetail(detail); + return ERRORS_BAD_REQUEST.withDetail(ERRORS.prettyPrintDetail(detail)); } if (status === 401) { - return ERRORS_UNAUTHORIZED.withDetail(detail); + return ERRORS_UNAUTHORIZED.withDetail(ERRORS.prettyPrintDetail(detail)); } if (status === 403) { - return ERRORS_UNAUTHORIZED.withDetail(detail); + return ERRORS_UNAUTHORIZED.withDetail(ERRORS.prettyPrintDetail(detail)); } if (status === 404) { - return ERRORS_NOT_FOUND.withDetail(detail); + return ERRORS_NOT_FOUND.withDetail(ERRORS.prettyPrintDetail(detail)); } if (status === 429) { - return ERRORS_TOO_MANY_REQUESTS.withDetail(detail); + return ERRORS_TOO_MANY_REQUESTS.withDetail( + ERRORS.prettyPrintDetail(detail), + ); } if (status === 500) { - return ERRORS_UNKNOWN.withDetail(detail); + return ERRORS_UNKNOWN.withDetail(ERRORS.prettyPrintDetail(detail)); } if (status === AxiosError.ECONNABORTED) { - return ERRORS_CONNECTION_TIMEOUT.withDetail(detail); + return ERRORS_CONNECTION_TIMEOUT.withDetail( + ERRORS.prettyPrintDetail(detail), + ); } console.log(`Lightspeed request failed with unknown error ${err}`); - return ERRORS_UNKNOWN.withDetail(detail); + return ERRORS_UNKNOWN.withDetail(ERRORS.prettyPrintDetail(detail)); } diff --git a/src/features/lightspeed/playbookExplanation.ts b/src/features/lightspeed/playbookExplanation.ts index 362babac2..f52015a4c 100644 --- a/src/features/lightspeed/playbookExplanation.ts +++ b/src/features/lightspeed/playbookExplanation.ts @@ -100,15 +100,25 @@ export const playbookExplanation = async (extensionUri: vscode.Uri) => { console.log(response); if (isError(response)) { const oneClickTrialProvider = getOneClickTrialProvider(); - const my_error = response as IError; - if (!(await oneClickTrialProvider.showPopup(my_error))) { - vscode.window.showErrorMessage(my_error.message ?? UNKNOWN_ERROR); + if (!(await oneClickTrialProvider.showPopup(response))) { + const errorMessage: string = `${response.message ?? UNKNOWN_ERROR} ${response.detail ?? ""}`; + vscode.window.showErrorMessage(errorMessage); currentPanel.setContent( - `

The operation has failed:

${my_error.message}

`, + `

The operation has failed:

${errorMessage}

`, ); } } else { markdown = response.content; + if (markdown.length === 0) { + markdown = "### No explanation provided."; + const customPrompt = + lightSpeedManager.settingsManager.settings.lightSpeedService + .playbookExplanationCustomPrompt ?? ""; + if (customPrompt.length > 0) { + markdown += + "\n\nYou may want to consider amending your custom prompt."; + } + } const html_snippet = marked.parse(markdown) as string; currentPanel.setContent(html_snippet, true); } diff --git a/src/features/lightspeed/playbookGeneration.ts b/src/features/lightspeed/playbookGeneration.ts index 1c052a96b..82ac3457e 100644 --- a/src/features/lightspeed/playbookGeneration.ts +++ b/src/features/lightspeed/playbookGeneration.ts @@ -156,9 +156,8 @@ export async function showPlaybookGenerationPage( if (isError(response)) { const oneClickTrialProvider = getOneClickTrialProvider(); if (!(await oneClickTrialProvider.showPopup(response))) { - vscode.window.showErrorMessage( - response.message ?? UNKNOWN_ERROR, - ); + const errorMessage: string = `${response.message ?? UNKNOWN_ERROR} ${response.detail ?? ""}`; + vscode.window.showErrorMessage(errorMessage); } } else { panel.webview.postMessage({ @@ -198,7 +197,8 @@ export async function showPlaybookGenerationPage( panel, ); if (isError(response)) { - vscode.window.showErrorMessage(response.message ?? UNKNOWN_ERROR); + const errorMessage: string = `${response.message ?? UNKNOWN_ERROR} ${response.detail ?? ""}`; + vscode.window.showErrorMessage(errorMessage); break; } playbook = response.playbook; diff --git a/src/interfaces/extensionSettings.ts b/src/interfaces/extensionSettings.ts index 8d8b0767d..ba5c31757 100644 --- a/src/interfaces/extensionSettings.ts +++ b/src/interfaces/extensionSettings.ts @@ -44,4 +44,6 @@ export interface LightSpeedServiceSettings { URL: string; suggestions: { enabled: boolean; waitWindow: number }; model: string | undefined; + playbookGenerationCustomPrompt: string | undefined; + playbookExplanationCustomPrompt: string | undefined; } diff --git a/src/interfaces/lightspeed.ts b/src/interfaces/lightspeed.ts index 01e6b9cfa..dcb42ddfb 100644 --- a/src/interfaces/lightspeed.ts +++ b/src/interfaces/lightspeed.ts @@ -142,6 +142,7 @@ export interface ISuggestionDetails { export interface GenerationRequestParams { text: string; + customPrompt?: string; outline?: string; generationId: string; createOutline: boolean; @@ -156,6 +157,7 @@ export interface GenerationResponseParams { export interface ExplanationRequestParams { content: string; + customPrompt?: string; explanationId: string; } diff --git a/src/settings.ts b/src/settings.ts index 3438d38d3..bea9c9bd8 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -47,6 +47,14 @@ export class SettingsManager { waitWindow: lightSpeedSettings.get("suggestions.waitWindow", 0), }, model: lightSpeedSettings.get("modelIdOverride", undefined), + playbookGenerationCustomPrompt: lightSpeedSettings.get( + "playbookGenerationCustomPrompt", + undefined, + ), + playbookExplanationCustomPrompt: lightSpeedSettings.get( + "playbookExplanationCustomPrompt", + undefined, + ), }, playbook: { arguments: playbookSettings.get("arguments", ""), diff --git a/src/webview/apps/lightspeed/playbookGeneration/main.ts b/src/webview/apps/lightspeed/playbookGeneration/main.ts index 2023467dd..b4770946b 100644 --- a/src/webview/apps/lightspeed/playbookGeneration/main.ts +++ b/src/webview/apps/lightspeed/playbookGeneration/main.ts @@ -76,6 +76,7 @@ window.addEventListener("message", async (event) => { } case "outline": { setupPage(2); + outline.update(message.outline.outline); savedPlaybook = message.outline.playbook; generationId = message.outline.generationId; diff --git a/test/mockLightspeedServer/explanations.ts b/test/mockLightspeedServer/explanations.ts index 8bd5e5178..8459b1444 100644 --- a/test/mockLightspeedServer/explanations.ts +++ b/test/mockLightspeedServer/explanations.ts @@ -9,6 +9,7 @@ export function explanations( res: any, ) { const playbook = req.body.content; + const customPrompt = req.body.customPrompt; const explanationId = req.body.explanationId ? req.body.explanationId : uuidv4(); @@ -28,8 +29,28 @@ export function explanations( }); } + // Special case to replicate explanation being unavailable + if (playbook !== undefined && playbook.includes("No explanation available")) { + logger.info("Returning empty content. Explanation is not available"); + return res.send({ + content: "", + format, + explanationId, + }); + } + + // Special case to replicate a broken custom prompt + if (customPrompt && customPrompt === "custom prompt broken") { + logger.info("Returning 400. Custom prompt is invalid"); + return res.status(400).send({ + code: "validation", + message: "invalid", + detail: { customPrompt: "custom prompt is invalid" }, + }); + } + // cSpell: disable - const content = ` + let content = ` ## Playbook Overview and Structure This playbook creates an Azure Virtual Network (VNET) with the name "VNET_1" @@ -73,6 +94,10 @@ the following parameters: `; // cSpell: enable + if (customPrompt) { + content += "\nCustom prompt explanation."; + } + return res.send({ content, format, diff --git a/test/mockLightspeedServer/generations.ts b/test/mockLightspeedServer/generations.ts index 63ef408a3..49ffb59e5 100644 --- a/test/mockLightspeedServer/generations.ts +++ b/test/mockLightspeedServer/generations.ts @@ -8,6 +8,7 @@ export function generations( res: any, ) { const text = req.body.text; + const customPrompt = req.body.customPrompt; const createOutline = req.body.createOutline; const generationId = req.body.generationId ? req.body.generationId : uuidv4(); const wizardId = req.body.wizardId; @@ -46,6 +47,16 @@ export function generations( }); } + // Special case to replicate a broken custom prompt + if (customPrompt && customPrompt === "custom prompt broken") { + logger.info("Returning 400. Custom prompt is invalid"); + return res.status(400).send({ + code: "validation", + message: "invalid", + detail: { customPrompt: "custom prompt is invalid" }, + }); + } + // cSpell: disable let outline: string | undefined = `1. Create VNET named VNET_1 2. Create VNET named VNET_2 @@ -98,6 +109,10 @@ export function generations( outline += "\n4. Some extra step."; } + if (customPrompt) { + outline += "\n5. Custom prompt step."; + } + return res.send({ playbook, outline, diff --git a/test/testFixtures/lightspeed/playbook_explanation_none.yml b/test/testFixtures/lightspeed/playbook_explanation_none.yml new file mode 100644 index 000000000..498e07f1a --- /dev/null +++ b/test/testFixtures/lightspeed/playbook_explanation_none.yml @@ -0,0 +1,5 @@ +--- +- name: + hosts: all + tasks: + - name: No explanation available... diff --git a/test/ui-test/lightspeedUiTest.ts b/test/ui-test/lightspeedUiTest.ts index f53631e17..c225d195a 100644 --- a/test/ui-test/lightspeedUiTest.ts +++ b/test/ui-test/lightspeedUiTest.ts @@ -498,6 +498,7 @@ export function lightspeedUIAssetsTest(): void { .undefined; let text = await outlineList.getText(); expect(text.includes("Create virtual network peering")).to.be.true; + expect(text.includes("Custom prompt step")).not.to.be.true; // Verify the prompt is displayed as a static text const prompt = await webView.findWebElement( @@ -629,6 +630,132 @@ export function lightspeedUIAssetsTest(): void { } }); + it("Playbook generation webview works as expected (fast path, custom prompt)", async function () { + // Execute only when TEST_LIGHTSPEED_URL environment variable is defined. + if (process.env.TEST_LIGHTSPEED_URL) { + // Set Playbook generation custom prompt + settingsEditor = await workbench.openSettings(); + await updateSettings( + settingsEditor, + "ansible.lightspeed.playbookGenerationCustomPrompt", + "custom prompt", + ); + await workbench.executeCommand("View: Close All Editor Groups"); + + // Open playbook generation webview. + await workbench.executeCommand( + "Ansible Lightspeed: Playbook generation", + ); + await sleep(2000); + const webView = await new WebView(); + expect(webView, "webView should not be undefined").not.to.be.undefined; + await webView.switchToFrame(5000); + expect( + webView, + "webView should not be undefined after switching to its frame", + ).not.to.be.undefined; + + // Set input text and invoke summaries API + const textArea = await webView.findWebElement( + By.xpath("//vscode-text-area"), + ); + expect(textArea, "textArea should not be undefined").not.to.be + .undefined; + const submitButton = await webView.findWebElement( + By.xpath("//vscode-button[@id='submit-button']"), + ); + expect(submitButton, "submitButton should not be undefined").not.to.be + .undefined; + // + // Note: Following line should succeed, but fails for some unknown reasons. + // + // expect((await submitButton.isEnabled()), "submit button should be disabled by default").is.false; + await textArea.sendKeys("Create an azure network."); + expect( + await submitButton.isEnabled(), + "submit button should be enabled now", + ).to.be.true; + await submitButton.click(); + await sleep(1000); + + // Verify outline output and text edit + const outlineList = await webView.findWebElement( + By.xpath("//ol[@id='outline-list']"), + ); + expect(outlineList, "An ordered list should exist.").to.be.not + .undefined; + const text = await outlineList.getText(); + expect(text.includes("Custom prompt step")).to.be.true; + + await webView.switchBack(); + await workbench.executeCommand("View: Close All Editor Groups"); + } else { + this.skip(); + } + }); + + it("Playbook generation webview works as expected (fast path, custom prompt, broken)", async function () { + // Execute only when TEST_LIGHTSPEED_URL environment variable is defined. + if (process.env.TEST_LIGHTSPEED_URL) { + // Set Playbook generation custom prompt + settingsEditor = await workbench.openSettings(); + await updateSettings( + settingsEditor, + "ansible.lightspeed.playbookGenerationCustomPrompt", + "custom prompt broken", + ); + await workbench.executeCommand("View: Close All Editor Groups"); + + // Open playbook generation webview. + await workbench.executeCommand( + "Ansible Lightspeed: Playbook generation", + ); + await sleep(2000); + const webView = await new WebView(); + expect(webView, "webView should not be undefined").not.to.be.undefined; + await webView.switchToFrame(5000); + expect( + webView, + "webView should not be undefined after switching to its frame", + ).not.to.be.undefined; + + // Set input text and invoke summaries API + const textArea = await webView.findWebElement( + By.xpath("//vscode-text-area"), + ); + expect(textArea, "textArea should not be undefined").not.to.be + .undefined; + const submitButton = await webView.findWebElement( + By.xpath("//vscode-button[@id='submit-button']"), + ); + expect(submitButton, "submitButton should not be undefined").not.to.be + .undefined; + // + // Note: Following line should succeed, but fails for some unknown reasons. + // + // expect((await submitButton.isEnabled()), "submit button should be disabled by default").is.false; + await textArea.sendKeys("Create an azure network."); + expect( + await submitButton.isEnabled(), + "submit button should be enabled now", + ).to.be.true; + await submitButton.click(); + await sleep(2000); + + await webView.switchBack(); + + const notifications = await workbench.getNotifications(); + const notification = notifications[0]; + expect(await notification.getMessage()).equals( + "Bad Request response. Please try again. customPrompt: custom prompt is invalid", + ); + + await workbench.executeCommand("View: Close All Editor Groups"); + } else { + this.skip(); + } + }); + it("Playbook explanation webview works as expected", async function () { if (process.env.TEST_LIGHTSPEED_URL) { const folder = "lightspeed"; @@ -663,6 +790,7 @@ export function lightspeedUIAssetsTest(): void { expect(mainDiv, "mainDiv should not be undefined").not.to.be.undefined; const text = await mainDiv.getText(); expect(text.includes("Playbook Overview and Structure")).to.be.true; + expect(text.includes("Custom prompt explanation")).not.to.be.true; await webView.switchBack(); await workbench.executeCommand("View: Close All Editor Groups"); @@ -693,6 +821,7 @@ export function lightspeedUIAssetsTest(): void { expect(group, "Group 1 of the editor view should be undefined").to.be .undefined; + await webView.switchBack(); await workbench.executeCommand("View: Close All Editor Groups"); } else { this.skip(); @@ -745,6 +874,216 @@ export function lightspeedUIAssetsTest(): void { } }); + it("Playbook explanation webview works as expected, no explanation", async function () { + if (process.env.TEST_LIGHTSPEED_URL) { + const folder = "lightspeed"; + const file = "playbook_explanation_none.yml"; + const filePath = getFixturePath(folder, file); + + // Open file in the editor + await VSBrowser.instance.openResources(filePath); + + // Open playbook explanation webview. + await workbench.executeCommand( + "Explain the playbook with Ansible Lightspeed", + ); + await sleep(2000); + + // Locate the playbook explanation webview + const webView = (await new EditorView().openEditor( + "Explanation", + 1, + )) as WebView; + expect(webView, "webView should not be undefined").not.to.be.undefined; + await webView.switchToFrame(5000); + expect( + webView, + "webView should not be undefined after switching to its frame", + ).not.to.be.undefined; + + // Find the main div element of the webview and verify the expected text is found. + const mainDiv = await webView.findWebElement( + By.xpath("//div[contains(@class, 'playbookGeneration') ]"), + ); + expect(mainDiv, "mainDiv should not be undefined").not.to.be.undefined; + const text = await mainDiv.getText(); + expect(text.includes("No explanation provided")).to.be.true; + + await webView.switchBack(); + await workbench.executeCommand("View: Close All Editor Groups"); + } else { + this.skip(); + } + }); + + it("Playbook explanation webview works as expected, no explanation, custom prompt", async function () { + if (process.env.TEST_LIGHTSPEED_URL) { + const folder = "lightspeed"; + const file = "playbook_explanation_none.yml"; + const filePath = getFixturePath(folder, file); + + // Set Playbook generation custom prompt + settingsEditor = await workbench.openSettings(); + await updateSettings( + settingsEditor, + "ansible.lightspeed.playbookExplanationCustomPrompt", + "custom prompt", + ); + await workbench.executeCommand("View: Close All Editor Groups"); + + // Open file in the editor + await VSBrowser.instance.openResources(filePath); + + // Open playbook explanation webview. + await workbench.executeCommand( + "Explain the playbook with Ansible Lightspeed", + ); + await sleep(2000); + + // Locate the playbook explanation webview + const webView = (await new EditorView().openEditor( + "Explanation", + 1, + )) as WebView; + expect(webView, "webView should not be undefined").not.to.be.undefined; + await webView.switchToFrame(5000); + expect( + webView, + "webView should not be undefined after switching to its frame", + ).not.to.be.undefined; + + // Find the main div element of the webview and verify the expected text is found. + const mainDiv = await webView.findWebElement( + By.xpath("//div[contains(@class, 'playbookGeneration') ]"), + ); + expect(mainDiv, "mainDiv should not be undefined").not.to.be.undefined; + const text = await mainDiv.getText(); + expect(text.includes("No explanation provided")).to.be.true; + expect( + text.includes("You may want to consider amending your custom prompt"), + ).to.be.true; + + await webView.switchBack(); + await workbench.executeCommand("View: Close All Editor Groups"); + } else { + this.skip(); + } + }); + + it("Playbook explanation webview works as expected, custom prompt", async function () { + if (process.env.TEST_LIGHTSPEED_URL) { + const folder = "lightspeed"; + const file = "playbook_4.yml"; + const filePath = getFixturePath(folder, file); + + // Set Playbook generation custom prompt + settingsEditor = await workbench.openSettings(); + await updateSettings( + settingsEditor, + "ansible.lightspeed.playbookExplanationCustomPrompt", + "custom prompt", + ); + await workbench.executeCommand("View: Close All Editor Groups"); + + // Open file in the editor + await VSBrowser.instance.openResources(filePath); + + // Open playbook explanation webview. + await workbench.executeCommand( + "Explain the playbook with Ansible Lightspeed", + ); + await sleep(2000); + + // Locate the playbook explanation webview + const webView = (await new EditorView().openEditor( + "Explanation", + 1, + )) as WebView; + expect(webView, "webView should not be undefined").not.to.be.undefined; + await webView.switchToFrame(5000); + expect( + webView, + "webView should not be undefined after switching to its frame", + ).not.to.be.undefined; + + // Find the main div element of the webview and verify the expected text is found. + const mainDiv = await webView.findWebElement( + By.xpath("//div[contains(@class, 'playbookGeneration') ]"), + ); + expect(mainDiv, "mainDiv should not be undefined").not.to.be.undefined; + const text = await mainDiv.getText(); + expect(text.includes("Playbook Overview and Structure")).to.be.true; + expect(text.includes("Custom prompt explanation")).to.be.true; + + await webView.switchBack(); + await workbench.executeCommand("View: Close All Editor Groups"); + } else { + this.skip(); + } + }); + + it("Playbook explanation webview works as expected, custom prompt, broken", async function () { + if (process.env.TEST_LIGHTSPEED_URL) { + const folder = "lightspeed"; + const file = "playbook_4.yml"; + const filePath = getFixturePath(folder, file); + + // Set Playbook generation custom prompt + settingsEditor = await workbench.openSettings(); + await updateSettings( + settingsEditor, + "ansible.lightspeed.playbookExplanationCustomPrompt", + "custom prompt broken", + ); + await workbench.executeCommand("View: Close All Editor Groups"); + + // Open file in the editor + await VSBrowser.instance.openResources(filePath); + + // Open playbook explanation webview. + await workbench.executeCommand( + "Explain the playbook with Ansible Lightspeed", + ); + await sleep(2000); + + // Locate the playbook explanation webview + const webView = (await new EditorView().openEditor( + "Explanation", + 1, + )) as WebView; + expect(webView, "webView should not be undefined").not.to.be.undefined; + await webView.switchToFrame(5000); + expect( + webView, + "webView should not be undefined after switching to its frame", + ).not.to.be.undefined; + + // Find the main div element of the webview and verify the expected text is found. + const mainDiv = await webView.findWebElement( + By.xpath("//div[contains(@class, 'playbookGeneration') ]"), + ); + expect(mainDiv, "mainDiv should not be undefined").not.to.be.undefined; + const text = await mainDiv.getText(); + expect( + text.includes( + "Bad Request response. Please try again. customPrompt: custom prompt is invalid", + ), + ).to.be.true; + + await webView.switchBack(); + + const notifications = await workbench.getNotifications(); + const notification = notifications[0]; + expect(await notification.getMessage()).equals( + "Bad Request response. Please try again. customPrompt: custom prompt is invalid", + ); + + await workbench.executeCommand("View: Close All Editor Groups"); + } else { + this.skip(); + } + }); + after(async function () { if (process.env.TEST_LIGHTSPEED_URL) { settingsEditor = await workbench.openSettings(); @@ -758,6 +1097,16 @@ export function lightspeedUIAssetsTest(): void { "ansible.lightspeed.URL", "https://c.ai.ansible.redhat.com", ); + await updateSettings( + settingsEditor, + "ansible.lightspeed.playbookGenerationCustomPrompt", + "", + ); + await updateSettings( + settingsEditor, + "ansible.lightspeed.playbookExplanationCustomPrompt", + "", + ); } }); }); diff --git a/test/units/lightspeed/utils/handleApiError.test.ts b/test/units/lightspeed/utils/handleApiError.test.ts index 52ee136c9..3c1ac8b0e 100644 --- a/test/units/lightspeed/utils/handleApiError.test.ts +++ b/test/units/lightspeed/utils/handleApiError.test.ts @@ -3,6 +3,7 @@ require("assert"); import { AxiosError, AxiosHeaders } from "axios"; import { mapError } from "../../../../src/features/lightspeed/handleApiError"; import assert from "assert"; +import { integer } from "vscode-languageclient"; function createError( http_code: number, @@ -37,6 +38,14 @@ function createError( } describe("testing the error handling", () => { + function withDetailTest(statusCode: integer, expectedMessage: string) { + const error = mapError( + createError(statusCode, { detail: { item: "details" } }), + ); + assert.equal(error.message, expectedMessage); + assert.equal(error.detail, "item: details"); + } + // ================================= // HTTP 200 // --------------------------------- @@ -93,7 +102,7 @@ describe("testing the error handling", () => { const error = mapError( createError(400, { code: "error__preprocess_invalid_yaml", - message: "A simple error.", + detail: "A simple error.", }), ); assert.equal( @@ -107,7 +116,7 @@ describe("testing the error handling", () => { const error = mapError( createError(400, { code: "error__preprocess_invalid_yaml", - message: ["error 1", "error 2"], + detail: ["error 1", "error 2"], }), ); assert.equal( @@ -117,6 +126,21 @@ describe("testing the error handling", () => { assert.equal(error.detail, "(1) error 1 (2) error 2"); }); + it("err generic validation error", () => { + const error = mapError( + createError(400, { + detail: { + field1: "field 1 is invalid", + field2: "field 2 is also invalid", + }, + }), + ); + assert.equal( + error.detail, + "field1: field 1 is invalid field2: field 2 is also invalid", + ); + }); + it("err Feedback validation error", () => { const error = mapError( createError(400, { @@ -138,6 +162,13 @@ describe("testing the error handling", () => { "You are not authorized to access Ansible Lightspeed. Please contact your administrator.", ); }); + + it("err Unauthorized with detail", () => { + withDetailTest( + 401, + "You are not authorized to access Ansible Lightspeed. Please contact your administrator.", + ); + }); // ================================= // ================================= @@ -290,6 +321,13 @@ describe("testing the error handling", () => { "Your organization does not have a subscription. Please contact your administrator.", ); }); + + it("err Forbidden with detail", () => { + withDetailTest( + 403, + "You are not authorized to access Ansible Lightspeed. Please contact your administrator.", + ); + }); // ================================= // ================================= @@ -314,6 +352,13 @@ describe("testing the error handling", () => { "The requested action is not available in your environment.", ); }); + + it("err Not found with detail", () => { + withDetailTest( + 404, + "The resource could not be found. Please try again later.", + ); + }); // ================================= // ================================= @@ -342,6 +387,13 @@ describe("testing the error handling", () => { "Too many requests to Ansible Lightspeed. Please try again later.", ); }); + + it("err Too Many Requests with detail", () => { + withDetailTest( + 429, + "Too many requests to Ansible Lightspeed. Please try again later.", + ); + }); // ================================= // ================================= @@ -398,6 +450,13 @@ describe("testing the error handling", () => { "IBM watsonx Code Assistant request/response correlation failed. Please contact your administrator.", ); }); + + it("err Internal Server Error - Generic with detail", () => { + withDetailTest( + 500, + "An error occurred attempting to complete your request. Please try again later.", + ); + }); // ================================= // =================================