diff --git a/packages/amazonq/.changes/next-release/Feature-9e413673-5ef6-4920-97b1-e73635f3a0f5.json b/packages/amazonq/.changes/next-release/Feature-9e413673-5ef6-4920-97b1-e73635f3a0f5.json new file mode 100644 index 00000000000..af699a24355 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-9e413673-5ef6-4920-97b1-e73635f3a0f5.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Explain and Fix for any issue in Code Issues panel will pull the experience into chat. Also no more view details tab." +} diff --git a/packages/amazonq/.changes/next-release/Feature-a0140eaf-abe8-43ae-9ea1-f0b1afcbc962.json b/packages/amazonq/.changes/next-release/Feature-a0140eaf-abe8-43ae-9ea1-f0b1afcbc962.json new file mode 100644 index 00000000000..1048a3e14c0 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-a0140eaf-abe8-43ae-9ea1-f0b1afcbc962.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "QCodeReview tool will update CodeIssues panel along with quick action - `/review`" +} diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index a37cddae124..e6b1dccd95c 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -442,17 +442,22 @@ }, { "command": "aws.amazonq.openSecurityIssuePanel", + "when": "false && view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", + "group": "inline@4" + }, + { + "command": "aws.amazonq.security.explain", "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@4" }, { - "command": "aws.amazonq.security.ignore", + "command": "aws.amazonq.security.generateFix", "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@5" }, { - "command": "aws.amazonq.security.generateFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithoutFix", + "command": "aws.amazonq.security.ignore", + "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@6" }, { @@ -535,16 +540,17 @@ "aws.amazonq.submenu.securityIssueMoreActions": [ { "command": "aws.amazonq.security.explain", + "when": "false", "group": "1_more@1" }, { "command": "aws.amazonq.applySecurityFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", + "when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", "group": "1_more@3" }, { "command": "aws.amazonq.security.regenerateFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", + "when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", "group": "1_more@4" }, { @@ -793,6 +799,7 @@ { "command": "aws.amazonq.security.explain", "title": "%AWS.command.amazonq.explainIssue%", + "icon": "$(search)", "enablement": "view == aws.amazonq.SecurityIssuesTree" }, { diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts index e95eae58d1b..83e70b7bae3 100644 --- a/packages/amazonq/src/lsp/chat/commands.ts +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -7,7 +7,9 @@ import { Commands, globals } from 'aws-core-vscode/shared' import { window } from 'vscode' import { AmazonQChatViewProvider } from './webviewProvider' import { CodeScanIssue } from 'aws-core-vscode/codewhisperer' -import { EditorContextExtractor } from 'aws-core-vscode/codewhispererChat' +import { getLogger } from 'aws-core-vscode/shared' +import * as vscode from 'vscode' +import * as path from 'path' /** * TODO: Re-enable these once we can figure out which path they're going to live in @@ -21,45 +23,24 @@ export function registerCommands(provider: AmazonQChatViewProvider) { registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', provider), registerGenericCommand('aws.amazonq.generateUnitTests', 'Generate Tests', provider), - Commands.register('aws.amazonq.explainIssue', async (issue: CodeScanIssue) => { - void focusAmazonQPanel().then(async () => { - const editorContextExtractor = new EditorContextExtractor() - const extractedContext = await editorContextExtractor.extractContextForTrigger('ContextMenu') - const selectedCode = - extractedContext?.activeFileContext?.fileText - ?.split('\n') - .slice(issue.startLine, issue.endLine) - .join('\n') ?? '' - - // The message that gets sent to the UI - const uiMessage = [ - 'Explain the ', - issue.title, - ' issue in the following code:', - '\n```\n', - selectedCode, - '\n```', - ].join('') - - // The message that gets sent to the backend - const contextMessage = `Explain the issue "${issue.title}" (${JSON.stringify( - issue - )}) and generate code demonstrating the fix` - - void provider.webview?.postMessage({ - command: 'sendToPrompt', - params: { - selection: '', - triggerType: 'contextMenu', - prompt: { - prompt: uiMessage, // what gets sent to the user - escapedPrompt: contextMessage, // what gets sent to the backend - }, - autoSubmit: true, - }, - }) - }) - }), + Commands.register('aws.amazonq.explainIssue', (issue: CodeScanIssue, filePath: string) => + handleIssueCommand( + issue, + filePath, + 'Explain', + 'Provide a small description of the issue. You must not attempt to fix the issue. You should only give a small summary of it to the user.', + provider + ) + ), + Commands.register('aws.amazonq.generateFix', (issue: CodeScanIssue, filePath: string) => + handleIssueCommand( + issue, + filePath, + 'Fix', + 'Generate a fix for the following code issue. You must not explain the issue, just generate and explain the fix. The user should have the option to accept or reject the fix before any code is changed.', + provider + ) + ), Commands.register('aws.amazonq.sendToPrompt', (data) => { const triggerType = getCommandTriggerType(data) const selection = getSelectedText() @@ -85,6 +66,58 @@ export function registerCommands(provider: AmazonQChatViewProvider) { ) } +async function handleIssueCommand( + issue: CodeScanIssue, + filePath: string, + action: string, + contextPrompt: string, + provider: AmazonQChatViewProvider +) { + await focusAmazonQPanel() + + if (issue && filePath) { + await openFileWithSelection(issue, filePath) + } + + const lineRange = createLineRangeText(issue) + const visibleMessageInChat = `_${action} **${issue.title}** issue in **${path.basename(filePath)}** at \`${lineRange}\`_` + const contextMessage = `${contextPrompt} Code issue - ${JSON.stringify(issue)}` + + void provider.webview?.postMessage({ + command: 'sendToPrompt', + params: { + selection: '', + triggerType: 'contextMenu', + prompt: { + prompt: visibleMessageInChat, + escapedPrompt: contextMessage, + }, + autoSubmit: true, + }, + }) +} + +async function openFileWithSelection(issue: CodeScanIssue, filePath: string) { + try { + const range = new vscode.Range(issue.startLine, 0, issue.endLine, 0) + const doc = await vscode.workspace.openTextDocument(filePath) + await vscode.window.showTextDocument(doc, { + selection: range, + viewColumn: vscode.ViewColumn.One, + preview: true, + }) + } catch (e) { + getLogger().error('openFileWithSelection: Failed to open file %s with selection: %O', filePath, e) + void vscode.window.showInformationMessage('Failed to display file with issue.') + } +} + +function createLineRangeText(issue: CodeScanIssue): string { + return issue.startLine === issue.endLine - 1 + ? `[${issue.startLine + 1}]` + : `[${issue.startLine + 1}, ${issue.endLine}]` +} + function getSelectedText(): string { const editor = window.activeTextEditor if (editor) { diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 918abb46f40..8496f996123 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -71,7 +71,16 @@ import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' -import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' +import { + AggregatedCodeScanIssue, + AuthUtil, + CodeAnalysisScope, + CodeWhispererSettings, + initSecurityScanRender, + ReferenceLogViewProvider, + SecurityIssueTreeViewProvider, + CodeWhispererConstants, +} from 'aws-core-vscode/codewhisperer' import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl, isTextEditor } from 'aws-core-vscode/shared' import { DefaultAmazonQAppInitContext, @@ -85,6 +94,7 @@ import { isValidResponseError } from './error' import { decryptResponse, encryptRequest } from '../encryption' import { getCursorState } from '../utils' import { focusAmazonQPanel } from './commands' +import { ChatMessage } from '@aws/language-server-runtimes/server-interface' export function registerActiveEditorChangeListener(languageClient: LanguageClient) { let debounceTimer: NodeJS.Timeout | undefined @@ -299,7 +309,8 @@ export function registerMessageListeners( encryptionKey, provider, chatParams.tabId, - chatDisposable + chatDisposable, + languageClient ) } catch (e) { const errorMsg = `Error occurred during chat request: ${e}` @@ -315,7 +326,8 @@ export function registerMessageListeners( encryptionKey, provider, chatParams.tabId, - chatDisposable + chatDisposable, + languageClient ) } finally { chatStreamTokens.delete(chatParams.tabId) @@ -359,7 +371,8 @@ export function registerMessageListeners( encryptionKey, provider, message.params.tabId, - quickActionDisposable + quickActionDisposable, + languageClient ) break } @@ -612,6 +625,12 @@ async function handlePartialResult( ) { const decryptedMessage = await decryptResponse(partialResult, encryptionKey) + // This is to filter out the message containing findings from qCodeReview tool to update CodeIssues panel + decryptedMessage.additionalMessages = decryptedMessage.additionalMessages?.filter( + (message) => + !(message.messageId !== undefined && message.messageId.endsWith(CodeWhispererConstants.findingsSuffix)) + ) + if (decryptedMessage.body !== undefined) { void provider.webview?.postMessage({ command: chatRequestType.method, @@ -632,10 +651,13 @@ async function handleCompleteResult( encryptionKey: Buffer | undefined, provider: AmazonQChatViewProvider, tabId: string, - disposable: Disposable + disposable: Disposable, + languageClient: LanguageClient ) { const decryptedMessage = await decryptResponse(result, encryptionKey) + handleSecurityFindings(decryptedMessage, languageClient) + void provider.webview?.postMessage({ command: chatRequestType.method, params: decryptedMessage, @@ -649,6 +671,37 @@ async function handleCompleteResult( disposable.dispose() } +function handleSecurityFindings( + decryptedMessage: { additionalMessages?: ChatMessage[] }, + languageClient: LanguageClient +): void { + if (decryptedMessage.additionalMessages === undefined || decryptedMessage.additionalMessages.length === 0) { + return + } + for (let i = decryptedMessage.additionalMessages.length - 1; i >= 0; i--) { + const message = decryptedMessage.additionalMessages[i] + if (message.messageId !== undefined && message.messageId.endsWith(CodeWhispererConstants.findingsSuffix)) { + if (message.body !== undefined) { + try { + const aggregatedCodeScanIssues: AggregatedCodeScanIssue[] = JSON.parse(message.body) + for (const aggregatedCodeScanIssue of aggregatedCodeScanIssues) { + for (const issue of aggregatedCodeScanIssue.issues) { + issue.visible = !CodeWhispererSettings.instance + .getIgnoredSecurityIssues() + .includes(issue.title) + } + } + initSecurityScanRender(aggregatedCodeScanIssues, undefined, CodeAnalysisScope.PROJECT) + SecurityIssueTreeViewProvider.focus() + } catch (e) { + languageClient.info('Failed to parse findings') + } + } + decryptedMessage.additionalMessages.splice(i, 1) + } + } +} + async function resolveChatResponse( requestMethod: string, params: any, diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 46557aa619e..6b0b9d62536 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -169,6 +169,7 @@ export async function startLanguageServer( reroute: true, modelSelection: true, workspaceFilePath: vscode.workspace.workspaceFile?.fsPath, + qCodeReviewInChat: true, }, window: { notifications: true, diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts index 956c3b43d73..9c1bb751a35 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts @@ -21,6 +21,25 @@ describe('securityIssueHoverProvider', () => { token = new vscode.CancellationTokenSource() }) + function buildCommandLink(command: string, args: any[], label: string, tooltip: string): string { + return `[$(${command.includes('ignore') ? 'error' : 'comment'}) ${label}](command:${command}?${encodeURIComponent(JSON.stringify(args))} '${tooltip}')` + } + + function buildExpectedContent(issue: any, fileName: string, description: string, severity?: string): string { + const severityBadge = severity ? ` ![${severity}](severity-${severity.toLowerCase()}.svg)` : ' ' + const commands = [ + buildCommandLink('aws.amazonq.explainIssue', [issue, fileName], 'Explain', 'Explain with Amazon Q'), + buildCommandLink('aws.amazonq.generateFix', [issue, fileName], 'Fix', 'Fix with Amazon Q'), + buildCommandLink('aws.amazonq.security.ignore', [issue, fileName, 'hover'], 'Ignore', 'Ignore Issue'), + buildCommandLink('aws.amazonq.security.ignoreAll', [issue, 'hover'], 'Ignore All', 'Ignore Similar Issues'), + ] + return `## title${severityBadge}\n${description}\n\n${commands.join('\n | ')}\n` + } + + function setupIssues(issues: any[]): void { + securityIssueProvider.issues = [{ filePath: mockDocument.fileName, issues }] + } + it('should return hover for each issue for the current position', () => { const issues = [ createCodeScanIssue({ findingId: 'finding-1', detectorId: 'language/detector-1', ruleId: 'Rule-123' }), @@ -32,82 +51,17 @@ describe('securityIssueHoverProvider', () => { }), ] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] - + setupIssues(issues) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 2) assert.strictEqual( (actual.contents[0] as vscode.MarkdownString).value, - '## title ![High](severity-high.svg)\n' + - 'fix\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[0]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[0], 'hover']) - )} 'Ignore Similar Issues')\n` + - ` | [$(wrench) Fix](command:aws.amazonq.applySecurityFix?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Fix with Amazon Q')\n` + - '### Suggested Fix Preview\n\n' + - '\n\n' + - '```undefined\n' + - '@@ -1,1 +1,1 @@ \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'first line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '-second line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '+third line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'fourth line \n' + - '```\n\n' + - '\n\n' + buildExpectedContent(issues[0], mockDocument.fileName, 'fix', 'High') ) assert.strictEqual( (actual.contents[1] as vscode.MarkdownString).value, - '## title ![High](severity-high.svg)\n' + - 'recommendationText\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[1], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[1]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[1], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[1], 'hover']) - )} 'Ignore Similar Issues')\n` + buildExpectedContent(issues[1], mockDocument.fileName, 'recommendationText', 'High') ) assertTelemetry('codewhisperer_codeScanIssueHover', [ { findingId: 'finding-1', detectorId: 'language/detector-1', ruleId: 'Rule-123', includesFix: true }, @@ -116,27 +70,15 @@ describe('securityIssueHoverProvider', () => { }) it('should return empty contents if there is no issue on the current position', () => { - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues: [createCodeScanIssue()], - }, - ] - + setupIssues([createCodeScanIssue()]) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(2, 0), token.token) assert.strictEqual(actual.contents.length, 0) }) it('should skip issues not in the current file', () => { securityIssueProvider.issues = [ - { - filePath: 'some/path', - issues: [createCodeScanIssue()], - }, - { - filePath: mockDocument.fileName, - issues: [createCodeScanIssue()], - }, + { filePath: 'some/path', issues: [createCodeScanIssue()] }, + { filePath: mockDocument.fileName, issues: [createCodeScanIssue()] }, ] const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 1) @@ -144,30 +86,12 @@ describe('securityIssueHoverProvider', () => { it('should not show severity badge if undefined', () => { const issues = [createCodeScanIssue({ severity: undefined, suggestedFixes: [] })] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] + setupIssues(issues) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 1) assert.strictEqual( (actual.contents[0] as vscode.MarkdownString).value, - '## title \n' + - 'recommendationText\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[0]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[0], 'hover']) - )} 'Ignore Similar Issues')\n` + buildExpectedContent(issues[0], mockDocument.fileName, 'recommendationText') ) }) @@ -182,75 +106,17 @@ describe('securityIssueHoverProvider', () => { ], }), ] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] + setupIssues(issues) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 1) assert.strictEqual( (actual.contents[0] as vscode.MarkdownString).value, - '## title ![High](severity-high.svg)\n' + - 'fix\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[0]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[0], 'hover']) - )} 'Ignore Similar Issues')\n` + - ` | [$(wrench) Fix](command:aws.amazonq.applySecurityFix?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Fix with Amazon Q')\n` + - '### Suggested Fix Preview\n\n' + - '\n\n' + - '```undefined\n' + - '@@ -1,1 +1,1 @@ \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'first line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '-second line \n' + - '-third line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '+fourth line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'fifth line \n' + - '```\n\n' + - '\n\n' + buildExpectedContent(issues[0], mockDocument.fileName, 'fix', 'High') ) }) it('should not show issues that are not visible', () => { - const issues = [createCodeScanIssue({ visible: false })] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] + setupIssues([createCodeScanIssue({ visible: false })]) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 0) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts index d72e1f8636f..6a74be85118 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts @@ -150,7 +150,7 @@ describe('SecurityIssueTreeViewProvider', function () { item.iconPath?.toString().includes(`${item.issue.severity.toLowerCase()}.svg`) ) ) - assert.ok(issueItems.every((item) => item.description?.toString().startsWith('[Ln '))) + assert.ok(issueItems.every((item) => !item.description?.toString().startsWith('[Ln '))) } }) }) diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index b3cf958c980..c8c091a609e 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -146,7 +146,7 @@ "AWS.command.amazonq.generateUnitTests": "Generate Tests", "AWS.command.amazonq.security.scan": "Run Project Review", "AWS.command.amazonq.security.fileScan": "Run File Review", - "AWS.command.amazonq.generateFix": "Generate Fix", + "AWS.command.amazonq.generateFix": "Fix", "AWS.command.amazonq.viewDetails": "View Details", "AWS.command.amazonq.explainIssue": "Explain", "AWS.command.amazonq.ignoreIssue": "Ignore Issue", diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index f2b67c49593..745fe1a45a9 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -12,7 +12,6 @@ import { DefaultCodeWhispererClient } from '../client/codewhisperer' import { confirmStopSecurityScan, startSecurityScan } from './startSecurityScan' import { SecurityPanelViewProvider } from '../views/securityPanelViewProvider' import { - codeFixState, CodeScanIssue, CodeScansState, codeScanState, @@ -50,7 +49,7 @@ import { once } from '../../shared/utilities/functionUtils' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { removeDiagnostic } from '../service/diagnosticsProvider' import { SsoAccessTokenProvider } from '../../auth/sso/ssoAccessTokenProvider' -import { ToolkitError, getErrorMsg, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' +import { ToolkitError, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' import { isRemoteWorkspace } from '../../shared/vscode/env' import { isBuilderIdConnection } from '../../auth/connection' import globals from '../../shared/extensionGlobals' @@ -61,7 +60,6 @@ import { SecurityIssueProvider } from '../service/securityIssueProvider' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { closeDiff, getPatchedCode } from '../../shared/utilities/diffUtils' import { insertCommentAboveLine } from '../../shared/utilities/commentUtils' -import { startCodeFixGeneration } from './startCodeFixGeneration' import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext' import path from 'path' import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' @@ -368,10 +366,6 @@ export const openSecurityIssuePanel = Commands.declare( const targetIssue: CodeScanIssue = issue instanceof IssueItem ? issue.issue : issue const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath await showSecurityIssueWebview(context.extensionContext, targetIssue, targetFilePath) - - if (targetIssue.suggestedFixes.length === 0) { - await generateFix.execute(targetIssue, targetFilePath, 'webview', true, false) - } telemetry.codewhisperer_codeScanIssueViewDetails.emit({ findingId: targetIssue.findingId, detectorId: targetIssue.detectorId, @@ -685,116 +679,13 @@ export const generateFix = Commands.declare( { id: 'aws.amazonq.security.generateFix' }, (client: DefaultCodeWhispererClient, context: ExtContext) => async ( - issue: CodeScanIssue | IssueItem | undefined, + issueItem: IssueItem, filePath: string, source: Component, refresh: boolean = false, shouldOpenSecurityIssuePanel: boolean = true ) => { - const targetIssue: CodeScanIssue | undefined = issue instanceof IssueItem ? issue.issue : issue - const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath - const targetSource: Component = issue instanceof IssueItem ? 'tree' : source - if (!targetIssue) { - return - } - if (targetIssue.ruleId === CodeWhispererConstants.sasRuleId) { - getLogger().warn('GenerateFix is not available for SAS findings.') - return - } - await telemetry.codewhisperer_codeScanIssueGenerateFix.run(async () => { - try { - if (shouldOpenSecurityIssuePanel) { - await vscode.commands - .executeCommand('aws.amazonq.openSecurityIssuePanel', targetIssue, targetFilePath) - .then(undefined, (e) => { - getLogger().error('Failed to open security issue panel: %s', e.message) - }) - } - await updateSecurityIssueWebview({ - isGenerateFixLoading: true, - // eslint-disable-next-line unicorn/no-null - generateFixError: null, - context: context.extensionContext, - filePath: targetFilePath, - shouldRefreshView: false, - }) - - codeFixState.setToRunning() - let hasSuggestedFix = false - const { suggestedFix, jobId } = await startCodeFixGeneration( - client, - targetIssue, - targetFilePath, - targetIssue.findingId - ) - // redact the fix if the user disabled references and there is a reference - if ( - // TODO: enable references later for scans - // !CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled() && - suggestedFix?.references && - suggestedFix?.references?.length > 0 - ) { - getLogger().debug( - `Received fix with reference and user settings disallow references. Job ID: ${jobId}` - ) - // TODO: re-enable notifications once references published - // void vscode.window.showInformationMessage( - // 'Your settings do not allow code generation with references.' - // ) - hasSuggestedFix = false - } else { - hasSuggestedFix = suggestedFix !== undefined - } - telemetry.record({ includesFix: hasSuggestedFix }) - const updatedIssue: CodeScanIssue = { - ...targetIssue, - fixJobId: jobId, - suggestedFixes: - hasSuggestedFix && suggestedFix - ? [ - { - code: suggestedFix.codeDiff, - description: suggestedFix.description ?? '', - references: suggestedFix.references, - }, - ] - : [], - } - await updateSecurityIssueWebview({ - issue: updatedIssue, - isGenerateFixLoading: false, - filePath: targetFilePath, - context: context.extensionContext, - shouldRefreshView: true, - }) - - SecurityIssueProvider.instance.updateIssue(updatedIssue, targetFilePath) - SecurityIssueTreeViewProvider.instance.refresh() - } catch (err) { - const error = err instanceof Error ? err : new TypeError('Unexpected error') - await updateSecurityIssueWebview({ - issue: targetIssue, - isGenerateFixLoading: false, - generateFixError: getErrorMsg(error, true), - filePath: targetFilePath, - context: context.extensionContext, - shouldRefreshView: false, - }) - SecurityIssueProvider.instance.updateIssue(targetIssue, targetFilePath) - SecurityIssueTreeViewProvider.instance.refresh() - throw err - } finally { - telemetry.record({ - component: targetSource, - detectorId: targetIssue.detectorId, - findingId: targetIssue.findingId, - ruleId: targetIssue.ruleId, - variant: refresh ? 'refresh' : undefined, - autoDetected: targetIssue.autoDetected, - codewhispererCodeScanJobId: targetIssue.scanJobId, - }) - } - }) + await vscode.commands.executeCommand('aws.amazonq.generateFix', issueItem.issue, issueItem.filePath) } ) @@ -824,19 +715,13 @@ export const rejectFix = Commands.declare( export const regenerateFix = Commands.declare( { id: 'aws.amazonq.security.regenerateFix' }, - () => async (issue: CodeScanIssue | IssueItem | undefined, filePath: string, source: Component) => { - const targetIssue: CodeScanIssue | undefined = issue instanceof IssueItem ? issue.issue : issue - const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath - const targetSource: Component = issue instanceof IssueItem ? 'tree' : source - const updatedIssue = await rejectFix.execute(targetIssue, targetFilePath) - await generateFix.execute(updatedIssue, targetFilePath, targetSource, true) - } + () => async (issue: CodeScanIssue | IssueItem | undefined, filePath: string, source: Component) => {} ) export const explainIssue = Commands.declare( { id: 'aws.amazonq.security.explain' }, () => async (issueItem: IssueItem) => { - await vscode.commands.executeCommand('aws.amazonq.explainIssue', issueItem.issue) + await vscode.commands.executeCommand('aws.amazonq.explainIssue', issueItem.issue, issueItem.filePath) } ) diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index d04fe6effc3..5ff6d13bd91 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -401,7 +401,7 @@ export function showSecurityScanResults( zipMetadata: ZipMetadata, totalIssues: number ) { - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + initSecurityScanRender(securityRecommendationCollection, editor, scope) if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) { populateCodeScanLogStream(zipMetadata.scannedFiles) @@ -439,7 +439,7 @@ export function showScanResultsInChat( break } - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + initSecurityScanRender(securityRecommendationCollection, editor, scope) if (totalIssues > 0) { SecurityIssueTreeViewProvider.focus() } diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 2a095de2f3e..eca88915e3f 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -903,3 +903,5 @@ export const predictionTrackerDefaultConfig = { maxAgeMs: 30000, maxSupplementalContext: 15, } + +export const findingsSuffix = '_qCodeReviewFindings' diff --git a/packages/core/src/codewhisperer/service/diagnosticsProvider.ts b/packages/core/src/codewhisperer/service/diagnosticsProvider.ts index 72407cd80f5..f181bdb146d 100644 --- a/packages/core/src/codewhisperer/service/diagnosticsProvider.ts +++ b/packages/core/src/codewhisperer/service/diagnosticsProvider.ts @@ -25,7 +25,6 @@ export const securityScanRender: SecurityScanRender = { export function initSecurityScanRender( securityRecommendationList: AggregatedCodeScanIssue[], - context: vscode.ExtensionContext, editor: vscode.TextEditor | undefined, scope: CodeAnalysisScope ) { diff --git a/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts b/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts index f1d01494d54..4dc7bceebe7 100644 --- a/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts @@ -66,7 +66,7 @@ export class SecurityIssueCodeActionProvider implements vscode.CodeActionProvide `Amazon Q: Explain "${issue.title}"`, vscode.CodeActionKind.QuickFix ) - const explainWithQArgs = [issue] + const explainWithQArgs = [issue, group.filePath] explainWithQ.command = { title: 'Explain with Amazon Q', command: 'aws.amazonq.explainIssue', diff --git a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts index b82c10063e6..c907f99abe3 100644 --- a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts @@ -10,7 +10,6 @@ import path from 'path' import { AuthUtil } from '../util/authUtil' import { TelemetryHelper } from '../util/telemetryHelper' import { SecurityIssueProvider } from './securityIssueProvider' -import { amazonqCodeIssueDetailsTabTitle } from '../models/constants' export class SecurityIssueHoverProvider implements vscode.HoverProvider { static #instance: SecurityIssueHoverProvider @@ -79,23 +78,23 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { `${suggestedFix?.code && suggestedFix.description !== '' ? suggestedFix.description : issue.recommendation.text}\n\n` ) - const viewDetailsCommand = this._getCommandMarkdown( - 'aws.amazonq.openSecurityIssuePanel', - [issue, filePath], - 'eye', - 'View Details', - `Open "${amazonqCodeIssueDetailsTabTitle}"` - ) - markdownString.appendMarkdown(viewDetailsCommand) - const explainWithQCommand = this._getCommandMarkdown( 'aws.amazonq.explainIssue', - [issue], + [issue, filePath], 'comment', 'Explain', 'Explain with Amazon Q' ) - markdownString.appendMarkdown(' | ' + explainWithQCommand) + markdownString.appendMarkdown(explainWithQCommand) + + const generateFixCommand = this._getCommandMarkdown( + 'aws.amazonq.generateFix', + [issue, filePath], + 'comment', + 'Fix', + 'Fix with Amazon Q' + ) + markdownString.appendMarkdown(' | ' + generateFixCommand) const ignoreIssueCommand = this._getCommandMarkdown( 'aws.amazonq.security.ignore', @@ -115,22 +114,6 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { ) markdownString.appendMarkdown(' | ' + ignoreSimilarIssuesCommand) - if (suggestedFix && suggestedFix.code) { - const applyFixCommand = this._getCommandMarkdown( - 'aws.amazonq.applySecurityFix', - [issue, filePath, 'hover'], - 'wrench', - 'Fix', - 'Fix with Amazon Q' - ) - markdownString.appendMarkdown(' | ' + applyFixCommand) - - markdownString.appendMarkdown('### Suggested Fix Preview\n') - markdownString.appendMarkdown( - `${this._makeCodeBlock(suggestedFix.code, issue.detectorId.split('/').shift())}\n` - ) - } - return markdownString } @@ -145,60 +128,4 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { } return `![${severity}](severity-${severity.toLowerCase()}.svg)` } - - /** - * Creates a markdown string to render a code diff block for a given code block. Lines - * that are highlighted red indicate deletion while lines highlighted in green indicate - * addition. An optional language can be provided for syntax highlighting on lines which are - * not additions or deletions. - * - * @param code The code containing the diff - * @param language The language for syntax highlighting - * @returns The markdown string - */ - private _makeCodeBlock(code: string, language?: string) { - const lines = code - .replaceAll('\n\\ No newline at end of file', '') - .replaceAll('--- buggyCode\n', '') - .replaceAll('+++ fixCode\n', '') - .split('\n') - const maxLineChars = lines.reduce((acc, curr) => Math.max(acc, curr.length), 0) - const paddedLines = lines.map((line) => line.padEnd(maxLineChars + 2)) - - // Group the lines into sections so consecutive lines of the same type can be placed in - // the same span below - const sections = [paddedLines[0]] - let i = 1 - while (i < paddedLines.length) { - if (paddedLines[i][0] === sections[sections.length - 1][0]) { - sections[sections.length - 1] += '\n' + paddedLines[i] - } else { - sections.push(paddedLines[i]) - } - i++ - } - - // Return each section with the correct syntax highlighting and background color - return sections - .map( - (section) => ` - - -\`\`\`${section.startsWith('-') || section.startsWith('+') ? 'diff' : section.startsWith('@@') ? undefined : language} -${section} -\`\`\` - - -` - ) - .join('
') - } } diff --git a/packages/core/src/codewhisperer/service/securityIssueProvider.ts b/packages/core/src/codewhisperer/service/securityIssueProvider.ts index 61957e6eca5..d055cb0a7d5 100644 --- a/packages/core/src/codewhisperer/service/securityIssueProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueProvider.ts @@ -5,6 +5,8 @@ import * as vscode from 'vscode' import { AggregatedCodeScanIssue, CodeScanIssue, SuggestedFix } from '../models/model' +import { randomUUID } from '../../shared/crypto' + export class SecurityIssueProvider { static #instance: SecurityIssueProvider public static get instance() { @@ -20,6 +22,15 @@ export class SecurityIssueProvider { this._issues = issues } + private _id: string = randomUUID() + public get id() { + return this._id + } + + public set id(id: string) { + this._id = id + } + public handleDocumentChange(event: vscode.TextDocumentChangeEvent) { // handleDocumentChange function may be triggered while testing by our own code generation. if (!event.contentChanges || event.contentChanges.length === 0) { diff --git a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts index d7c93f70423..b1f7f73907b 100644 --- a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts @@ -189,11 +189,8 @@ export class IssueItem extends vscode.TreeItem { } private getDescription() { - const positionStr = `[Ln ${this.issue.startLine + 1}, Col 1]` const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() - return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation - ? `${path.basename(this.filePath)} ${positionStr}` - : positionStr + return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation ? `${path.basename(this.filePath)}` : '' } private getContextValue() { diff --git a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts index d511bd9a5f6..632283215ab 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts +++ b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts @@ -109,7 +109,10 @@ export class SecurityIssueWebview extends VueWebview { } public generateFix() { - void vscode.commands.executeCommand('aws.amazonq.security.generateFix', this.issue, this.filePath, 'webview') + const args = [this.issue] + void this.navigateToFile()?.then(() => { + void vscode.commands.executeCommand('aws.amazonq.generateFix', ...args) + }) } public regenerateFix() { diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index b911c9687ee..05164274b70 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -8,7 +8,7 @@ import assert from 'assert' import * as sinon from 'sinon' import * as CodeWhispererConstants from '../../../codewhisperer/models/constants' import { createCodeScanIssue, createMockDocument, resetCodeWhispererGlobalVariables } from '../testUtil' -import { assertNoTelemetryMatch, assertTelemetry, assertTelemetryCurried, tryRegister } from '../../testUtil' +import { assertTelemetry, assertTelemetryCurried, tryRegister } from '../../testUtil' import { toggleCodeSuggestions, showSecurityScan, @@ -19,10 +19,8 @@ import { reconnect, signoutCodeWhisperer, toggleCodeScans, - generateFix, rejectFix, ignoreIssue, - regenerateFix, ignoreAllIssues, } from '../../../codewhisperer/commands/basicCommands' import { FakeExtensionContext } from '../../fakeExtensionContext' @@ -30,7 +28,7 @@ import { testCommand } from '../../shared/vscode/testUtils' import { Command, placeholder } from '../../../shared/vscode/commands2' import { SecurityPanelViewProvider } from '../../../codewhisperer/views/securityPanelViewProvider' import { DefaultCodeWhispererClient } from '../../../codewhisperer/client/codewhisperer' -import { Stub, stub } from '../../utilities/stubber' +import { stub } from '../../utilities/stubber' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { getTestWindow } from '../../shared/vscode/window' import { ExtContext } from '../../../shared/extensions' @@ -68,7 +66,6 @@ import { SecurityIssueProvider } from '../../../codewhisperer/service/securityIs import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' import { confirm } from '../../../shared' import * as commentUtils from '../../../shared/utilities/commentUtils' -import * as startCodeFixGeneration from '../../../codewhisperer/commands/startCodeFixGeneration' import * as extUtils from '../../../shared/extensionUtilities' describe('CodeWhisperer-basicCommands', function () { @@ -790,173 +787,7 @@ def execute_input_compliant(): }) }) - describe('generateFix', function () { - let sandbox: sinon.SinonSandbox - let mockClient: Stub - let startCodeFixGenerationStub: sinon.SinonStub - let filePath: string - let codeScanIssue: CodeScanIssue - let issueItem: IssueItem - let updateSecurityIssueWebviewMock: sinon.SinonStub - let updateIssueMock: sinon.SinonStub - let refreshTreeViewMock: sinon.SinonStub - let mockExtContext: ExtContext - - beforeEach(async function () { - sandbox = sinon.createSandbox() - mockClient = stub(DefaultCodeWhispererClient) - startCodeFixGenerationStub = sinon.stub(startCodeFixGeneration, 'startCodeFixGeneration') - filePath = 'dummy/file.py' - codeScanIssue = createCodeScanIssue({ - findingId: randomUUID(), - ruleId: 'dummy-rule-id', - }) - issueItem = new IssueItem(filePath, codeScanIssue) - updateSecurityIssueWebviewMock = sinon.stub(securityIssueWebview, 'updateSecurityIssueWebview') - updateIssueMock = sinon.stub(SecurityIssueProvider.instance, 'updateIssue') - refreshTreeViewMock = sinon.stub(SecurityIssueTreeViewProvider.instance, 'refresh') - mockExtContext = await FakeExtensionContext.getFakeExtContext() - }) - - afterEach(function () { - sandbox.restore() - }) - - it('should call generateFix command successfully', async function () { - startCodeFixGenerationStub.resolves({ - suggestedFix: { - codeDiff: 'codeDiff', - description: 'description', - references: [], - }, - jobId: 'jobId', - }) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(codeScanIssue, filePath, 'webview') - - assert.ok(updateSecurityIssueWebviewMock.calledWith(sinon.match({ isGenerateFixLoading: true }))) - assert.ok( - startCodeFixGenerationStub.calledWith(mockClient, codeScanIssue, filePath, codeScanIssue.findingId) - ) - - const expectedUpdatedIssue = { - ...codeScanIssue, - fixJobId: 'jobId', - suggestedFixes: [{ code: 'codeDiff', description: 'description', references: [] }], - } - assert.ok( - updateSecurityIssueWebviewMock.calledWith( - sinon.match({ - issue: expectedUpdatedIssue, - isGenerateFixLoading: false, - filePath: filePath, - shouldRefreshView: true, - }) - ) - ) - assert.ok(updateIssueMock.calledWith(expectedUpdatedIssue, filePath)) - assert.ok(refreshTreeViewMock.calledOnce) - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'webview', - result: 'Succeeded', - }) - }) - - it('should call generateFix from tree view item', async function () { - startCodeFixGenerationStub.resolves({ - suggestedFix: { - codeDiff: 'codeDiff', - description: 'description', - references: [], - }, - jobId: 'jobId', - }) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(issueItem, filePath, 'tree') - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'tree', - result: 'Succeeded', - }) - }) - - it('should call generateFix with refresh=true to indicate fix regenerated', async function () { - startCodeFixGenerationStub.resolves({ - suggestedFix: { - codeDiff: 'codeDiff', - description: 'description', - references: [], - }, - jobId: 'jobId', - }) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(codeScanIssue, filePath, 'webview', true) - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'webview', - result: 'Succeeded', - variant: 'refresh', - }) - }) - - it('should handle generateFix error', async function () { - startCodeFixGenerationStub.throws(new Error('Unexpected error')) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(codeScanIssue, filePath, 'webview') - - assert.ok(updateSecurityIssueWebviewMock.calledWith(sinon.match({ isGenerateFixLoading: true }))) - assert.ok( - updateSecurityIssueWebviewMock.calledWith( - sinon.match({ - issue: codeScanIssue, - isGenerateFixLoading: false, - generateFixError: 'Unexpected error', - shouldRefreshView: false, - }) - ) - ) - assert.ok(updateIssueMock.calledWith(codeScanIssue, filePath)) - assert.ok(refreshTreeViewMock.calledOnce) - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'webview', - result: 'Failed', - reason: 'Error', - reasonDesc: 'Unexpected error', - }) - }) - - it('exits early for SAS findings', async function () { - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - codeScanIssue = createCodeScanIssue({ - ruleId: CodeWhispererConstants.sasRuleId, - }) - issueItem = new IssueItem(filePath, codeScanIssue) - await targetCommand.execute(codeScanIssue, filePath, 'webview') - assert.ok(updateSecurityIssueWebviewMock.notCalled) - assert.ok(startCodeFixGenerationStub.notCalled) - assert.ok(updateIssueMock.notCalled) - assert.ok(refreshTreeViewMock.notCalled) - assertNoTelemetryMatch('codewhisperer_codeScanIssueGenerateFix') - }) - }) + // TODO: Add integ test for generateTest describe('rejectFix', function () { let mockExtensionContext: vscode.ExtensionContext @@ -1150,51 +981,4 @@ def execute_input_compliant(): }) }) }) - - describe('regenerateFix', function () { - let sandbox: sinon.SinonSandbox - let filePath: string - let codeScanIssue: CodeScanIssue - let issueItem: IssueItem - let rejectFixMock: sinon.SinonStub - let generateFixMock: sinon.SinonStub - - beforeEach(function () { - sandbox = sinon.createSandbox() - filePath = 'dummy/file.py' - codeScanIssue = createCodeScanIssue({ - findingId: randomUUID(), - suggestedFixes: [{ code: 'diff', description: 'description' }], - }) - issueItem = new IssueItem(filePath, codeScanIssue) - rejectFixMock = sinon.stub() - generateFixMock = sinon.stub() - }) - - afterEach(function () { - sandbox.restore() - }) - - it('should call regenerateFix command successfully', async function () { - const updatedIssue = createCodeScanIssue({ findingId: 'updatedIssue' }) - sinon.stub(rejectFix, 'execute').value(rejectFixMock.resolves(updatedIssue)) - sinon.stub(generateFix, 'execute').value(generateFixMock) - targetCommand = testCommand(regenerateFix) - await targetCommand.execute(codeScanIssue, filePath) - - assert.ok(rejectFixMock.calledWith(codeScanIssue, filePath)) - assert.ok(generateFixMock.calledWith(updatedIssue, filePath)) - }) - - it('should call regenerateFix from tree view item', async function () { - const updatedIssue = createCodeScanIssue({ findingId: 'updatedIssue' }) - sinon.stub(rejectFix, 'execute').value(rejectFixMock.resolves(updatedIssue)) - sinon.stub(generateFix, 'execute').value(generateFixMock) - targetCommand = testCommand(regenerateFix) - await targetCommand.execute(issueItem, filePath) - - assert.ok(rejectFixMock.calledWith(codeScanIssue, filePath)) - assert.ok(generateFixMock.calledWith(updatedIssue, filePath)) - }) - }) })