Skip to content

Commit 2c12271

Browse files
authored
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.
1 parent 20e1f00 commit 2c12271

File tree

3 files changed

+184
-3
lines changed

3 files changed

+184
-3
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import assert from 'assert'
8+
import {
9+
assertTelemetry,
10+
closeAllEditors,
11+
getTestWindow,
12+
registerAuthHook,
13+
resetCodeWhispererGlobalVariables,
14+
TestFolder,
15+
toTextEditor,
16+
using,
17+
} from 'aws-core-vscode/test'
18+
import { RecommendationHandler, RecommendationService } from 'aws-core-vscode/codewhisperer'
19+
import { Commands, globals, sleep, waitUntil } from 'aws-core-vscode/shared'
20+
import { loginToIdC } from '../amazonq/utils/setup'
21+
22+
describe('Amazon Q Inline', async function () {
23+
let tempFolder: string
24+
const waitOptions = {
25+
interval: 500,
26+
timeout: 10000,
27+
retryOnFail: false,
28+
}
29+
30+
before(async function () {
31+
await using(registerAuthHook('amazonq-test-account'), async () => {
32+
await loginToIdC()
33+
})
34+
})
35+
36+
beforeEach(async function () {
37+
registerAuthHook('amazonq-test-account')
38+
const folder = await TestFolder.create()
39+
tempFolder = folder.path
40+
await closeAllEditors()
41+
await resetCodeWhispererGlobalVariables(false)
42+
})
43+
44+
afterEach(async function () {
45+
await closeAllEditors()
46+
})
47+
48+
async function setupEditor({ name, contents }: { name?: string; contents?: string } = {}) {
49+
const fileName = name ?? 'test.ts'
50+
const textContents =
51+
contents ??
52+
`function fib() {
53+
54+
55+
}`
56+
await toTextEditor(textContents, fileName, tempFolder, {
57+
selection: new vscode.Range(new vscode.Position(1, 4), new vscode.Position(1, 4)),
58+
})
59+
}
60+
61+
async function waitForRecommendations() {
62+
const ok = await waitUntil(async () => RecommendationHandler.instance.isSuggestionVisible(), waitOptions)
63+
if (!ok) {
64+
assert.fail('Suggestions failed to become visible')
65+
}
66+
}
67+
68+
async function waitForTelemetry() {
69+
const ok = await waitUntil(
70+
async () =>
71+
globals.telemetry.logger.query({
72+
metricName: 'codewhisperer_userTriggerDecision',
73+
}).length > 0,
74+
waitOptions
75+
)
76+
if (!ok) {
77+
assert.fail('Telemetry failed to be emitted')
78+
}
79+
}
80+
81+
for (const [name, invokeCompletion] of [
82+
['automatic', async () => await vscode.commands.executeCommand('type', { text: '\n' })],
83+
['manual', async () => Commands.tryExecute('aws.amazonq.invokeInlineCompletion')],
84+
] as const) {
85+
describe(`${name} invoke`, async function () {
86+
let originalEditorContents: string | undefined
87+
88+
describe('supported filetypes', () => {
89+
beforeEach(async () => {
90+
await setupEditor()
91+
92+
/**
93+
* Allow some time between when the editor is opened and when we start typing.
94+
* If we don't do this then the time between the initial editor selection
95+
* and invoking the "type" command is too low, causing completion to never
96+
* activate. AFAICT there isn't anything we can use waitUntil on here.
97+
*
98+
* note: this number is entirely arbitrary
99+
**/
100+
await sleep(1000)
101+
102+
await invokeCompletion()
103+
originalEditorContents = vscode.window.activeTextEditor?.document.getText()
104+
105+
// wait until the ghost text appears
106+
await waitForRecommendations()
107+
})
108+
109+
it(`${name} invoke accept`, async function () {
110+
/**
111+
* keep accepting the suggestion until the text contents change
112+
* this is required because we have no access to the inlineSuggest panel
113+
**/
114+
const suggestionAccepted = await waitUntil(async () => {
115+
// Accept the suggestion
116+
await vscode.commands.executeCommand('editor.action.inlineSuggest.commit')
117+
return vscode.window.activeTextEditor?.document.getText() !== originalEditorContents
118+
}, waitOptions)
119+
120+
assert.ok(suggestionAccepted, 'Editor contents should have changed')
121+
122+
await waitForTelemetry()
123+
assertTelemetry('codewhisperer_userTriggerDecision', {
124+
codewhispererSuggestionState: 'Accept',
125+
})
126+
})
127+
128+
it(`${name} invoke reject`, async function () {
129+
// Reject the suggestion
130+
await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion')
131+
132+
// Contents haven't changed
133+
assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents)
134+
135+
await waitForTelemetry()
136+
assertTelemetry('codewhisperer_userTriggerDecision', {
137+
codewhispererSuggestionState: 'Reject',
138+
})
139+
})
140+
141+
it(`${name} invoke discard`, async function () {
142+
// Discard the suggestion by moving it back to the original position
143+
const position = new vscode.Position(1, 4)
144+
const editor = vscode.window.activeTextEditor
145+
if (!editor) {
146+
assert.fail('Could not find text editor')
147+
}
148+
editor.selection = new vscode.Selection(position, position)
149+
150+
// Contents are the same
151+
assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents)
152+
})
153+
})
154+
155+
it(`${name} invoke on unsupported filetype`, async function () {
156+
await setupEditor({
157+
name: 'test.zig',
158+
contents: `fn doSomething() void {
159+
160+
}`,
161+
})
162+
163+
/**
164+
* Add delay between editor loading and invoking completion
165+
* @see beforeEach in supported filetypes for more information
166+
*/
167+
await sleep(1000)
168+
await invokeCompletion()
169+
170+
if (name === 'automatic') {
171+
// It should never get triggered since its not a supported file type
172+
assert.deepStrictEqual(RecommendationService.instance.isRunning, false)
173+
} else {
174+
await getTestWindow().waitForMessage('currently not supported by Amazon Q inline suggestions')
175+
}
176+
})
177+
})
178+
}
179+
})

