Skip to content

Commit

Permalink
Merge master into feature/emr
Browse files Browse the repository at this point in the history
  • Loading branch information
aws-toolkit-automation authored Nov 12, 2024
2 parents ef9453e + c09b09a commit 65a7e17
Show file tree
Hide file tree
Showing 27 changed files with 698 additions and 142 deletions.
12 changes: 8 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "Amazon Q /dev: Add an action to accept individual files"
}
127 changes: 126 additions & 1 deletion packages/amazonq/test/e2e/amazonq/featureDev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ describe('Amazon Q Feature Dev', function () {
let tab: Messenger

const prompt = 'Add blank.txt file with empty content'
const codegenApproachPrompt = prompt + ' and add a readme that describes the changes'
const codegenApproachPrompt = `${prompt} and add a readme that describes the changes`
const fileLevelAcceptPrompt = `${prompt} and add a license, and a contributing file`
const tooManyRequestsWaitTime = 100000

function waitForButtons(buttons: FollowUpTypes[]) {
Expand Down Expand Up @@ -50,6 +51,14 @@ describe('Amazon Q Feature Dev', function () {
)
}

async function clickActionButton(filePath: string, actionName: string) {
tab.clickFileActionButton(filePath, actionName)
await tab.waitForEvent(() => !tab.hasAction(filePath, actionName), {
waitIntervalInMs: 500,
waitTimeoutInMs: 600000,
})
}

/**
* Wait for the original request to finish.
* If the response has a retry button or encountered a guardrails error, continue retrying
Expand Down Expand Up @@ -216,4 +225,120 @@ describe('Amazon Q Feature Dev', function () {
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])
})
})

describe('file-level accepts', async () => {
beforeEach(async function () {
tab.addChatMessage({ command: '/dev', prompt: fileLevelAcceptPrompt })
await retryIfRequired(
async () => {
await tab.waitForChatFinishesLoading()
},
() => {
tab.addChatMessage({ prompt })
}
)
await retryIfRequired(async () => {
await Promise.any([
waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]),
waitForButtons([FollowUpTypes.Retry]),
])
})
})

describe('fileList', async () => {
it('has both accept-change and reject-change action buttons for file', async () => {
const filePath = tab.getFilePaths()[0]
assert.ok(tab.getActionsByFilePath(filePath).length === 2)
assert.ok(tab.hasAction(filePath, 'accept-change'))
assert.ok(tab.hasAction(filePath, 'reject-change'))
})

it('has only revert-rejection action button for rejected file', async () => {
const filePath = tab.getFilePaths()[0]
await clickActionButton(filePath, 'reject-change')

assert.ok(tab.getActionsByFilePath(filePath).length === 1)
assert.ok(tab.hasAction(filePath, 'revert-rejection'))
})

it('does not have any of the action buttons for accepted file', async () => {
const filePath = tab.getFilePaths()[0]
await clickActionButton(filePath, 'accept-change')

assert.ok(tab.getActionsByFilePath(filePath).length === 0)
})

it('disables all action buttons when new task is clicked', async () => {
tab.clickButton(FollowUpTypes.InsertCode)
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])
tab.clickButton(FollowUpTypes.NewTask)
await waitForText('What new task would you like to work on?')

const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
assert.ok(tab.getActionsByFilePath(filePath).length === 0)
}
})

it('disables all action buttons when close session is clicked', async () => {
tab.clickButton(FollowUpTypes.InsertCode)
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])
tab.clickButton(FollowUpTypes.CloseSession)
await waitForText(
"Okay, I've ended this chat session. You can open a new tab to chat or start another workflow."
)

const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
assert.ok(tab.getActionsByFilePath(filePath).length === 0)
}
})
})

describe('accept button', async () => {
describe('button text', async () => {
it('shows "Accept all changes" when no files are accepted or rejected, and "Accept remaining changes" otherwise', async () => {
let insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept all changes')

const filePath = tab.getFilePaths()[0]
await clickActionButton(filePath, 'reject-change')

insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept remaining changes')

await clickActionButton(filePath, 'revert-rejection')

insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept all changes')

await clickActionButton(filePath, 'accept-change')

insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept remaining changes')
})

it('shows "Continue" when all files are either accepted or rejected, with at least one of them rejected', async () => {
const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
await clickActionButton(filePath, 'reject-change')
}

const insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Continue')
})
})

it('disappears and automatically moves on to the next step when all changes are accepted', async () => {
const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
await clickActionButton(filePath, 'accept-change')
}
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])

assert.ok(tab.hasButton(FollowUpTypes.InsertCode) === false)
assert.ok(tab.hasButton(FollowUpTypes.ProvideFeedbackAndRegenerateCode) === false)
})
})
})
})
58 changes: 58 additions & 0 deletions packages/amazonq/test/e2e/amazonq/framework/messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export class Messenger {
this.mynahUIProps.onFollowUpClicked(this.tabID, lastChatItem?.messageId ?? '', option[0])
}

clickFileActionButton(filePath: string, actionName: string) {
if (!this.mynahUIProps.onFileActionClick) {
assert.fail('onFileActionClick must be defined to use it in the tests')
}

this.mynahUIProps.onFileActionClick(this.tabID, this.getFileListMessageId(), filePath, actionName)
}

findCommand(command: string) {
return this.getCommands()
.map((groups) => groups.commands)
Expand All @@ -78,6 +86,52 @@ export class Messenger {
return this.getStore().promptInputPlaceholder
}

getFollowUpButton(type: FollowUpTypes) {
const followUpButton = this.getChatItems()
.pop()
?.followUp?.options?.find((action) => action.type === type)
if (!followUpButton) {
assert.fail(`Could not find follow up button with type ${type}`)
}
return followUpButton
}

getFileList() {
const chatItems = this.getChatItems()
const fileList = chatItems.find((item) => 'fileList' in item)
if (!fileList) {
assert.fail('Could not find file list')
}
return fileList
}

getFileListMessageId() {
const fileList = this.getFileList()
const messageId = fileList?.messageId
if (!messageId) {
assert.fail('Could not find file list message id')
}
return messageId
}

getFilePaths() {
const fileList = this.getFileList()
const filePaths = fileList?.fileList?.filePaths
if (!filePaths) {
assert.fail('Could not find file paths')
}
if (filePaths.length === 0) {
assert.fail('File paths list is empty')
}
return filePaths
}

getActionsByFilePath(filePath: string) {
const fileList = this.getFileList()
const actions = fileList?.fileList?.actions
return actions?.[filePath] ?? []
}

hasButton(type: FollowUpTypes) {
return (
this.getChatItems()
Expand All @@ -87,6 +141,10 @@ export class Messenger {
)
}

hasAction(filePath: string, actionName: string) {
return this.getActionsByFilePath(filePath).some((action) => action.name === actionName)
}

async waitForChatFinishesLoading() {
return this.waitForEvent(() => this.getStore().loadingChat === false || this.hasButton(FollowUpTypes.Retry))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe('session', () => {
rejected: false,
virtualMemoryUri: uri,
workspaceFolder: controllerSetup.workspaceFolder,
changeApplied: false,
},
{
zipFilePath: 'rejectedFile.js',
Expand All @@ -91,6 +92,7 @@ describe('session', () => {
rejected: true,
virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'rejectedFile.js'),
workspaceFolder: controllerSetup.workspaceFolder,
changeApplied: false,
},
],
[],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@
"@aws-sdk/property-provider": "3.46.0",
"@aws-sdk/smithy-client": "^3.46.0",
"@aws-sdk/util-arn-parser": "^3.46.0",
"@aws/mynah-ui": "^4.15.11",
"@aws/mynah-ui": "^4.18.0",
"@gerhobbelt/gitignore-parser": "^0.2.0-9",
"@iarna/toml": "^2.2.5",
"@smithy/middleware-retry": "^2.3.1",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@
"AWS.amazonq.featureDev.pillText.generatingCode": "Generating code...",
"AWS.amazonq.featureDev.pillText.requestingChanges": "Requesting changes ...",
"AWS.amazonq.featureDev.pillText.insertCode": "Accept code",
"AWS.amazonq.featureDev.pillText.continue": "Continue",
"AWS.amazonq.featureDev.pillText.acceptAllChanges": "Accept all changes",
"AWS.amazonq.featureDev.pillText.acceptRemainingChanges": "Accept remaining changes",
"AWS.amazonq.featureDev.pillText.stoppingCodeGeneration": "Stopping code generation...",
"AWS.amazonq.featureDev.pillText.sendFeedback": "Send feedback",
"AWS.amazonq.featureDev.pillText.selectFiles": "Select files for context",
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/amazonq/commons/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export async function openDiff(leftPath: string, rightPath: string, tabId: strin
}

export async function openDeletedDiff(filePath: string, name: string, tabId: string) {
const fileUri = await getOriginalFileUri(filePath, tabId)
await vscode.commands.executeCommand('vscode.open', fileUri, {}, `${name} (Deleted)`)
const left = await getOriginalFileUri(filePath, tabId)
const right = createAmazonQUri('empty', tabId)
await vscode.commands.executeCommand('vscode.diff', left, right, `${name} (Deleted)`)
}

export async function getOriginalFileUri(fullPath: string, tabId: string) {
Expand All @@ -32,3 +33,11 @@ export function createAmazonQUri(path: string, tabId: string) {
// TODO change the featureDevScheme to a more general amazon q scheme
return vscode.Uri.from({ scheme: featureDevScheme, path, query: `tabID=${tabId}` })
}

export async function openFile(path: string) {
if (!(await fs.exists(path))) {
return
}
const fileUri = vscode.Uri.file(path)
await vscode.commands.executeCommand('vscode.diff', fileUri, fileUri)
}
9 changes: 8 additions & 1 deletion packages/core/src/amazonq/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ export { amazonQHelpUrl } from '../shared/constants'
export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/statusBarMenu'
export { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands'
export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens'
export { createAmazonQUri, openDiff, openDeletedDiff, getOriginalFileUri, getFileDiffUris } from './commons/diff'
export {
createAmazonQUri,
openDiff,
openDeletedDiff,
getOriginalFileUri,
getFileDiffUris,
openFile,
} from './commons/diff'
export { CodeReference } from '../codewhispererChat/view/connector/connector'
export { AuthMessageDataMap, AuthFollowUpType } from './auth/model'
export { extractAuthFollowUp } from './util/authUtils'
Expand Down
Loading

0 comments on commit 65a7e17

Please sign in to comment.