From 19d0346db98d45480f9909629b50daf31cba1186 Mon Sep 17 00:00:00 2001 From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:19:01 -0500 Subject: [PATCH 1/6] fix(amazonq): right click options dont send context if chat not loaded (#6527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem: When a user starts up VSC, hasn't opened Q chat yet, then highlights text and does a right click option like "send to prompt" THEN the whole prompt fails to make it to chat. This is because there is a race condition between the message being sent to the webview, and the webview completed loading and ready to recieve messages. ## Solution: In the appToMessage publisher, verify that the chat is ready to recieve messages based on if the webview sent the chat ui ready message. Otherwise wait until we get it, and then send the message. ### Demo #### Before https://github.com/user-attachments/assets/baf86737-4a5a-4f80-86fb-9abca2c56827 #### After  https://github.com/user-attachments/assets/c3361403-5dae-49df-a878-94722eb4fc62 --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Signed-off-by: nkomonen-amazon --- ...-704d175a-a99f-4ce7-bf08-b94d6f7218c0.json | 4 ++ packages/core/src/amazonq/apps/initContext.ts | 8 ++-- .../src/amazonq/messages/messagePublisher.ts | 45 +++++++++++++++++++ .../webview/messages/messageDispatcher.ts | 3 +- packages/core/src/shared/logger/logger.ts | 2 +- 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-704d175a-a99f-4ce7-bf08-b94d6f7218c0.json diff --git a/packages/amazonq/.changes/next-release/Bug Fix-704d175a-a99f-4ce7-bf08-b94d6f7218c0.json b/packages/amazonq/.changes/next-release/Bug Fix-704d175a-a99f-4ce7-bf08-b94d6f7218c0.json new file mode 100644 index 00000000000..01d6ec7c8b0 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-704d175a-a99f-4ce7-bf08-b94d6f7218c0.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "`Send to prompt` and other context menu options not sent if chat was closed" +} diff --git a/packages/core/src/amazonq/apps/initContext.ts b/packages/core/src/amazonq/apps/initContext.ts index e33fafca7c3..f8b06e4b46a 100644 --- a/packages/core/src/amazonq/apps/initContext.ts +++ b/packages/core/src/amazonq/apps/initContext.ts @@ -4,20 +4,20 @@ */ import { EventEmitter } from 'vscode' -import { MessagePublisher } from '../messages/messagePublisher' +import { MessagePublisher, UiMessagePublisher } from '../messages/messagePublisher' import { MessageListener } from '../messages/messageListener' import { TabType } from '../webview/ui/storages/tabsStorage' export interface AmazonQAppInitContext { registerWebViewToAppMessagePublisher(eventEmitter: MessagePublisher, tabType: TabType): void - getAppsToWebViewMessagePublisher(): MessagePublisher + getAppsToWebViewMessagePublisher(): UiMessagePublisher onDidChangeAmazonQVisibility: EventEmitter } export class DefaultAmazonQAppInitContext implements AmazonQAppInitContext { private readonly appsToWebViewEventEmitter = new EventEmitter() private readonly appsToWebViewMessageListener = new MessageListener(this.appsToWebViewEventEmitter) - private readonly appsToWebViewMessagePublisher = new MessagePublisher(this.appsToWebViewEventEmitter) + private readonly appsToWebViewMessagePublisher = new UiMessagePublisher(this.appsToWebViewEventEmitter) private readonly webViewToAppsMessagePublishers: Map> = new Map() public readonly onDidChangeAmazonQVisibility = new EventEmitter() @@ -41,7 +41,7 @@ export class DefaultAmazonQAppInitContext implements AmazonQAppInitContext { return this.appsToWebViewMessageListener } - getAppsToWebViewMessagePublisher(): MessagePublisher { + getAppsToWebViewMessagePublisher(): UiMessagePublisher { return this.appsToWebViewMessagePublisher } } diff --git a/packages/core/src/amazonq/messages/messagePublisher.ts b/packages/core/src/amazonq/messages/messagePublisher.ts index d2987a99536..7fa6976b9a9 100644 --- a/packages/core/src/amazonq/messages/messagePublisher.ts +++ b/packages/core/src/amazonq/messages/messagePublisher.ts @@ -12,3 +12,48 @@ export class MessagePublisher { this.eventEmitter.fire(event) } } + +/** + * Same as {@link MessagePublisher}, but will wait until the UI indicates it + * is ready to recieve messages, before the message is published. + * + * This solves a problem when running a right click menu option like + * "Send To Prompt" BUT chat is not opened yet, it would result in the prompt failing to + * be recieved by chat. + */ +export class UiMessagePublisher extends MessagePublisher { + private isUiReady: boolean = false + private buffer: T[] = [] + + constructor(eventEmitter: EventEmitter) { + super(eventEmitter) + } + + public override publish(event: T): void { + // immediately send if Chat UI is ready + if (this.isUiReady) { + super.publish(event) + return + } + + this.buffer.push(event) + } + + /** + * Indicate the Q Chat UI is ready to recieve messages. + */ + public setUiReady() { + this.isUiReady = true + this.flush() + } + + /** + * Publishes all blocked messages + */ + private flush() { + for (const msg of this.buffer) { + super.publish(msg) + } + this.buffer = [] + } +} diff --git a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts index 8acd04e8953..fdd1584b767 100644 --- a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts +++ b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts @@ -13,6 +13,7 @@ import { telemetry } from '../../../shared/telemetry' import { AmazonQChatMessageDuration } from '../../messages/chatMessageDuration' import { globals, openUrl } from '../../../shared' import { isClickTelemetry, isOpenAgentTelemetry } from '../ui/telemetry/actions' +import { DefaultAmazonQAppInitContext } from '../../apps/initContext' export function dispatchWebViewMessagesToApps( webview: Webview, @@ -21,12 +22,12 @@ export function dispatchWebViewMessagesToApps( webview.onDidReceiveMessage((msg) => { switch (msg.command) { case 'ui-is-ready': { + DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessagePublisher().setUiReady() /** * ui-is-ready isn't associated to any tab so just record the telemetry event and continue. * This would be equivalent of the duration between "user clicked open q" and "ui has become available" * NOTE: Amazon Q UI is only loaded ONCE. The state is saved between each hide/show of the webview. */ - telemetry.webview_load.emit({ webviewName: 'amazonq', duration: performance.measure(amazonqMark.uiReady, amazonqMark.open).duration, diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index d5bf5f13380..19be5ac2887 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' -export type LogTopic = 'crashMonitoring' | 'dev/beta' | 'notifications' | 'test' | 'childProcess' | 'unknown' +export type LogTopic = 'crashMonitoring' | 'dev/beta' | 'notifications' | 'test' | 'childProcess' | 'unknown' | 'chat' class ErrorLog { constructor( From 750770ff4e0d68930d89a0a5751ad51cb0ddafa4 Mon Sep 17 00:00:00 2001 From: David <60020664+dhasani23@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:21:13 -0500 Subject: [PATCH 2/6] feat(amazonq): auto-download results (#6519) ## Problem Transformation results/artifacts expire after 24h, and sometimes users forget to manually click the "Download Proposed Changes" button within that time period. ## Solution Auto-download the results from S3 when ready. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: David Hasani --- .../Feature-d9128dff-2867-4bf4-9046-2a52a36803d7.json | 4 ++++ .../core/src/codewhisperer/commands/startTransformByQ.ts | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 packages/amazonq/.changes/next-release/Feature-d9128dff-2867-4bf4-9046-2a52a36803d7.json diff --git a/packages/amazonq/.changes/next-release/Feature-d9128dff-2867-4bf4-9046-2a52a36803d7.json b/packages/amazonq/.changes/next-release/Feature-d9128dff-2867-4bf4-9046-2a52a36803d7.json new file mode 100644 index 00000000000..add00c79b5e --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-d9128dff-2867-4bf4-9046-2a52a36803d7.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "/transform: automatically download results when ready" +} diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index 50be99b7fcf..cec513d4f58 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -775,6 +775,12 @@ export async function postTransformationJob() { if (transformByQState.getPayloadFilePath() !== '') { fs.rmSync(transformByQState.getPayloadFilePath(), { recursive: true, force: true }) // delete ZIP if it exists } + + // attempt download for user + // TODO: refactor as explained here https://github.com/aws/aws-toolkit-vscode/pull/6519/files#r1946873107 + if (transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded()) { + await vscode.commands.executeCommand('aws.amazonq.transformationHub.reviewChanges.startReview') + } } export async function transformationJobErrorHandler(error: any) { From b3987695f64c9f7c6b6a6eaa01beea0c59a0a1b4 Mon Sep 17 00:00:00 2001 From: Dan Knutsen Date: Fri, 7 Feb 2025 17:00:01 -0600 Subject: [PATCH 3/6] feat(amazonq): support .hbs, .gjs, .gts, .astro, .mdx, .erb, .rake, .svelte files in /dev (#6523) ## Problem The following filetypes are not currently supported in `/dev`: - .hbs (Handlebars) - .gjs/.gts (Glimmer JavaScript/Glimmer TypeScript) - .astro/.mdx (Astro) - .svelte (Svelte) - .erb/.rake (Ruby) ## Solution Add .hbs, .gjs, .gts, .astro, .mdx, .svelte, .erb, and .rake to list of supported file extensions --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../Feature-d86356a0-bc42-4187-a7f8-205afbc25d76.json | 4 ++++ packages/core/src/shared/filetypes.ts | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 packages/amazonq/.changes/next-release/Feature-d86356a0-bc42-4187-a7f8-205afbc25d76.json diff --git a/packages/amazonq/.changes/next-release/Feature-d86356a0-bc42-4187-a7f8-205afbc25d76.json b/packages/amazonq/.changes/next-release/Feature-d86356a0-bc42-4187-a7f8-205afbc25d76.json new file mode 100644 index 00000000000..510dd1c150d --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-d86356a0-bc42-4187-a7f8-205afbc25d76.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Amazon Q /dev: support `.hbs`, `.gjs`, `.gts`, `.astro`, `.mdx`, `.svelte`, `.erb`, `.rake` files" +} diff --git a/packages/core/src/shared/filetypes.ts b/packages/core/src/shared/filetypes.ts index 843397e6a17..9457592c8b2 100644 --- a/packages/core/src/shared/filetypes.ts +++ b/packages/core/src/shared/filetypes.ts @@ -155,6 +155,7 @@ export const codefileExtensions = new Set([ '.ads', '.apl', '.asm', + '.astro', '.awk', '.b', '.bas', @@ -195,6 +196,7 @@ export const codefileExtensions = new Set([ '.el', '.elm', '.env', + '.erb', '.erl', '.ex', '.exs', @@ -211,6 +213,7 @@ export const codefileExtensions = new Set([ '.fsx', '.gd', '.gitignore', + '.gjs', '.go', '.gql', '.gradle', @@ -220,9 +223,11 @@ export const codefileExtensions = new Set([ '.gsp', '.gst', '.gsx', + '.gts', '.gvy', '.h', '.hack', + '.hbs', '.hh', '.hpp', '.hrl', @@ -253,6 +258,7 @@ export const codefileExtensions = new Set([ '.mak', '.makefile', '.md', + '.mdx', '.mjs', '.ml', '.mli', @@ -289,6 +295,7 @@ export const codefileExtensions = new Set([ '.pyw', '.qs', '.r', + '.rake', '.raku', '.rakumod', '.rakutest', @@ -331,6 +338,7 @@ export const codefileExtensions = new Set([ '.ss', '.st', '.sv', + '.svelte', '.svg', '.swift', '.t', From 53f11e7a7c35fd30651860265e766a68a5201aef Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 10 Feb 2025 05:58:12 -0800 Subject: [PATCH 4/6] refactor(logs): remove "Watch Logs" command #6533 ## Problem The vscode Output panel now supports filtering: https://code.visualstudio.com/updates/v1_97#_output-panel-filtering This means we no longer need the `AWS (Developer): Watch Logs` command. ## Solution Remove the `AWS (Developer): Watch Logs` command. --- CONTRIBUTING.md | 19 ++++++++---------- docs/images/debug-console-filter.png | Bin 23432 -> 0 bytes packages/core/src/dev/activation.ts | 14 ------------- packages/core/src/shared/logger/logger.ts | 10 --------- .../core/src/shared/logger/toolkitLogger.ts | 4 ---- packages/core/src/test/testLogger.ts | 2 -- packages/toolkit/package.json | 5 ----- 7 files changed, 8 insertions(+), 46 deletions(-) delete mode 100644 docs/images/debug-console-filter.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 087d6e838ab..af930a3a34d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -368,31 +368,28 @@ The `aws.dev.forceDevMode` setting enables or disables Toolkit "dev mode". Witho ``` tail -F ~/awstoolkit.log ``` -- Use the `AWS (Developer): Watch Logs` command to watch and filter Toolkit logs (including - telemetry) in VSCode. - - Only available if you enabled "dev mode" (`aws.dev.forceDevMode` setting, see above). - - Enter text in the Debug Console filter box to show only log messages with that text.
- VSCode Debug Console +- Use the Output panel to watch and filter Toolkit logs (including telemetry) in VSCode. + - Enter text in the Output panel filter box to show only log messages with that text. #### Enabling Debug Logs How to enable more detailed debug logs in the extensions. If you need to report an issue attach these to give the most detailed information. -1. Open the Command Palette (`cmd/ctrl` + `shift` + `p`), then search for "View Logs". Choose the correct option for the extension you want, eg: `AWS: View Logs` or `Amazon Q: View Logs` - ![](./docs/images/logsView.png) +1. Open the Command Palette (`cmd/ctrl` + `shift` + `p`), then search for "View Logs". Choose either `AWS: View Logs` or `Amazon Q: View Logs`. + - ![](./docs/images/logsView.png) 2. Click the gear icon on the bottom right and select `Debug` - ![](./docs/images/logsSetDebug.png) + - ![](./docs/images/logsSetDebug.png) 3. Click the gear icon again and select `Set As Default`. This will ensure we stay in `Debug` until explicitly changed - ![](./docs/images/logsSetDefault.png) + - ![](./docs/images/logsSetDefault.png) 4. Open the Command Palette again and select `Reload Window`. 5. Now you should see additional `[debug]` prefixed logs in the output. - ![](./docs/images/logsDebugLog.png) + - ![](./docs/images/logsDebugLog.png) ### Telemetry - See [docs/telemetry.md](./docs/telemetry.md) for guidelines on developing telemetry in this project. -- To watch Toolkit telemetry events, use the `AWS (Developer): Watch Logs` command (see [Logging](#logging) above) and enter "telemetry" in the Debug Console filter box. +- To watch Toolkit telemetry events, use the `Amazon Q: View Logs` command (see [Logging](#logging) above) and enter "telemetry" in the filter box. ### Service Endpoints diff --git a/docs/images/debug-console-filter.png b/docs/images/debug-console-filter.png deleted file mode 100644 index 06703ab414935cf5273cace02e65c432588999cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23432 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±t*;W0dGpQCAv?a!^= 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 { .filter((e) => (opts.menuOptions ?? Object.keys(options)).includes(e[0])) .map((e) => e[1]) ) - }), - // "AWS (Developer): Watch Logs" - Commands.register('aws.dev.viewLogs', async () => { - // HACK: Use startDebugging() so we can use the DEBUG CONSOLE (which supports - // user-defined filtering, unlike the OUTPUT panel). - await vscode.debug.startDebugging(undefined, { - name: 'aws-dev-log', - request: 'launch', - type: 'node', // Nonsense, to force the debugger to start. - }) - getLogger().enableDebugConsole() - if (!getLogger().logLevelEnabled('debug')) { - getLogger().setLogLevel('debug') - } }) ) diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index 19be5ac2887..adada634a22 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -35,8 +35,6 @@ export interface Logger { /** Returns true if the given log level is being logged. */ logLevelEnabled(logLevel: LogLevel): boolean getLogById(logID: number, file: vscode.Uri): string | undefined - /** HACK: Enables logging to vscode Debug Console. */ - enableDebugConsole(): void sendToLog( logLevel: 'debug' | 'verbose' | 'info' | 'warn' | 'error', message: string | Error, @@ -74,8 +72,6 @@ export abstract class BaseLogger implements Logger { abstract setLogLevel(logLevel: LogLevel): void abstract logLevelEnabled(logLevel: LogLevel): boolean abstract getLogById(logID: number, file: vscode.Uri): string | undefined - /** HACK: Enables logging to vscode Debug Console. */ - abstract enableDebugConsole(): void } /** @@ -166,7 +162,6 @@ export class NullLogger extends BaseLogger { public getLogById(logID: number, file: vscode.Uri): string | undefined { return undefined } - public enableDebugConsole(): void {} override sendToLog( logLevel: 'error' | 'warn' | 'info' | 'verbose' | 'debug', message: string | Error, @@ -190,7 +185,6 @@ export class ConsoleLogger extends BaseLogger { public getLogById(logID: number, file: vscode.Uri): string | undefined { return undefined } - public enableDebugConsole(): void {} override sendToLog( logLevel: 'error' | 'warn' | 'info' | 'verbose' | 'debug', message: string | Error, @@ -244,10 +238,6 @@ export class TopicLogger extends BaseLogger implements vscode.Disposable { return this.logger.getLogById(logID, file) } - override enableDebugConsole(): void { - this.logger.enableDebugConsole() - } - override sendToLog(level: LogLevel, message: string | Error, ...meta: any[]): number { if (typeof message === 'string') { message = prependTopic(this.topic, message) as string diff --git a/packages/core/src/shared/logger/toolkitLogger.ts b/packages/core/src/shared/logger/toolkitLogger.ts index d525fb7cf20..0fdf58dc939 100644 --- a/packages/core/src/shared/logger/toolkitLogger.ts +++ b/packages/core/src/shared/logger/toolkitLogger.ts @@ -48,10 +48,6 @@ export class ToolkitLogger extends BaseLogger implements vscode.Disposable { }) } - public enableDebugConsole(): void { - this.logToConsole() - } - public setLogLevel(logLevel: LogLevel) { if (this.logger.level === logLevel) { return diff --git a/packages/core/src/test/testLogger.ts b/packages/core/src/test/testLogger.ts index db9d460652c..a2970c37d8b 100644 --- a/packages/core/src/test/testLogger.ts +++ b/packages/core/src/test/testLogger.ts @@ -23,8 +23,6 @@ export class TestLogger extends BaseLogger { super() } - public enableDebugConsole(): void {} - public getLoggedEntries(...logLevels: LogLevel[]): Loggable[] { return this.loggedEntries .filter((loggedEntry) => logLevels.length === 0 || logLevels.includes(loggedEntry.logLevel)) diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 382fee49e2a..b216b0bbd81 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -3891,11 +3891,6 @@ "category": "AWS (Developer)", "enablement": "aws.isDevMode" }, - { - "command": "aws.dev.viewLogs", - "title": "Watch Logs", - "category": "AWS (Developer)" - }, { "command": "aws.openInApplicationComposerDialog", "title": "%AWS.command.applicationComposer.openDialog%", From 20e1f00563023ddbca804459a30eedf9d6cca329 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:58:23 -0500 Subject: [PATCH 5/6] test(toolkit): Add region provider remote test (#6425) ## Problem In the release yesterday we found an issue where fetching remote region data was failing ## Solution Add a new test since its critical to the flow --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../core/src/shared/regions/regionProvider.ts | 39 ++++++++++--------- .../core/src/testInteg/regionProvider.test.ts | 37 ++++++++++++++++++ 2 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 packages/core/src/testInteg/regionProvider.test.ts diff --git a/packages/core/src/shared/regions/regionProvider.ts b/packages/core/src/shared/regions/regionProvider.ts index 00d13942196..426e27d73c5 100644 --- a/packages/core/src/shared/regions/regionProvider.ts +++ b/packages/core/src/shared/regions/regionProvider.ts @@ -162,25 +162,30 @@ export class RegionProvider { remote: () => Endpoints | Promise }): RegionProvider { const instance = new this() + void instance.init(endpointsProvider) + return instance + } - async function load() { - getLogger().info('endpoints: retrieving AWS endpoints data') - instance.loadFromEndpoints(await endpointsProvider.local()) - - try { - instance.loadFromEndpoints(await endpointsProvider.remote()) - } catch (err) { - getLogger().warn( - `endpoints: failed to load from remote source, region data may appear outdated: %s`, - err - ) - } + async init(endpointsProvider: { + local: () => Endpoints | Promise + remote: () => Endpoints | Promise + }) { + getLogger().info('endpoints: retrieving AWS endpoints data') + + try { + this.loadFromEndpoints(await endpointsProvider.local()) + } catch (err) { + getLogger().warn(`endpoints: failed to load from local source: %s`, err) } - load().catch((err) => { - getLogger().error('Failure while loading Endpoints Manifest: %s', err) + try { + this.loadFromEndpoints(await endpointsProvider.remote()) + } catch (err) { + getLogger().warn(`endpoints: failed to load from remote source, region data may appear outdated: %s`, err) + } - return vscode.window.showErrorMessage( + if (this.getRegions().length === 0) { + void vscode.window.showErrorMessage( `${localize( 'AWS.error.endpoint.load.failure', 'The {0} Toolkit was unable to load endpoints data.', @@ -190,9 +195,7 @@ export class RegionProvider { 'Toolkit functionality may be impacted until VS Code is restarted.' )}` ) - }) - - return instance + } } } diff --git a/packages/core/src/testInteg/regionProvider.test.ts b/packages/core/src/testInteg/regionProvider.test.ts new file mode 100644 index 00000000000..a3486c2bac0 --- /dev/null +++ b/packages/core/src/testInteg/regionProvider.test.ts @@ -0,0 +1,37 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { makeEndpointsProvider } from '../extension' +import { RegionProvider } from '../shared/regions/regionProvider' +import globals from '../shared/extensionGlobals' +import path from 'path' +import { TestFolder } from '../test/testUtil' + +describe('Region Provider', async function () { + let tempFolder: TestFolder + + before(async () => { + tempFolder = await TestFolder.create() + }) + + it('resolves from remote', async function () { + /** + * Make sure the local file doesn't resolve to any endpoints. + * That way we can make sure remote contents are fetched + */ + await tempFolder.write('foo.json', '{}') + globals.manifestPaths.endpoints = path.join(tempFolder.path, 'foo.json') + + await assert.doesNotReject(async () => { + const endpointProvider = makeEndpointsProvider() + const regionProvider = new RegionProvider() + await regionProvider.init(endpointProvider) + + // regions loaded from the remote + assert.ok(regionProvider.getRegions().length > 0) + }) + }) +}) From 2c1227164854e1bbc9ff00dd675f5aeab03585ad Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:17:41 -0500 Subject: [PATCH 6/6] test(amazonq): Add inline completion e2e tests (#6486) ## Problem - we have no confidence checks on inline completion e2e flows - we want to use these tests to eventually verify that codewhisperer language server is behaving correctly ## Solution - add e2e tests that focus on sending inputs to the text editor and seeing how it reacts. That way we can re-use these tests with the codewhisperer language server --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../amazonq/test/e2e/inline/inline.test.ts | 179 ++++++++++++++++++ .../core/src/test/codewhisperer/testUtil.ts | 6 +- packages/core/src/test/setupUtil.ts | 2 +- 3 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 packages/amazonq/test/e2e/inline/inline.test.ts diff --git a/packages/amazonq/test/e2e/inline/inline.test.ts b/packages/amazonq/test/e2e/inline/inline.test.ts new file mode 100644 index 00000000000..34463449cd2 --- /dev/null +++ b/packages/amazonq/test/e2e/inline/inline.test.ts @@ -0,0 +1,179 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import assert from 'assert' +import { + assertTelemetry, + closeAllEditors, + getTestWindow, + registerAuthHook, + resetCodeWhispererGlobalVariables, + TestFolder, + toTextEditor, + using, +} from 'aws-core-vscode/test' +import { RecommendationHandler, RecommendationService } from 'aws-core-vscode/codewhisperer' +import { Commands, globals, sleep, waitUntil } from 'aws-core-vscode/shared' +import { loginToIdC } from '../amazonq/utils/setup' + +describe('Amazon Q Inline', async function () { + let tempFolder: string + const waitOptions = { + interval: 500, + timeout: 10000, + retryOnFail: false, + } + + before(async function () { + await using(registerAuthHook('amazonq-test-account'), async () => { + await loginToIdC() + }) + }) + + beforeEach(async function () { + registerAuthHook('amazonq-test-account') + const folder = await TestFolder.create() + tempFolder = folder.path + await closeAllEditors() + await resetCodeWhispererGlobalVariables(false) + }) + + afterEach(async function () { + await closeAllEditors() + }) + + async function setupEditor({ name, contents }: { name?: string; contents?: string } = {}) { + const fileName = name ?? 'test.ts' + const textContents = + contents ?? + `function fib() { + + +}` + await toTextEditor(textContents, fileName, tempFolder, { + selection: new vscode.Range(new vscode.Position(1, 4), new vscode.Position(1, 4)), + }) + } + + async function waitForRecommendations() { + const ok = await waitUntil(async () => RecommendationHandler.instance.isSuggestionVisible(), waitOptions) + if (!ok) { + assert.fail('Suggestions failed to become visible') + } + } + + async function waitForTelemetry() { + const ok = await waitUntil( + async () => + globals.telemetry.logger.query({ + metricName: 'codewhisperer_userTriggerDecision', + }).length > 0, + waitOptions + ) + if (!ok) { + assert.fail('Telemetry failed to be emitted') + } + } + + for (const [name, invokeCompletion] of [ + ['automatic', async () => await vscode.commands.executeCommand('type', { text: '\n' })], + ['manual', async () => Commands.tryExecute('aws.amazonq.invokeInlineCompletion')], + ] as const) { + describe(`${name} invoke`, async function () { + let originalEditorContents: string | undefined + + describe('supported filetypes', () => { + beforeEach(async () => { + await setupEditor() + + /** + * Allow some time between when the editor is opened and when we start typing. + * If we don't do this then the time between the initial editor selection + * and invoking the "type" command is too low, causing completion to never + * activate. AFAICT there isn't anything we can use waitUntil on here. + * + * note: this number is entirely arbitrary + **/ + await sleep(1000) + + await invokeCompletion() + originalEditorContents = vscode.window.activeTextEditor?.document.getText() + + // wait until the ghost text appears + await waitForRecommendations() + }) + + it(`${name} invoke accept`, async function () { + /** + * keep accepting the suggestion until the text contents change + * this is required because we have no access to the inlineSuggest panel + **/ + const suggestionAccepted = await waitUntil(async () => { + // Accept the suggestion + await vscode.commands.executeCommand('editor.action.inlineSuggest.commit') + return vscode.window.activeTextEditor?.document.getText() !== originalEditorContents + }, waitOptions) + + assert.ok(suggestionAccepted, 'Editor contents should have changed') + + await waitForTelemetry() + assertTelemetry('codewhisperer_userTriggerDecision', { + codewhispererSuggestionState: 'Accept', + }) + }) + + it(`${name} invoke reject`, async function () { + // Reject the suggestion + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + + // Contents haven't changed + assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents) + + await waitForTelemetry() + assertTelemetry('codewhisperer_userTriggerDecision', { + codewhispererSuggestionState: 'Reject', + }) + }) + + it(`${name} invoke discard`, async function () { + // Discard the suggestion by moving it back to the original position + const position = new vscode.Position(1, 4) + const editor = vscode.window.activeTextEditor + if (!editor) { + assert.fail('Could not find text editor') + } + editor.selection = new vscode.Selection(position, position) + + // Contents are the same + assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents) + }) + }) + + it(`${name} invoke on unsupported filetype`, async function () { + await setupEditor({ + name: 'test.zig', + contents: `fn doSomething() void { + + }`, + }) + + /** + * Add delay between editor loading and invoking completion + * @see beforeEach in supported filetypes for more information + */ + await sleep(1000) + await invokeCompletion() + + if (name === 'automatic') { + // It should never get triggered since its not a supported file type + assert.deepStrictEqual(RecommendationService.instance.isRunning, false) + } else { + await getTestWindow().waitForMessage('currently not supported by Amazon Q inline suggestions') + } + }) + }) + } +}) diff --git a/packages/core/src/test/codewhisperer/testUtil.ts b/packages/core/src/test/codewhisperer/testUtil.ts index e7e1971a77d..ce2b0e51aa8 100644 --- a/packages/core/src/test/codewhisperer/testUtil.ts +++ b/packages/core/src/test/codewhisperer/testUtil.ts @@ -28,14 +28,16 @@ import * as model from '../../codewhisperer/models/model' import { stub } from '../utilities/stubber' import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports -export async function resetCodeWhispererGlobalVariables() { +export async function resetCodeWhispererGlobalVariables(clearGlobalState: boolean = true) { vsCodeState.isIntelliSenseActive = false vsCodeState.isCodeWhispererEditing = false CodeWhispererCodeCoverageTracker.instances.clear() globals.telemetry.logger.clear() const session = CodeWhispererSessionState.instance.getSession() session.reset() - await globals.globalState.clear() + if (clearGlobalState) { + await globals.globalState.clear() + } await CodeSuggestionsState.instance.setSuggestionsEnabled(true) await RecommendationHandler.instance.clearInlineCompletionStates() } diff --git a/packages/core/src/test/setupUtil.ts b/packages/core/src/test/setupUtil.ts index 538af7daa06..d0a4cd0b594 100644 --- a/packages/core/src/test/setupUtil.ts +++ b/packages/core/src/test/setupUtil.ts @@ -211,7 +211,7 @@ function maskArns(text: string) { */ export function registerAuthHook(secret: string, lambdaId = process.env['AUTH_UTIL_LAMBDA_ARN']) { return getTestWindow().onDidShowMessage((message) => { - if (message.items[0].title.match(new RegExp(proceedToBrowser))) { + if (message.items.length > 0 && message.items[0].title.match(new RegExp(proceedToBrowser))) { if (!lambdaId) { const baseMessage = 'Browser login flow was shown during testing without an authorizer function' if (process.env['AWS_TOOLKIT_AUTOMATION'] === 'local') {