Skip to content

Commit 99a92e4

Browse files
authored
Amazon Q Transform - Bugfixes (#4993)
Amazon Q Transform Bugfixes - Chore - fix spelling error - Bugfix - uploadId overwriting jobId causing issues calling getTransformation - Bugfix - fix Human in the loop chat messages
1 parent b24e963 commit 99a92e4

File tree

6 files changed

+125
-58
lines changed

6 files changed

+125
-58
lines changed

packages/core/src/amazonqGumby/chat/controller/controller.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import { getLogger } from '../../../shared/logger'
1616
import { featureName } from '../../models/constants'
1717
import { AuthUtil } from '../../../codewhisperer/util/authUtil'
1818
import {
19+
cleanupTransformationJob,
1920
compileProject,
2021
finishHumanInTheLoop,
2122
getValidCandidateProjects,
2223
openHilPomFile,
24+
postTransformationJob,
2325
processTransformFormInput,
2426
startTransformByQ,
2527
stopTransformByQ,
@@ -29,6 +31,7 @@ import { JDKVersion, TransformationCandidateProject, transformByQState } from '.
2931
import {
3032
AlternateDependencyVersionsNotFoundError,
3133
JavaHomeNotSetError,
34+
JobStartError,
3235
ModuleUploadError,
3336
NoJavaProjectsFoundError,
3437
NoMavenJavaProjectsFoundError,
@@ -251,17 +254,19 @@ export class GumbyController {
251254
break
252255
case ButtonActions.STOP_TRANSFORMATION_JOB:
253256
await stopTransformByQ(transformByQState.getJobId(), CancelActionPositions.Chat)
257+
await postTransformationJob()
258+
await cleanupTransformationJob()
254259
break
255260
case ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW:
256261
this.messenger.sendCommandMessage({ ...message, command: GumbyCommands.CLEAR_CHAT })
257262
await this.transformInitiated(message)
258263
break
259264
case ButtonActions.CONFIRM_DEPENDENCY_FORM:
260-
await this.continueJobWithSelectedDependency({ ...message, tabID: message.tabId })
265+
await this.continueJobWithSelectedDependency(message)
261266
break
262267
case ButtonActions.CANCEL_DEPENDENCY_FORM:
263-
this.messenger.sendUserPrompt('Cancel', message.tabId)
264-
await this.continueTransformationWithoutHIL({ tabID: message.tabId })
268+
this.messenger.sendUserPrompt('Cancel', message.tabID)
269+
await this.continueTransformationWithoutHIL(message)
265270
break
266271
case ButtonActions.OPEN_FILE:
267272
await openHilPomFile()
@@ -349,12 +354,16 @@ export class GumbyController {
349354
await this.prepareProjectForSubmission(message)
350355
}
351356

352-
private async transformationFinished(data: { message?: string; tabID: string }) {
353-
this.sessionStorage.getSession().conversationState = ConversationState.IDLE
357+
private transformationFinished(data: { message?: string; tabID: string }) {
358+
this.resetTransformationChatFlow()
354359
// at this point job is either completed, partially_completed, cancelled, or failed
355360
this.messenger.sendJobFinishedMessage(data.tabID, data.message)
356361
}
357362

363+
private resetTransformationChatFlow() {
364+
this.sessionStorage.getSession().conversationState = ConversationState.IDLE
365+
}
366+
358367
private startHILIntervention(data: { tabID: string; codeSnippet: string }) {
359368
this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT
360369
this.messenger.sendHumanInTheLoopInitialMessage(data.tabID, data.codeSnippet)
@@ -406,10 +415,11 @@ export class GumbyController {
406415
this.messenger.sendKnownErrorResponse('no-alternate-dependencies-found', message.tabID)
407416
await this.continueTransformationWithoutHIL(message)
408417
} else if (message.error instanceof ModuleUploadError) {
409-
this.messenger.sendKnownErrorResponse('upload-to-s3-failed', message.tabID)
410-
await this.transformationFinished(message)
411-
} else {
412-
this.messenger.sendErrorMessage(message.error.message, message.tabID)
418+
this.messenger.sendUnrecoverableErrorResponse('upload-to-s3-failed', message.tabID)
419+
this.resetTransformationChatFlow()
420+
} else if (message.error instanceof JobStartError) {
421+
this.messenger.sendUnrecoverableErrorResponse('job-start-failed', message.tabID)
422+
this.resetTransformationChatFlow()
413423
}
414424
}
415425

@@ -421,7 +431,7 @@ export class GumbyController {
421431
try {
422432
await finishHumanInTheLoop()
423433
} catch (err: any) {
424-
await this.transformationFinished({ tabID: message.tabID })
434+
this.transformationFinished({ tabID: message.tabID })
425435
}
426436

427437
this.messenger.sendStaticTextResponse('end-HIL-early', message.tabID)

packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,15 @@ export type UnrecoverableErrorType =
4343
| 'could-not-compile-project'
4444
| 'invalid-java-home'
4545
| 'unsupported-source-jdk-version'
46+
| 'upload-to-s3-failed'
47+
| 'job-start-failed'
4648

47-
export type ErrorResponseType = 'no-alternate-dependencies-found' | 'upload-to-s3-failed'
49+
export type ErrorResponseType = 'no-alternate-dependencies-found'
4850

4951
export enum GumbyNamedMessages {
5052
COMPILATION_PROGRESS_MESSAGE = 'gumbyProjectCompilationMessage',
5153
JOB_SUBMISSION_STATUS_MESSAGE = 'gumbyJobSubmissionMessage',
54+
JOB_SUBMISSION_WITH_DEPENDENCY_STATUS_MESSAGE = 'gumbyJobSubmissionWithDependencyMessage',
5255
}
5356

5457
export class Messenger {
@@ -227,7 +230,8 @@ export class Messenger {
227230
public sendJobSubmittedMessage(
228231
tabID: string,
229232
disableJobActions: boolean = false,
230-
message = CodeWhispererConstants.jobStartedChatMessage
233+
message: string = CodeWhispererConstants.jobStartedChatMessage,
234+
messageID: string = GumbyNamedMessages.JOB_SUBMISSION_STATUS_MESSAGE
231235
) {
232236
const buttons: ChatItemButton[] = []
233237

@@ -251,7 +255,7 @@ export class Messenger {
251255
{
252256
message,
253257
messageType: 'ai-prompt',
254-
messageId: GumbyNamedMessages.JOB_SUBMISSION_STATUS_MESSAGE,
258+
messageId: messageID,
255259
buttons,
256260
},
257261
tabID
@@ -322,6 +326,13 @@ export class Messenger {
322326
break
323327
case 'unsupported-source-jdk-version':
324328
message = CodeWhispererConstants.unsupportedJavaVersionChatMessage
329+
break
330+
case 'upload-to-s3-failed':
331+
message = `I was not able to upload your module to be transformed. Please try again later.`
332+
break
333+
case 'job-start-failed':
334+
message = CodeWhispererConstants.failedToStartJobTooManyJobsChatMessage
335+
break
325336
}
326337

327338
const buttons: ChatItemButton[] = []
@@ -356,9 +367,6 @@ export class Messenger {
356367
case 'no-alternate-dependencies-found':
357368
message = `I could not find any other versions of this dependency in your local Maven repository. Try transforming the dependency to make it compatible with Java 17, and then try transforming this module again.`
358369
break
359-
case 'upload-to-s3-failed':
360-
message = `I was not able to upload your module to be transformed. Please try again later.`
361-
break
362370
}
363371

364372
this.dispatcher.sendChatMessage(
@@ -533,12 +541,23 @@ ${codeSnippet}
533541

534542
this.dispatcher.sendChatMessage(new ChatMessage({ message, messageType: 'prompt' }, tabID))
535543

536-
message = `I recieved your target version dependency.`
544+
message = `I received your target version dependency.`
537545
this.sendInProgressMessage(tabID, message)
538546
}
539547

540548
public sendHILResumeMessage(tabID: string) {
541549
const message = `I will continue transforming your code. You can monitor progress in the Transformation Hub.`
542-
this.sendJobSubmittedMessage(tabID, false, message)
550+
this.sendAsyncEventProgress(
551+
tabID,
552+
true,
553+
undefined,
554+
GumbyNamedMessages.JOB_SUBMISSION_WITH_DEPENDENCY_STATUS_MESSAGE
555+
)
556+
this.sendJobSubmittedMessage(
557+
tabID,
558+
false,
559+
message,
560+
GumbyNamedMessages.JOB_SUBMISSION_WITH_DEPENDENCY_STATUS_MESSAGE
561+
)
543562
}
544563
}

packages/core/src/amazonqGumby/errors.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,15 @@ export class ModuleUploadError extends Error {
5353
super('Failed to upload module to S3')
5454
}
5555
}
56+
57+
export class JobStartError extends Error {
58+
constructor() {
59+
super('Failed to start job')
60+
}
61+
}
62+
63+
export class PollJobError extends Error {
64+
constructor() {
65+
super('Poll job failed')
66+
}
67+
}

packages/core/src/codewhisperer/commands/startTransformByQ.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ import { placeholder } from '../../shared/vscode/commands2'
6060
import {
6161
AlternateDependencyVersionsNotFoundError,
6262
JavaHomeNotSetError,
63+
JobStartError,
6364
ModuleUploadError,
65+
PollJobError,
6466
} from '../../amazonqGumby/errors'
6567
import { ChatSessionManager } from '../../amazonqGumby/chat/storages/chatSession'
6668
import {
@@ -425,6 +427,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) {
425427
},
426428
}),
427429
})
430+
428431
await uploadPayload(uploadPayloadFilePath, {
429432
transformationUploadContext: {
430433
jobId,
@@ -486,7 +489,7 @@ export async function startTransformationJob(uploadId: string) {
486489
transformByQState.setJobFailureErrorNotification(CodeWhispererConstants.failedToStartJobNotification)
487490
transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToStartJobChatMessage)
488491
}
489-
throw new Error('Start job failed')
492+
throw new JobStartError()
490493
}
491494

492495
await sleep(2000) // sleep before polling job to prevent ThrottlingException
@@ -506,7 +509,7 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string) {
506509
if (!transformByQState.getJobFailureErrorChatMessage()) {
507510
transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage)
508511
}
509-
throw new Error('Poll job failed')
512+
throw new PollJobError()
510513
}
511514
let plan = undefined
512515
try {
@@ -542,7 +545,7 @@ export async function pollTransformationStatusUntilComplete(jobId: string) {
542545
if (!transformByQState.getJobFailureErrorChatMessage()) {
543546
transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage)
544547
}
545-
throw new Error('Poll job failed')
548+
throw new PollJobError()
546549
}
547550

548551
return status
@@ -695,9 +698,13 @@ export async function transformationJobErrorHandler(error: any) {
695698
} else {
696699
transformByQState.setToCancelled()
697700
transformByQState.setPolledJobStatus('CANCELLED')
698-
transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.jobCancelledChatMessage)
699701
}
700702
getLogger().error(`CodeTransformation: ${error.message}`)
703+
704+
transformByQState.getChatControllers()?.errorThrown.fire({
705+
error,
706+
tabID: ChatSessionManager.Instance.getSession().tabID,
707+
})
701708
}
702709

703710
export async function cleanupTransformationJob() {
@@ -743,12 +750,13 @@ export async function stopTransformByQ(
743750
void submitFeedback(placeholder, CodeWhispererConstants.amazonQFeedbackKey)
744751
}
745752
})
753+
} finally {
754+
telemetry.codeTransform_jobIsCancelledByUser.emit({
755+
codeTransformCancelSrcComponents: cancelSrc as CodeTransformCancelSrcComponents,
756+
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
757+
result: MetadataResult.Pass,
758+
})
746759
}
747-
telemetry.codeTransform_jobIsCancelledByUser.emit({
748-
codeTransformCancelSrcComponents: cancelSrc as CodeTransformCancelSrcComponents,
749-
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
750-
result: MetadataResult.Pass,
751-
})
752760
}
753761
}
754762

packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -174,40 +174,46 @@ export async function resumeTransformationJob(jobId: string, userActionStatus: T
174174
}
175175

176176
export async function stopJob(jobId: string) {
177-
if (jobId !== '') {
178-
try {
179-
const apiStartTime = Date.now()
180-
const response = await codeWhisperer.codeWhispererClient.codeModernizerStopCodeTransformation({
181-
transformationJobId: jobId,
182-
})
183-
if (response !== undefined) {
184-
telemetry.codeTransform_logApiLatency.emit({
185-
codeTransformApiNames: 'StopTransformation',
186-
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
187-
codeTransformJobId: jobId,
188-
codeTransformRunTimeLatency: calculateTotalLatency(apiStartTime),
189-
codeTransformRequestId: response.$response.requestId,
190-
result: MetadataResult.Pass,
191-
})
192-
// always store request ID, but it will only show up in a notification if an error occurs
193-
if (response.$response.requestId) {
194-
transformByQState.setJobFailureMetadata(` (request ID: ${response.$response.requestId})`)
195-
}
196-
}
197-
} catch (e: any) {
198-
const errorMessage = (e as Error).message
199-
getLogger().error(`CodeTransformation: StopTransformation error = ${errorMessage}`)
200-
telemetry.codeTransform_logApiError.emit({
177+
if (!jobId) {
178+
throw new Error('Job ID is empty')
179+
}
180+
181+
if (transformByQState.isNotStarted()) {
182+
return
183+
}
184+
185+
try {
186+
const apiStartTime = Date.now()
187+
const response = await codeWhisperer.codeWhispererClient.codeModernizerStopCodeTransformation({
188+
transformationJobId: jobId,
189+
})
190+
if (response !== undefined) {
191+
telemetry.codeTransform_logApiLatency.emit({
201192
codeTransformApiNames: 'StopTransformation',
202193
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
203194
codeTransformJobId: jobId,
204-
codeTransformApiErrorMessage: errorMessage,
205-
codeTransformRequestId: e.requestId ?? '',
206-
result: MetadataResult.Fail,
207-
reason: 'StopTransformationFailed',
195+
codeTransformRunTimeLatency: calculateTotalLatency(apiStartTime),
196+
codeTransformRequestId: response.$response.requestId,
197+
result: MetadataResult.Pass,
208198
})
209-
throw new Error('Stop job failed')
199+
// always store request ID, but it will only show up in a notification if an error occurs
200+
if (response.$response.requestId) {
201+
transformByQState.setJobFailureMetadata(` (request ID: ${response.$response.requestId})`)
202+
}
210203
}
204+
} catch (e: any) {
205+
const errorMessage = (e as Error).message
206+
getLogger().error(`CodeTransformation: StopTransformation error = ${errorMessage}`)
207+
telemetry.codeTransform_logApiError.emit({
208+
codeTransformApiNames: 'StopTransformation',
209+
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
210+
codeTransformJobId: jobId,
211+
codeTransformApiErrorMessage: errorMessage,
212+
codeTransformRequestId: e.requestId ?? '',
213+
result: MetadataResult.Fail,
214+
reason: 'StopTransformationFailed',
215+
})
216+
throw new Error('Stop job failed')
211217
}
212218
}
213219

@@ -256,7 +262,11 @@ export async function uploadPayload(payloadFileName: string, uploadContext?: Upl
256262
getLogger().error(`CodeTransformation: UploadArtifactToS3 error: = ${errorMessage}`)
257263
throw new Error('S3 upload failed')
258264
}
259-
transformByQState.setJobId(encodeHTML(response.uploadId))
265+
// UploadContext only exists for subsequent uploads, and they will return a uploadId that is NOT
266+
// the jobId. Only the initial call will uploadId be the jobId
267+
if (!uploadContext) {
268+
transformByQState.setJobId(encodeHTML(response.uploadId))
269+
}
260270
updateJobHistory()
261271
return response.uploadId
262272
}

packages/core/src/test/codewhisperer/commands/transformByQ.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('transformByQ', function () {
4242

4343
beforeEach(async function () {
4444
tempDir = await makeTemporaryToolkitFolder()
45+
transformByQState.setToNotStarted()
4546
})
4647

4748
afterEach(async function () {
@@ -130,15 +131,22 @@ describe('transformByQ', function () {
130131
}, NoOpenProjectsError)
131132
})
132133

134+
it('WHEN stop job called with invalid jobId THEN throws error', async function () {
135+
await assert.rejects(async () => {
136+
await stopJob('')
137+
}, Error)
138+
})
139+
133140
it('WHEN stop job called with valid jobId THEN stop API called', async function () {
141+
transformByQState.setToRunning()
134142
const stopJobStub = sinon.stub(codeWhisperer.codeWhispererClient, 'codeModernizerStopCodeTransformation')
135143
await stopJob('dummyId')
136144
sinon.assert.calledWithExactly(stopJobStub, { transformationJobId: 'dummyId' })
137145
})
138146

139147
it('WHEN stop job that has not been started THEN stop API not called', async function () {
140148
const stopJobStub = sinon.stub(codeWhisperer.codeWhispererClient, 'codeModernizerStopCodeTransformation')
141-
await stopJob('')
149+
await stopJob('dummyId')
142150
sinon.assert.notCalled(stopJobStub)
143151
})
144152

0 commit comments

Comments
 (0)