Skip to content

Commit 8b1b4ad

Browse files
feat(amazonq): handle client signalling support for q developer profiles (#839)
1 parent 9352454 commit 8b1b4ad

File tree

8 files changed

+218
-101
lines changed

8 files changed

+218
-101
lines changed

client/vscode/src/activation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ export async function activateDocumentsLanguageServer(extensionContext: Extensio
153153
clientId: randomUUID(),
154154
},
155155
awsClientCapabilities: {
156+
q: {
157+
developerProfiles: false,
158+
},
156159
window: {
157160
notifications: true,
158161
},

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/aws-lsp-codewhisperer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@amzn/codewhisperer-streaming": "file:../../core/codewhisperer-streaming/amzn-codewhisperer-streaming-1.0.0.tgz",
3131
"@aws-sdk/util-retry": "^3.374.0",
3232
"@aws/chat-client-ui-types": "^0.1.5",
33-
"@aws/language-server-runtimes": "^0.2.40",
33+
"@aws/language-server-runtimes": "^0.2.43",
3434
"@aws/lsp-core": "^0.0.1",
3535
"@smithy/node-http-handler": "^2.5.0",
3636
"adm-zip": "^0.5.10",

server/aws-lsp-codewhisperer/src/language-server/amazonQServiceManager/qDeveloperProfiles.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import * as assert from 'assert'
22
import { StubbedInstance, stubInterface } from 'ts-sinon'
33
import { CodeWhispererServiceToken } from '../codeWhispererService'
44
import { SsoConnectionType } from '../utils'
5-
import { Logging } from '@aws/language-server-runtimes/server-interface'
5+
import { AWSInitializationOptions, Logging } from '@aws/language-server-runtimes/server-interface'
66
import {
77
AmazonQDeveloperProfile,
88
getListAllAvailableProfilesHandler,
99
ListAllAvailableProfilesHandler,
10+
signalsAWSQDeveloperProfilesEnabled,
1011
} from './qDeveloperProfiles'
1112
import { DEFAULT_AWS_Q_ENDPOINT_URL, DEFAULT_AWS_Q_REGION } from '../../constants'
13+
import { isBool, isObject } from '../utils'
1214

1315
const SOME_Q_DEVELOPER_PROFILE_ARN = 'some-random-q-developer-profile-arn'
1416
const SOME_Q_DEVELOPER_PROFILE_NAME = 'some-random-q-developer-profile-name'
@@ -123,3 +125,31 @@ describe('ListAllAvailableProfiles Handler', () => {
123125
})
124126
})
125127
})
128+
129+
describe('signalsAWSQDeveloperProfilesEnabled', () => {
130+
const makeQCapability = (value?: any) => {
131+
return value !== undefined ? { developerProfiles: value } : {}
132+
}
133+
134+
const makeInitOptions = (value?: any): AWSInitializationOptions => {
135+
return { awsClientCapabilities: { q: makeQCapability(value) } }
136+
}
137+
138+
const TEST_CASES: { input: AWSInitializationOptions; expected: boolean }[] = [
139+
{ input: {}, expected: false },
140+
{ input: { awsClientCapabilities: {} }, expected: false },
141+
{ input: makeInitOptions(), expected: false },
142+
{ input: makeInitOptions([]), expected: false },
143+
{ input: makeInitOptions({}), expected: false },
144+
{ input: makeInitOptions(42), expected: false },
145+
{ input: makeInitOptions('some-string'), expected: false },
146+
{ input: makeInitOptions(false), expected: false },
147+
{ input: makeInitOptions(true), expected: true },
148+
]
149+
150+
TEST_CASES.forEach(testCase => {
151+
it(`should return: ${testCase.expected} when passed: ${JSON.stringify(testCase.input)}`, () => {
152+
assert.strictEqual(signalsAWSQDeveloperProfilesEnabled(testCase.input), testCase.expected)
153+
})
154+
})
155+
})