packages/core/src/test/codewhisperer/testUtil.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ import * as model from '../../codewhisperer/models/model'
2828
import { stub } from '../utilities/stubber'
2929
import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports
3030

31-
export async function resetCodeWhispererGlobalVariables() {
31+
export async function resetCodeWhispererGlobalVariables(clearGlobalState: boolean = true) {
3232
vsCodeState.isIntelliSenseActive = false
3333
vsCodeState.isCodeWhispererEditing = false
3434
CodeWhispererCodeCoverageTracker.instances.clear()
3535
globals.telemetry.logger.clear()
3636
const session = CodeWhispererSessionState.instance.getSession()
3737
session.reset()
38-
await globals.globalState.clear()
38+
if (clearGlobalState) {
39+
await globals.globalState.clear()
40+
}
3941
await CodeSuggestionsState.instance.setSuggestionsEnabled(true)
4042
await RecommendationHandler.instance.clearInlineCompletionStates()
4143
}

packages/core/src/test/setupUtil.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ function maskArns(text: string) {
211211
*/
212212
export function registerAuthHook(secret: string, lambdaId = process.env['AUTH_UTIL_LAMBDA_ARN']) {
213213
return getTestWindow().onDidShowMessage((message) => {
214-
if (message.items[0].title.match(new RegExp(proceedToBrowser))) {
214+
if (message.items.length > 0 && message.items[0].title.match(new RegExp(proceedToBrowser))) {
215215
if (!lambdaId) {
216216
const baseMessage = 'Browser login flow was shown during testing without an authorizer function'
217217
if (process.env['AWS_TOOLKIT_AUTOMATION'] === 'local') {

0 commit comments

Comments
 (0)