diff --git a/packages/amazonq/.changes/next-release/Feature-96409066-931c-4488-b943-7bee68626547.json b/packages/amazonq/.changes/next-release/Feature-96409066-931c-4488-b943-7bee68626547.json new file mode 100644 index 00000000000..43ea135f6f9 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-96409066-931c-4488-b943-7bee68626547.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "/test: display test plan summary in chat after generating tests" +} diff --git a/packages/core/src/amazonqTest/app.ts b/packages/core/src/amazonqTest/app.ts index 041670b0c2e..62ed24aee0d 100644 --- a/packages/core/src/amazonqTest/app.ts +++ b/packages/core/src/amazonqTest/app.ts @@ -23,7 +23,7 @@ export function init(appContext: AmazonQAppInitContext) { authClicked: new vscode.EventEmitter(), startTestGen: new vscode.EventEmitter(), processHumanChatMessage: new vscode.EventEmitter(), - updateShortAnswer: new vscode.EventEmitter(), + updateTargetFileInfo: new vscode.EventEmitter(), showCodeGenerationResults: new vscode.EventEmitter(), openDiff: new vscode.EventEmitter(), formActionClicked: new vscode.EventEmitter(), diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts index 2eaf28b9c4d..4e29bb31464 100644 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ b/packages/core/src/amazonqTest/chat/controller/controller.ts @@ -53,7 +53,7 @@ import { randomUUID } from '../../../shared/crypto' import { tempDirPath, testGenerationLogsDir } from '../../../shared/filesystemUtilities' import { CodeReference } from '../../../codewhispererChat/view/connector/connector' import { TelemetryHelper } from '../../../codewhisperer/util/telemetryHelper' -import { ShortAnswer, ShortAnswerReference, testGenState } from '../../../codewhisperer/models/model' +import { Reference, testGenState } from '../../../codewhisperer/models/model' import { referenceLogText, TestGenerationBuildStep, @@ -63,6 +63,7 @@ import { } from '../../../codewhisperer/models/constants' import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker' import { ReferenceLogViewProvider } from '../../../codewhisperer/service/referenceLogViewProvider' +import { TargetFileInfo } from '../../../codewhisperer/client/codewhispereruserclient' import { submitFeedback } from '../../../feedback/vue/submitFeedback' import { placeholder } from '../../../shared/vscode/commands2' import { Auth } from '../../../auth/auth' @@ -73,7 +74,7 @@ export interface TestChatControllerEventEmitters { readonly authClicked: vscode.EventEmitter readonly startTestGen: vscode.EventEmitter readonly processHumanChatMessage: vscode.EventEmitter - readonly updateShortAnswer: vscode.EventEmitter + readonly updateTargetFileInfo: vscode.EventEmitter readonly showCodeGenerationResults: vscode.EventEmitter readonly openDiff: vscode.EventEmitter readonly formActionClicked: vscode.EventEmitter @@ -134,8 +135,8 @@ export class TestController { return this.handleFormActionClicked(data) }) - this.chatControllerMessageListeners.updateShortAnswer.event((data) => { - return this.updateShortAnswer(data) + this.chatControllerMessageListeners.updateTargetFileInfo.event((data) => { + return this.updateTargetFileInfo(data) }) this.chatControllerMessageListeners.showCodeGenerationResults.event((data) => { @@ -638,10 +639,9 @@ export class TestController { } } - private async updateShortAnswer(message: { + private async updateTargetFileInfo(message: { tabID: string - status: string - shortAnswer?: ShortAnswer + targetFileInfo?: TargetFileInfo testGenerationJobGroupName: string testGenerationJobId: string type: ChatItemType @@ -651,11 +651,11 @@ export class TestController { type: 'answer', tabID: message.tabID, message: testGenSummaryMessage( - path.basename(message.shortAnswer?.sourceFilePath ?? message.filePath), - message.shortAnswer?.planSummary?.replaceAll('```', '') + path.basename(message.targetFileInfo?.filePath ?? message.filePath), + message.targetFileInfo?.filePlan?.replaceAll('```', '') ), canBeVoted: true, - filePath: message.shortAnswer?.testFilePath, + filePath: message.targetFileInfo?.testFilePath, }) } @@ -705,7 +705,7 @@ export class TestController { tabID: data.tabID, messageType: 'answer', codeGenerationId: '', - message: `Please see the unit tests generated below. Click “View diff” to review the changes in the code editor.`, + message: `${session.jobSummary}\n\n Please see the unit tests generated below. Click “View diff” to review the changes in the code editor.`, canBeVoted: true, messageId: '', followUps, @@ -715,7 +715,7 @@ export class TestController { filePaths: [data.filePath], }, codeReference: session.references.map( - (ref: ShortAnswerReference) => + (ref: Reference) => ({ ...ref, information: `${ref.licenseName} - ${ref.repository}`, diff --git a/packages/core/src/amazonqTest/chat/session/session.ts b/packages/core/src/amazonqTest/chat/session/session.ts index 16809ad7cef..4e3780e6f99 100644 --- a/packages/core/src/amazonqTest/chat/session/session.ts +++ b/packages/core/src/amazonqTest/chat/session/session.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ShortAnswer, ShortAnswerReference } from '../../../codewhisperer/models/model' -import { TestGenerationJob } from '../../../codewhisperer/client/codewhispereruserclient' +import { ShortAnswer, Reference } from '../../../codewhisperer/models/model' +import { TargetFileInfo, TestGenerationJob } from '../../../codewhisperer/client/codewhispereruserclient' export enum ConversationState { IDLE, @@ -43,6 +43,8 @@ export class Session { public projectRootPath: string = '' public fileLanguage: string | undefined = 'plaintext' public stopIteration: boolean = false + public targetFileInfo: TargetFileInfo | undefined + public jobSummary: string = '' // Telemetry public testGenerationStartTime: number = 0 @@ -65,7 +67,7 @@ export class Session { public testCoveragePercentage: number = 90 public isInProgress: boolean = false public acceptedJobId = '' - public references: ShortAnswerReference[] = [] + public references: Reference[] = [] constructor() {} diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 1f33cb8c98c..ed7661bec4c 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -1760,6 +1760,39 @@ "type": "string", "enum": ["OPTIN", "OPTOUT"] }, + "PackageInfo": { + "type": "structure", + "members": { + "executionCommand": { "shape": "SensitiveString" }, + "buildCommand": { "shape": "SensitiveString" }, + "buildOrder": { "shape": "PackageInfoBuildOrderInteger" }, + "testFramework": { "shape": "String" }, + "packageSummary": { "shape": "PackageInfoPackageSummaryString" }, + "packagePlan": { "shape": "PackageInfoPackagePlanString" }, + "targetFileInfoList": { "shape": "TargetFileInfoList" } + } + }, + "PackageInfoBuildOrderInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "PackageInfoList": { + "type": "list", + "member": { "shape": "PackageInfo" } + }, + "PackageInfoPackagePlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "PackageInfoPackageSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, "PaginationToken": { "type": "string", "max": 2048, @@ -2418,6 +2451,45 @@ "min": 1, "sensitive": true }, + "TargetFileInfo": { + "type": "structure", + "members": { + "filePath": { "shape": "SensitiveString" }, + "testFilePath": { "shape": "SensitiveString" }, + "testCoverage": { "shape": "TargetFileInfoTestCoverageInteger" }, + "fileSummary": { "shape": "TargetFileInfoFileSummaryString" }, + "filePlan": { "shape": "TargetFileInfoFilePlanString" }, + "codeReferences": { "shape": "References" }, + "numberOfTestMethods": { "shape": "TargetFileInfoNumberOfTestMethodsInteger" } + } + }, + "TargetFileInfoFilePlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "TargetFileInfoFileSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "TargetFileInfoList": { + "type": "list", + "member": { "shape": "TargetFileInfo" } + }, + "TargetFileInfoNumberOfTestMethodsInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "TargetFileInfoTestCoverageInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 0 + }, "TaskAssistPlan": { "type": "list", "member": { "shape": "TaskAssistPlanStep" }, @@ -2556,7 +2628,11 @@ "status": { "shape": "TestGenerationJobStatus" }, "shortAnswer": { "shape": "SensitiveString" }, "creationTime": { "shape": "Timestamp" }, - "progressRate": { "shape": "TestGenerationJobProgressRateInteger" } + "progressRate": { "shape": "TestGenerationJobProgressRateInteger" }, + "jobStatusReason": { "shape": "String" }, + "jobSummary": { "shape": "TestGenerationJobJobSummaryString" }, + "jobPlan": { "shape": "TestGenerationJobJobPlanString" }, + "packageInfoList": { "shape": "PackageInfoList" } }, "documentation": "

Represents a test generation job

" }, @@ -2567,6 +2643,18 @@ "min": 1, "pattern": "[a-zA-Z0-9-_]+" }, + "TestGenerationJobJobPlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "TestGenerationJobJobSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, "TestGenerationJobProgressRateInteger": { "type": "integer", "box": true, diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index f7d1fe60f1b..f7a150cb1f0 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -1165,7 +1165,7 @@ export interface FolderInfo { name: string } -export interface ShortAnswerReference { +export interface Reference { licenseName?: string repository?: string url?: string @@ -1175,6 +1175,7 @@ export interface ShortAnswerReference { } } +// TODO: remove ShortAnswer because it will be deprecated export interface ShortAnswer { testFilePath: string buildCommands: string[] @@ -1185,6 +1186,6 @@ export interface ShortAnswer { testCoverage?: number stopIteration?: string errorMessage?: string - codeReferences?: ShortAnswerReference[] + codeReferences?: References numberOfTestMethods?: number } diff --git a/packages/core/src/codewhisperer/service/testGenHandler.ts b/packages/core/src/codewhisperer/service/testGenHandler.ts index cb2875ab78e..bd3f2167d83 100644 --- a/packages/core/src/codewhisperer/service/testGenHandler.ts +++ b/packages/core/src/codewhisperer/service/testGenHandler.ts @@ -18,12 +18,11 @@ import { CreateUploadUrlError, ExportResultsArchiveError, InvalidSourceZipError, - TestGenFailedError, TestGenStoppedError, TestGenTimedOutError, } from '../../amazonqTest/error' import { getMd5, uploadArtifactToS3 } from './securityScanHandler' -import { ShortAnswer, testGenState } from '../models/model' +import { testGenState, Reference } from '../models/model' import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient' import { downloadExportResultArchive } from '../../shared/utilities/download' @@ -156,35 +155,35 @@ export async function pollTestJobStatus( status: 'InProgress', progressRate, }) - const shortAnswerString = resp.testGenerationJob?.shortAnswer - if (shortAnswerString) { - const parsedShortAnswer = JSON.parse(shortAnswerString) - const shortAnswer: ShortAnswer = JSON.parse(parsedShortAnswer) - // Stop the Unit test generation workflow if IDE receive stopIteration = true - if (shortAnswer.stopIteration === 'true') { - session.stopIteration = true - throw new TestGenFailedError(shortAnswer.planSummary) - } - if (shortAnswer.numberOfTestMethods) { - session.numberOfTestsGenerated = Number(shortAnswer.numberOfTestMethods) + const jobSummary = resp.testGenerationJob?.jobSummary ?? '' + const jobSummaryNoBackticks = jobSummary.replace(/^`+|`+$/g, '') + ChatSessionManager.Instance.getSession().jobSummary = jobSummaryNoBackticks + const packageInfoList = resp.testGenerationJob?.packageInfoList ?? [] + const packageInfo = packageInfoList[0] + const targetFileInfo = packageInfo?.targetFileInfoList?.[0] + + if (packageInfo) { + // TODO: will need some fields from packageInfo such as buildCommand, packagePlan, packageSummary + } + if (targetFileInfo) { + if (targetFileInfo.numberOfTestMethods) { + session.numberOfTestsGenerated = Number(targetFileInfo.numberOfTestMethods) } - if (shortAnswer.codeReferences) { - session.references = shortAnswer.codeReferences + if (targetFileInfo.codeReferences) { + session.references = targetFileInfo.codeReferences as Reference[] } if (initialExecution) { - session.generatedFilePath = shortAnswer?.testFilePath ?? '' - const currentPlanSummary = session.shortAnswer?.planSummary - const newPlanSummary = shortAnswer?.planSummary - const status = shortAnswer.stopIteration + session.generatedFilePath = targetFileInfo.testFilePath ?? '' + const currentPlanSummary = session.targetFileInfo?.filePlan + const newPlanSummary = targetFileInfo?.filePlan if (currentPlanSummary !== newPlanSummary && newPlanSummary) { const chatControllers = testGenState.getChatControllers() if (chatControllers) { const currentSession = ChatSessionManager.Instance.getSession() - chatControllers.updateShortAnswer.fire({ + chatControllers.updateTargetFileInfo.fire({ tabID: currentSession.tabID, - status, - shortAnswer, + targetFileInfo, testGenerationJobGroupName: resp.testGenerationJob?.testGenerationJobGroupName, testGenerationJobId: resp.testGenerationJob?.testGenerationJobId, filePath, @@ -192,8 +191,8 @@ export async function pollTestJobStatus( } } } - ChatSessionManager.Instance.getSession().shortAnswer = shortAnswer } + ChatSessionManager.Instance.getSession().targetFileInfo = targetFileInfo if (resp.testGenerationJob?.status !== CodeWhispererConstants.TestGenerationJobStatus.IN_PROGRESS) { // This can be FAILED or COMPLETED status = resp.testGenerationJob?.status as CodeWhispererConstants.TestGenerationJobStatus @@ -243,7 +242,7 @@ export async function exportResultsArchive( const zip = new AdmZip(pathToArchive) zip.extractAllTo(pathToArchiveDir, true) - const testFilePathFromResponse = session?.shortAnswer?.testFilePath + const testFilePathFromResponse = session?.targetFileInfo?.testFilePath const testFilePath = testFilePathFromResponse ? testFilePathFromResponse.split('/').slice(1).join('/') // remove the project name : await getTestFilePathFromZip(pathToArchiveDir)