server/aws-lsp-codewhisperer/src/language-server/amazonQServiceManager/qDeveloperProfiles.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { Logging, LSPErrorCodes, ResponseError } from '@aws/language-server-runtimes/server-interface'
2-
import { SsoConnectionType } from '../utils'
1+
import {
2+
AWSInitializationOptions,
3+
Logging,
4+
LSPErrorCodes,
5+
ResponseError,
6+
} from '@aws/language-server-runtimes/server-interface'
7+
import { isBool, isObject, SsoConnectionType } from '../utils'
38
import { AWS_Q_ENDPOINTS } from '../../constants'
49
import { CodeWhispererServiceToken } from '../codeWhispererService'
510

@@ -95,3 +100,36 @@ async function fetchProfilesFromRegion(
95100
throw error
96101
}
97102
}
103+
104+
const AWSQCapabilitiesKey = 'q'
105+
const developerProfilesEnabledKey = 'developerProfiles'
106+
107+
/**
108+
* @returns true if AWSInitializationOptions has the Q developer profiles flag set explicitly to true
109+
*
110+
* @example
111+
* The function expects to receive the following structure:
112+
* ```ts
113+
* {
114+
* awsClientCapabilities?: {
115+
* q?: {
116+
* developerProfiles?: boolean
117+
* }
118+
* }
119+
* }
120+
* ```
121+
*/
122+
export function signalsAWSQDeveloperProfilesEnabled(initializationOptions: AWSInitializationOptions): boolean {
123+
const qCapibilities = initializationOptions.awsClientCapabilities?.[AWSQCapabilitiesKey]
124+
125+
if (
126+
isObject(qCapibilities) &&
127+
!(qCapibilities instanceof Array) &&
128+
developerProfilesEnabledKey in qCapibilities &&
129+
isBool(qCapibilities[developerProfilesEnabledKey])
130+
) {
131+
return qCapibilities[developerProfilesEnabledKey]
132+
}
133+
134+
return false
135+
}

server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.test.ts

Lines changed: 110 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -11,105 +11,136 @@ import { TestFeatures } from '@aws/language-server-runtimes/testing'
1111
import { CodeWhispererServiceToken } from '../codeWhispererService'
1212
import { Server } from '@aws/language-server-runtimes/server-interface'
1313

