Skip to content
31 changes: 26 additions & 5 deletions packages/amazonq/test/e2e/amazonq/transformByQ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ describe('Amazon Q Code Transformation', function () {
},
])

transformByQState.setSourceJDKVersion(JDKVersion.JDK8)
transformByQState.setTargetJDKVersion(JDKVersion.JDK17)

tab.addChatMessage({ command: '/transform' })

// wait for /transform to respond with some intro messages and the first user input form
Expand Down Expand Up @@ -141,16 +144,34 @@ describe('Amazon Q Code Transformation', function () {
formItemValues: oneOrMultipleDiffsFormValues,
})

// 2 additional chat messages (including message with 4th form) get sent after 3rd form submitted; wait for both of them
// 2 additional chat messages get sent after 3rd form submitted; wait for both of them
await tab.waitForEvent(() => tab.getChatItems().length > 11, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
const jdkPathPrompt = tab.getChatItems().pop()
assert.strictEqual(jdkPathPrompt?.body?.includes('Enter the path to JDK'), true)

// 2 additional chat messages get sent after 4th form submitted; wait for both of them
// TO-DO: add this back when releasing CSB
/*
const customDependencyVersionPrompt = tab.getChatItems().pop()
assert.strictEqual(
customDependencyVersionPrompt?.body?.includes('You can optionally upload a YAML file'),
true
)
tab.clickCustomFormButton({ id: 'gumbyTransformFormContinue' })

// 2 additional chat messages get sent after Continue button clicked; wait for both of them
await tab.waitForEvent(() => tab.getChatItems().length > 13, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
*/

const sourceJdkPathPrompt = tab.getChatItems().pop()
assert.strictEqual(sourceJdkPathPrompt?.body?.includes('Enter the path to JDK 8'), true)

tab.addChatMessage({ prompt: '/dummy/path/to/jdk8' })

// 2 additional chat messages get sent after JDK path submitted; wait for both of them
await tab.waitForEvent(() => tab.getChatItems().length > 13, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
Expand Down Expand Up @@ -401,7 +422,7 @@ describe('Amazon Q Code Transformation', function () {

it('WHEN transforming a Java 8 project E2E THEN job is successful', async function () {
transformByQState.setTransformationType(TransformationType.LANGUAGE_UPGRADE)
await setMaven()
setMaven()
await startTransformByQ.processLanguageUpgradeTransformFormInput(tempDir, JDKVersion.JDK8, JDKVersion.JDK17)
await startTransformByQ.startTransformByQ()
assert.strictEqual(transformByQState.getPolledJobStatus(), 'COMPLETED')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import assert from 'assert'
import {
TransformationProgressUpdate,
TransformationStep,
findDownloadArtifactProgressUpdate,
findDownloadArtifactStep,
getArtifactsFromProgressUpdate,
} from 'aws-core-vscode/codewhisperer/node'
Expand Down Expand Up @@ -95,4 +96,55 @@ describe('Amazon Q Transform - transformApiHandler tests', function () {
assert.strictEqual(progressUpdate, undefined)
})
})

describe('findDownloadArtifactProgressUpdate', function () {
it('will return correct progress update from transformationStep', function () {
const transformationStepsFixture: TransformationStep[] = [
{
id: 'dummy-id',
name: 'Step name',
description: 'Step description',
status: 'TRANSFORMING',
progressUpdates: [
{
name: 'Progress update name',
status: 'AWAITING_CLIENT_ACTION',
description: 'Client-side build happening now',
startTime: new Date(),
endTime: new Date(),
downloadArtifacts: [
{
downloadArtifactId: 'some-download-artifact-id',
downloadArtifactType: 'some-download-artifact-type',
},
],
},
],
startTime: new Date(),
endTime: new Date(),
},
]
const progressUpdate = findDownloadArtifactProgressUpdate(transformationStepsFixture)
assert.strictEqual(progressUpdate, transformationStepsFixture[0].progressUpdates?.[0])
})

it('will return undefined if step status is NOT AWAITING_CLIENT_ACTION', function () {
const transformationStepsFixture: TransformationStep[] = [
{
id: 'random-id',
name: 'not-awaiting-client-action step name',
description: 'not-awaiting-client-action step description',
status: 'TRANSFORMING',
progressUpdates: [
{
name: 'some progress update name',
status: 'SOMETHING-BESIDES-AWAITING_CLIENT_ACTION',
},
],
},
]
const progressUpdate = findDownloadArtifactProgressUpdate(transformationStepsFixture)
assert.strictEqual(progressUpdate, undefined)
})
})
})
132 changes: 84 additions & 48 deletions packages/core/src/amazonqGumby/chat/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@ import {
processSQLConversionTransformFormInput,
startTransformByQ,
stopTransformByQ,
validateCanCompileProject,
getValidSQLConversionCandidateProjects,
openHilPomFile,
} from '../../../codewhisperer/commands/startTransformByQ'
import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model'
import {
AbsolutePathDetectedError,
AlternateDependencyVersionsNotFoundError,
JavaHomeNotSetError,
JobStartError,
ModuleUploadError,
NoJavaProjectsFoundError,
Expand All @@ -59,8 +57,10 @@ import {
openBuildLogFile,
parseBuildFile,
validateSQLMetadataFile,
validateCustomVersionsFile,
} from '../../../codewhisperer/service/transformByQ/transformFileHandler'
import { getAuthType } from '../../../auth/utils'
import fs from '../../../shared/fs/fs'

// These events can be interactions within the chat,
// or elsewhere in the IDE
Expand Down Expand Up @@ -243,7 +243,7 @@ export class GumbyController {
CodeTransformTelemetryState.instance.setSessionId()

this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_TRANSFORMATION_OBJECTIVE
this.messenger.sendStaticTextResponse('choose-transformation-objective', message.tabID)
this.messenger.sendMessage(CodeWhispererConstants.chooseTransformationObjective, message.tabID, 'ai-prompt')
this.messenger.sendChatInputEnabled(message.tabID, true)
this.messenger.sendUpdatePlaceholder(
message.tabID,
Expand Down Expand Up @@ -299,7 +299,7 @@ export class GumbyController {
const validProjects = await this.validateSQLConversionProjects(message)
if (validProjects.length > 0) {
this.sessionStorage.getSession().updateCandidateProjects(validProjects)
await this.messenger.sendSelectSQLMetadataFileMessage(message.tabID)
this.messenger.sendSelectSQLMetadataFileMessage(message.tabID)
}
})
.catch((err) => {
Expand Down Expand Up @@ -383,6 +383,18 @@ export class GumbyController {
case ButtonActions.SELECT_SQL_CONVERSION_METADATA_FILE:
await this.processMetadataFile(message)
break
case ButtonActions.SELECT_CUSTOM_DEPENDENCY_VERSION_FILE:
await this.processCustomDependencyVersionFile(message)
break
case ButtonActions.CONTINUE_TRANSFORMATION_FORM:
this.messenger.sendMessage(
CodeWhispererConstants.continueWithoutYamlMessage,
message.tabID,
'ai-prompt'
)
transformByQState.setCustomDependencyVersionFilePath('')
this.promptJavaHome('source', message.tabID)
break
case ButtonActions.VIEW_TRANSFORMATION_HUB:
await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB, CancelActionPositions.Chat)
break
Expand All @@ -403,7 +415,7 @@ export class GumbyController {
await this.continueJobWithSelectedDependency(message)
break
case ButtonActions.CANCEL_DEPENDENCY_FORM:
this.messenger.sendUserPrompt('Cancel', message.tabID)
this.messenger.sendMessage('Cancel', message.tabID, 'prompt')
await this.continueTransformationWithoutHIL(message)
break
case ButtonActions.OPEN_FILE:
Expand Down Expand Up @@ -448,11 +460,27 @@ export class GumbyController {
})

this.messenger.sendOneOrMultipleDiffsMessage(oneOrMultipleDiffsSelection, message.tabID)
// perform local build
await this.validateBuildWithPromptOnError(message)
this.promptJavaHome('source', message.tabID)
// TO-DO: delete line above and uncomment line below when releasing CSB
// await this.messenger.sendCustomDependencyVersionMessage(message.tabID)
})
}

private promptJavaHome(type: 'source' | 'target', tabID: any) {
let jdkVersion = undefined
if (type === 'source') {
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_SOURCE_JAVA_HOME
jdkVersion = transformByQState.getSourceJDKVersion()
} else if (type === 'target') {
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_TARGET_JAVA_HOME
jdkVersion = transformByQState.getTargetJDKVersion()
}
const message = MessengerUtils.createJavaHomePrompt(jdkVersion)
this.messenger.sendMessage(message, tabID, 'ai-prompt')
this.messenger.sendChatInputEnabled(tabID, true)
this.messenger.sendUpdatePlaceholder(tabID, CodeWhispererConstants.enterJavaHomePlaceholder)
}

private async handleUserLanguageUpgradeProjectChoice(message: any) {
await telemetry.codeTransform_submitSelection.run(async () => {
const pathToProject: string = message.formSelectedValues['GumbyTransformLanguageUpgradeProjectForm']
Expand Down Expand Up @@ -521,66 +549,59 @@ export class GumbyController {
})
}

private async prepareLanguageUpgradeProject(message: { pathToJavaHome: string; tabID: string }) {
if (message.pathToJavaHome) {
transformByQState.setJavaHome(message.pathToJavaHome)
getLogger().info(
`CodeTransformation: using JAVA_HOME = ${transformByQState.getJavaHome()} since source JDK does not match Maven JDK`
)
}

// Pre-build project locally
private async prepareLanguageUpgradeProject(tabID: string) {
// build project locally
try {
this.sessionStorage.getSession().conversationState = ConversationState.COMPILING
this.messenger.sendCompilationInProgress(message.tabID)
this.messenger.sendCompilationInProgress(tabID)
await compileProject()
} catch (err: any) {
this.messenger.sendUnrecoverableErrorResponse('could-not-compile-project', message.tabID)
this.messenger.sendUnrecoverableErrorResponse('could-not-compile-project', tabID)
// reset state to allow "Start a new transformation" button to work
this.sessionStorage.getSession().conversationState = ConversationState.IDLE
throw err
}

this.messenger.sendCompilationFinished(message.tabID)
this.messenger.sendCompilationFinished(tabID)

// since compilation can potentially take a long time, double check auth
const authState = await AuthUtil.instance.getChatAuthState()
if (authState.amazonQ !== 'connected') {
void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID)
void this.messenger.sendAuthNeededExceptionMessage(authState, tabID)
this.sessionStorage.getSession().isAuthenticating = true
return
}

// give user a non-blocking warning if build file appears to contain absolute paths
await parseBuildFile()

this.messenger.sendAsyncEventProgress(
message.tabID,
true,
undefined,
GumbyNamedMessages.JOB_SUBMISSION_STATUS_MESSAGE
)
this.messenger.sendJobSubmittedMessage(message.tabID)
this.messenger.sendAsyncEventProgress(tabID, true, undefined, GumbyNamedMessages.JOB_SUBMISSION_STATUS_MESSAGE)
this.messenger.sendJobSubmittedMessage(tabID)
this.sessionStorage.getSession().conversationState = ConversationState.JOB_SUBMITTED
await startTransformByQ()
}

// only for Language Upgrades
private async validateBuildWithPromptOnError(message: any | undefined = undefined): Promise<void> {
try {
// Check Java Home is set (not yet prebuilding)
await validateCanCompileProject()
} catch (err: any) {
if (err instanceof JavaHomeNotSetError) {
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_JAVA_HOME
this.messenger.sendStaticTextResponse('java-home-not-set', message.tabID)
this.messenger.sendChatInputEnabled(message.tabID, true)
this.messenger.sendUpdatePlaceholder(message.tabID, 'Enter the path to your Java installation.')
}
private async processCustomDependencyVersionFile(message: any) {
const fileUri = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: 'Select',
filters: {
'YAML file': ['yaml'], // restrict user to only pick a .yaml file
},
})
if (!fileUri || fileUri.length === 0) {
return
}
const fileContents = await fs.readFileText(fileUri[0].fsPath)
const isValidFile = await validateCustomVersionsFile(fileContents)

await this.prepareLanguageUpgradeProject(message)
if (!isValidFile) {
this.messenger.sendUnrecoverableErrorResponse('invalid-custom-versions-file', message.tabID)
return
}
this.messenger.sendMessage('Received custom dependency version YAML file.', message.tabID, 'ai-prompt')
transformByQState.setCustomDependencyVersionFilePath(fileUri[0].fsPath)
this.promptJavaHome('source', message.tabID)
}

private async processMetadataFile(message: any) {
Expand Down Expand Up @@ -657,19 +678,34 @@ export class GumbyController {
}

private async processHumanChatMessage(data: { message: string; tabID: string }) {
this.messenger.sendUserPrompt(data.message, data.tabID)
this.messenger.sendMessage(data.message, data.tabID, 'prompt')
this.messenger.sendChatInputEnabled(data.tabID, false)
this.messenger.sendUpdatePlaceholder(data.tabID, 'Open a new tab to chat with Q')
this.messenger.sendUpdatePlaceholder(data.tabID, CodeWhispererConstants.openNewTabPlaceholder)

const session = this.sessionStorage.getSession()
switch (session.conversationState) {
case ConversationState.PROMPT_JAVA_HOME: {
case ConversationState.PROMPT_SOURCE_JAVA_HOME: {
const pathToJavaHome = extractPath(data.message)
if (pathToJavaHome) {
await this.prepareLanguageUpgradeProject({
pathToJavaHome,
tabID: data.tabID,
})
transformByQState.setSourceJavaHome(pathToJavaHome)
// if source and target JDK versions are the same, just re-use the source JAVA_HOME and start the build
if (transformByQState.getTargetJDKVersion() === transformByQState.getSourceJDKVersion()) {
transformByQState.setTargetJavaHome(pathToJavaHome)
await this.prepareLanguageUpgradeProject(data.tabID)
} else {
this.promptJavaHome('target', data.tabID)
}
} else {
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
}
break
}

case ConversationState.PROMPT_TARGET_JAVA_HOME: {
const pathToJavaHome = extractPath(data.message)
if (pathToJavaHome) {
transformByQState.setTargetJavaHome(pathToJavaHome)
await this.prepareLanguageUpgradeProject(data.tabID) // build right after we get target JDK path
} else {
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
}
Expand Down Expand Up @@ -747,7 +783,7 @@ export class GumbyController {
})
}

this.messenger.sendStaticTextResponse('end-HIL-early', message.tabID)
this.messenger.sendMessage(CodeWhispererConstants.continueWithoutHilMessage, message.tabID, 'ai-prompt')
}
}

Expand Down
Loading
Loading