14-
describe('QConfigurationServer', () => {
15-
describe('OnGetConfigurationFromServer', () => {
16-
let testFeatures: TestFeatures
17-
let disposeServer: () => void
18-
let listAvailableProfilesStub: sinon.SinonStub
19-
let listAvailableCustomizationsStub: sinon.SinonStub
20-
21-
beforeEach(() => {
22-
testFeatures = new TestFeatures()
23-
24-
const codeWhispererService = stubInterface<CodeWhispererServiceToken>()
25-
const configurationServerFactory: Server = QConfigurationServerToken(() => codeWhispererService)
26-
27-
listAvailableCustomizationsStub = sinon.stub(
28-
ServerConfigurationProvider.prototype,
29-
'listAvailableCustomizations'
30-
)
31-
listAvailableProfilesStub = sinon.stub(ServerConfigurationProvider.prototype, 'listAvailableProfiles')
32-
33-
disposeServer = configurationServerFactory(testFeatures)
34-
})
14+
describe('QConfigurationServerToken', () => {
15+
let testFeatures: TestFeatures
16+
let disposeServer: () => void
17+
let listAvailableProfilesStub: sinon.SinonStub
18+
let listAvailableCustomizationsStub: sinon.SinonStub
19+
let qDeveloperProfilesEnabledPropertyStub: sinon.SinonStub
20+
let qDeveloperProfilesEnabledSetterSpy: sinon.SinonSpy
21+
22+
beforeEach(() => {
23+
testFeatures = new TestFeatures()
24+
25+
const codeWhispererService = stubInterface<CodeWhispererServiceToken>()
26+
const configurationServerFactory: Server = QConfigurationServerToken(() => codeWhispererService)
27+
28+
listAvailableCustomizationsStub = sinon.stub(
29+
ServerConfigurationProvider.prototype,
30+
'listAvailableCustomizations'
31+
)
32+
listAvailableProfilesStub = sinon.stub(ServerConfigurationProvider.prototype, 'listAvailableProfiles')
33+
qDeveloperProfilesEnabledSetterSpy = sinon.spy()
34+
qDeveloperProfilesEnabledPropertyStub = sinon
35+
.stub(ServerConfigurationProvider.prototype, 'qDeveloperProfilesEnabled')
36+
.set(qDeveloperProfilesEnabledSetterSpy)
37+
38+
disposeServer = configurationServerFactory(testFeatures)
39+
})
3540

36-
afterEach(() => {
37-
sinon.restore()
38-
})
41+
afterEach(() => {
42+
sinon.restore()
43+
})
3944

40-
// WIP: temporary test case until client can signal they support developer profiles
41-
it(`calls all list methods when ${Q_CONFIGURATION_SECTION} is requested`, () => {
42-
testFeatures.lsp.extensions.onGetConfigurationFromServer.firstCall.firstArg({
43-
section: Q_CONFIGURATION_SECTION,
45+
it(`enables Q developer profiles when signalled by client`, () => {
46+
const initialize = (developerProfiles: boolean) => {
47+
testFeatures.lsp.addInitializer.firstCall.firstArg({
48+
initializationOptions: {
49+
aws: {
50+
awsClientCapabilities: {
51+
q: {
52+
developerProfiles,
53+
},
54+
},
55+
},
56+
},
4457
})
58+
}
59+
60+
initialize(false)
61+
sinon.assert.calledWith(qDeveloperProfilesEnabledSetterSpy.firstCall, false)
62+
63+
initialize(true)
64+
sinon.assert.calledWith(qDeveloperProfilesEnabledSetterSpy.secondCall, true)
65+
})
4566

46-
sinon.assert.calledOnce(listAvailableCustomizationsStub)
47-
sinon.assert.calledOnce(listAvailableProfilesStub)
67+
it(`calls all list methods when ${Q_CONFIGURATION_SECTION} is requested`, () => {
68+
testFeatures.lsp.extensions.onGetConfigurationFromServer.firstCall.firstArg({
69+
section: Q_CONFIGURATION_SECTION,
4870
})
4971

50-
it(`only calls listAvailableCustomizations when ${Q_CUSTOMIZATIONS_CONFIGURATION_SECTION} is requested`, () => {
51-
testFeatures.lsp.extensions.onGetConfigurationFromServer.firstCall.firstArg({
52-
section: Q_CUSTOMIZATIONS_CONFIGURATION_SECTION,
53-
})
72+
sinon.assert.calledOnce(listAvailableCustomizationsStub)
73+
sinon.assert.calledOnce(listAvailableProfilesStub)
74+
})
5475

55-
sinon.assert.calledOnce(listAvailableCustomizationsStub)
56-
sinon.assert.notCalled(listAvailableProfilesStub)
76+
it(`only calls listAvailableCustomizations when ${Q_CUSTOMIZATIONS_CONFIGURATION_SECTION} is requested`, () => {
77+
testFeatures.lsp.extensions.onGetConfigurationFromServer.firstCall.firstArg({
78+
section: Q_CUSTOMIZATIONS_CONFIGURATION_SECTION,
5779
})
5880

59-
it(`only calls listAvailableProfiles when ${Q_DEVELOPER_PROFILES_CONFIGURATION_SECTION} is requested`, () => {
60-
testFeatures.lsp.extensions.onGetConfigurationFromServer.firstCall.firstArg({
61-
section: Q_DEVELOPER_PROFILES_CONFIGURATION_SECTION,
62-
})
81+
sinon.assert.calledOnce(listAvailableCustomizationsStub)
82+
sinon.assert.notCalled(listAvailableProfilesStub)
83+
})
6384

64-
sinon.assert.notCalled(listAvailableCustomizationsStub)
65-
sinon.assert.calledOnce(listAvailableProfilesStub)
85+
it(`only calls listAvailableProfiles when ${Q_DEVELOPER_PROFILES_CONFIGURATION_SECTION} is requested`, () => {
86+
testFeatures.lsp.extensions.onGetConfigurationFromServer.firstCall.firstArg({
87+
section: Q_DEVELOPER_PROFILES_CONFIGURATION_SECTION,
6688
})
89+
90+
sinon.assert.notCalled(listAvailableCustomizationsStub)
91+
sinon.assert.calledOnce(listAvailableProfilesStub)
6792
})
93+
})
6894

69-
describe('ServerConfigurationProvider', () => {
70-
let serverConfigurationProvider: ServerConfigurationProvider
71-
let codeWhispererService: StubbedInstance<CodeWhispererServiceToken>
72-
let testFeatures: TestFeatures
73-
let listAvailableProfilesHandlerSpy: sinon.SinonSpy
74-
75-
beforeEach(() => {
76-
codeWhispererService = stubInterface<CodeWhispererServiceToken>()
77-
codeWhispererService.listAvailableCustomizations.resolves({
78-
customizations: [],
79-
$response: {} as any,
80-
})
95+
describe('ServerConfigurationProvider', () => {
96+
let serverConfigurationProvider: ServerConfigurationProvider
97+
let codeWhispererService: StubbedInstance<CodeWhispererServiceToken>
98+
let testFeatures: TestFeatures
99+
let listAvailableProfilesHandlerSpy: sinon.SinonSpy
100+
101+
beforeEach(() => {
102+
codeWhispererService = stubInterface<CodeWhispererServiceToken>()
103+
codeWhispererService.listAvailableCustomizations.resolves({
104+
customizations: [],
105+
$response: {} as any,
106+
})
81107

82-
testFeatures = new TestFeatures()
108+
testFeatures = new TestFeatures()
83109

84-
serverConfigurationProvider = new ServerConfigurationProvider(
85-
codeWhispererService,
86-
testFeatures.credentialsProvider,
87-
testFeatures.logging,
88-
() => codeWhispererService
89-
)
110+
serverConfigurationProvider = new ServerConfigurationProvider(
111+
codeWhispererService,
112+
testFeatures.credentialsProvider,
113+
testFeatures.logging,
114+
() => codeWhispererService
115+
)
90116

91-
listAvailableProfilesHandlerSpy = sinon.spy(
92-
serverConfigurationProvider,
93-
'listAllAvailableProfilesHandler' as keyof ServerConfigurationProvider
94-
)
95-
})
117+
listAvailableProfilesHandlerSpy = sinon.spy(
118+
serverConfigurationProvider,
119+
'listAllAvailableProfilesHandler' as keyof ServerConfigurationProvider
120+
)
121+
})
96122

97-
afterEach(() => {
98-
sinon.restore()
99-
})
123+
afterEach(() => {
124+
sinon.restore()
125+
})
100126

101-
it(`calls corresponding API when listAvailableCustomizations is invoked`, async () => {
102-
await serverConfigurationProvider.listAvailableCustomizations()
127+
it(`calls corresponding API when listAvailableCustomizations is invoked`, async () => {
128+
await serverConfigurationProvider.listAvailableCustomizations()
103129

104-
sinon.assert.calledOnce(codeWhispererService.listAvailableCustomizations)
105-
})
130+
sinon.assert.calledOnce(codeWhispererService.listAvailableCustomizations)
131+
})
106132

107-
// WIP: alter test case when client can signal they support developer profiles
108-
it(`does not use corresponding handler when listAvailableProfiles is invoked`, async () => {
109-
const result = await serverConfigurationProvider.listAvailableProfiles()
133+
it(`does not use listAvailableProfiles handler when developer profiles is disabled`, async () => {
134+
const result = await serverConfigurationProvider.listAvailableProfiles()
110135

111-
sinon.assert.notCalled(listAvailableProfilesHandlerSpy)
112-
assert.deepStrictEqual(result, [])
113-
})
136+
sinon.assert.notCalled(listAvailableProfilesHandlerSpy)
137+
assert.deepStrictEqual(result, [])
138+
})
139+
140+
it(`uses listAvailableProfiles handler when developer profiles is enabled`, async () => {
141+
serverConfigurationProvider.qDeveloperProfilesEnabled = true
142+
await serverConfigurationProvider.listAvailableProfiles()
143+
144+
sinon.assert.called(listAvailableProfilesHandlerSpy)
114145
})
115146
})

0 commit comments

Comments
 (0)