Skip to content

Commit 10de36d

Browse files
feat(amazonq): update A/B config & and region endpoints
1 parent 50fbecc commit 10de36d

File tree

6 files changed

+669
-43
lines changed

6 files changed

+669
-43
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,7 +1805,7 @@ export class AgenticChatController implements ChatHandlers {
18051805
case SemanticSearch.toolName:
18061806
const confirmation = this.#processToolConfirmation(
18071807
toolUse,
1808-
true, // TODO: Do we need to make this a variable?
1808+
true,
18091809
`About to invoke tool “${SemanticSearch.toolName}”. Do you want to proceed?`,
18101810
undefined,
18111811
SemanticSearch.toolName // Pass the original tool name here
@@ -4610,8 +4610,8 @@ export class AgenticChatController implements ChatHandlers {
46104610
codeWhispererServiceToken
46114611
.listFeatureEvaluations({ userContext })
46124612
.then(result => {
4613-
const feature = result.featureEvaluations?.find(
4614-
feature => feature.feature === 'MaestroWorkspaceContext'
4613+
const feature = result.featureEvaluations?.find(feature =>
4614+
['MaestroWorkspaceContext', 'SematicSearchTool'].includes(feature.feature)
46154615
)
46164616
if (feature) {
46174617
this.#abTestingAllocation = {
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import * as assert from 'assert'
2+
import * as sinon from 'sinon'
3+
import axios from 'axios'
4+
import { SemanticSearch, SemanticSearchParams, CodeChunkResult } from './semanticSearch'
5+
import { TestFeatures } from '@aws/language-server-runtimes/testing'
6+
import { BearerCredentials } from '@aws/language-server-runtimes/server-interface'
7+
import { WorkspaceFolderManager } from '../../../workspaceContext/workspaceFolderManager'
8+
9+
describe('SemanticSearch Tool', () => {
10+
let features: TestFeatures
11+
let semanticSearch: SemanticSearch
12+
let axiosPostStub: sinon.SinonStub
13+
let workspaceFolderManagerStub: sinon.SinonStub
14+
let mockCredentialsProvider: any
15+
let mockWorkspaceState: any
16+
17+
beforeEach(() => {
18+
features = new TestFeatures()
19+
20+
// Mock credentials provider
21+
mockCredentialsProvider = {
22+
getCredentials: sinon.stub().returns({
23+
token: 'mock-bearer-token',
24+
} as BearerCredentials),
25+
}
26+
27+
// Mock workspace state
28+
mockWorkspaceState = {
29+
webSocketClient: {
30+
isConnected: sinon.stub().returns(true),
31+
},
32+
environmentId: 'test-env-123',
33+
workspaceId: 'test-workspace-456',
34+
}
35+
36+
// Stub WorkspaceFolderManager.getInstance()
37+
workspaceFolderManagerStub = sinon.stub(WorkspaceFolderManager, 'getInstance').returns({
38+
getWorkspaceState: () => mockWorkspaceState,
39+
} as any)
40+
41+
// Stub axios.post
42+
axiosPostStub = sinon.stub(axios, 'post')
43+
44+
semanticSearch = new SemanticSearch(features.logging, mockCredentialsProvider, 'us-east-1')
45+
})
46+
47+
afterEach(() => {
48+
sinon.restore()
49+
})
50+
51+
describe('validation', () => {
52+
it('should reject empty query', async () => {
53+
await assert.rejects(
54+
semanticSearch.validate({ query: '' }),
55+
/Semantic search query cannot be empty/i,
56+
'Expected an error for empty query'
57+
)
58+
})
59+
60+
it('should reject whitespace-only query', async () => {
61+
await assert.rejects(
62+
semanticSearch.validate({ query: ' \t\n ' }),
63+
/Semantic search query cannot be empty/i,
64+
'Expected an error for whitespace-only query'
65+
)
66+
})
67+
68+
it('should accept valid query', async () => {
69+
await assert.doesNotReject(
70+
semanticSearch.validate({ query: 'valid search query' }),
71+
'Should accept valid query'
72+
)
73+
})
74+
75+
it('should accept query with programming language', async () => {
76+
await assert.doesNotReject(
77+
semanticSearch.validate({ query: 'test', programmingLanguage: 'typescript' }),
78+
'Should accept query with programming language'
79+
)
80+
})
81+
})
82+
83+
describe('error handling', () => {
84+
it('should throw error when bearer token is missing', async () => {
85+
mockCredentialsProvider.getCredentials.returns({ token: null })
86+
87+
await assert.rejects(
88+
semanticSearch.invoke({ query: 'test query' }),
89+
/Authorization failed, bearer token is not set/i,
90+
'Expected error when bearer token is missing'
91+
)
92+
})
93+
94+
it('should throw error when workspace is not connected', async () => {
95+
mockWorkspaceState.webSocketClient.isConnected.returns(false)
96+
97+
await assert.rejects(
98+
semanticSearch.invoke({ query: 'test query' }),
99+
/Remote workspace is not ready yet/i,
100+
'Expected error when workspace is not connected'
101+
)
102+
})
103+
104+
it('should throw error when environmentId is missing', async () => {
105+
mockWorkspaceState.environmentId = null
106+
107+
await assert.rejects(
108+
semanticSearch.invoke({ query: 'test query' }),
109+
/Remote workspace is not ready yet/i,
110+
'Expected error when environmentId is missing'
111+
)
112+
})
113+
114+
it('should throw error when WorkspaceFolderManager instance is null', async () => {
115+
workspaceFolderManagerStub.returns(null)
116+
117+
await assert.rejects(
118+
semanticSearch.invoke({ query: 'test query' }),
119+
/Remote workspace is not ready yet/i,
120+
'Expected error when WorkspaceFolderManager instance is null'
121+
)
122+
})
123+
124+
it('should handle axios network errors', async () => {
125+
axiosPostStub.rejects(new Error('Network error'))
126+
127+
await assert.rejects(
128+
semanticSearch.invoke({ query: 'test query' }),
129+
/Network error/i,
130+
'Expected network error to be propagated'
131+
)
132+
})
133+
})
134+
135+
describe('successful invocation', () => {
136+
const mockSemanticResults: CodeChunkResult[] = [
137+
{
138+
fileUri: '/workspace/src/main.ts',
139+
content: 'function main() { console.log("Hello World"); }',
140+
score: 0.95,
141+
},
142+
{
143+
fileUri: 'file:///workspace/src/utils.js',
144+
content: 'export function helper() { return true; }',
145+
score: 0.87,
146+
},
147+
{
148+
fileUri: 'workspace/src/config.json',
149+
content: '{ "name": "test-project" }',
150+
score: 0.72,
151+
},
152+
]
153+
154+
beforeEach(() => {
155+
axiosPostStub.resolves({
156+
data: {
157+
contextResult: {
158+
documentContext: {
159+
queryOutputMap: {
160+
SEMANTIC: mockSemanticResults,
161+
},
162+
},
163+
},
164+
},
165+
})
166+
})
167+
168+
it('should perform semantic search with basic query', async () => {
169+
const result = await semanticSearch.invoke({ query: 'test function' })
170+
171+
// Verify axios was called with correct parameters
172+
assert.ok(axiosPostStub.calledOnce, 'axios.post should be called once')
173+
174+
const [url, requestBody, config] = axiosPostStub.firstCall.args
175+
assert.strictEqual(url, 'https://test-env-123--8080.wc.q.us-east-1.amazonaws.com/getWorkspaceContext')
176+
assert.strictEqual(requestBody.workspaceId, 'test-workspace-456')
177+
assert.strictEqual(requestBody.contextParams.documentContextParams.query, 'test function')
178+
assert.strictEqual(config.headers.Authorization, 'Bearer mock-bearer-token')
179+
180+
// Verify result structure
181+
assert.strictEqual(result.output.kind, 'json')
182+
const content = result.output.content as any[]
183+
assert.strictEqual(content.length, 3)
184+
})
185+
186+
it('should include programming language filter when specified', async () => {
187+
await semanticSearch.invoke({
188+
query: 'test function',
189+
programmingLanguage: 'typescript',
190+
})
191+
192+
const [, requestBody] = axiosPostStub.firstCall.args
193+
const queryConfig = requestBody.contextParams.documentContextParams.queryConfigurationMap.SEMANTIC
194+
assert.strictEqual(queryConfig.programmingLanguage, 'typescript')
195+
})
196+
197+
it('should not include programming language when not specified', async () => {
198+
await semanticSearch.invoke({ query: 'test function' })
199+
200+
const [, requestBody] = axiosPostStub.firstCall.args
201+
const queryConfig = requestBody.contextParams.documentContextParams.queryConfigurationMap.SEMANTIC
202+
assert.ok(!('programmingLanguage' in queryConfig))
203+
})
204+
205+
it('should normalize file URIs correctly', async () => {
206+
const result = await semanticSearch.invoke({ query: 'test' })
207+
const content = result.output.content as any[]
208+
209+
// Check URI normalization
210+
assert.strictEqual(content[0].fileUri, 'file:///workspace/src/main.ts')
211+
assert.strictEqual(content[1].fileUri, 'file:///workspace/src/utils.js') // Already has file://
212+
assert.strictEqual(content[2].fileUri, 'file:///workspace/src/config.json')
213+
})
214+
215+
it('should include similarity scores when available', async () => {
216+
const result = await semanticSearch.invoke({ query: 'test' })
217+
const content = result.output.content as any[]
218+
219+
assert.strictEqual(content[0].similarityScore, 0.95)
220+
assert.strictEqual(content[1].similarityScore, 0.87)
221+
assert.strictEqual(content[2].similarityScore, 0.72)
222+
})
223+
224+
it('should handle results without scores', async () => {
225+
const resultsWithoutScores: CodeChunkResult[] = [
226+
{
227+
fileUri: '/workspace/test.js',
228+
content: 'test content',
229+
// No score property
230+
},
231+
]
232+
233+
axiosPostStub.resolves({
234+
data: {
235+
contextResult: {
236+
documentContext: {
237+
queryOutputMap: {
238+
SEMANTIC: resultsWithoutScores,
239+
},
240+
},
241+
},
242+
},
243+
})
244+
245+
const result = await semanticSearch.invoke({ query: 'test' })
246+
const content = result.output.content as any[]
247+
248+
assert.strictEqual(content.length, 1)
249+
assert.strictEqual(content[0].fileUri, 'file:///workspace/test.js')
250+
assert.strictEqual(content[0].content, 'test content')
251+
assert.ok(!('similarityScore' in content[0]))
252+
})
253+
254+
it('should handle empty search results', async () => {
255+
axiosPostStub.resolves({
256+
data: {
257+
contextResult: {
258+
documentContext: {
259+
queryOutputMap: {
260+
SEMANTIC: [],
261+
},
262+
},
263+
},
264+
},
265+
})
266+
267+
const result = await semanticSearch.invoke({ query: 'nonexistent' })
268+
const content = result.output.content as any[]
269+
270+
assert.strictEqual(content.length, 0)
271+
})
272+
273+
it('should handle missing semantic results', async () => {
274+
axiosPostStub.resolves({
275+
data: {
276+
contextResult: {
277+
documentContext: {
278+
queryOutputMap: {
279+
SEMANTIC: undefined,
280+
},
281+
},
282+
},
283+
},
284+
})
285+
286+
const result = await semanticSearch.invoke({ query: 'test' })
287+
const content = result.output.content as any[]
288+
289+
assert.strictEqual(content.length, 0)
290+
})
291+
292+
it('should handle malformed response structure', async () => {
293+
axiosPostStub.resolves({
294+
data: {
295+
// Missing expected structure
296+
},
297+
})
298+
299+
const result = await semanticSearch.invoke({ query: 'test' })
300+
const content = result.output.content as any[]
301+
302+
assert.strictEqual(content.length, 0)
303+
})
304+
})
305+
306+
describe('getSpec', () => {
307+
it('should return correct tool specification', () => {
308+
const spec = semanticSearch.getSpec()
309+
310+
assert.strictEqual(spec.name, 'serverSideSemanticSearch')
311+
assert.ok(spec.description.includes('semantic search'))
312+
assert.strictEqual(spec.inputSchema.type, 'object')
313+
assert.ok('query' in spec.inputSchema.properties)
314+
assert.ok('programmingLanguage' in spec.inputSchema.properties)
315+
assert.deepStrictEqual(spec.inputSchema.required, ['query'])
316+
})
317+
318+
it('should have correct programming language enum values', () => {
319+
const spec = semanticSearch.getSpec()
320+
const langProperty = spec.inputSchema.properties.programmingLanguage as any
321+
322+
assert.deepStrictEqual(langProperty.enum, ['java', 'python', 'javascript', 'typescript'])
323+
})
324+
})
325+
326+
describe('constructor', () => {
327+
it('should construct with correct endpoint suffix', () => {
328+
const search1 = new SemanticSearch(features.logging, mockCredentialsProvider, 'us-west-2')
329+
const search2 = new SemanticSearch(features.logging, mockCredentialsProvider, 'eu-west-1')
330+
331+
// We can't directly test the private property, but we can test the behavior
332+
// by mocking a call and checking the URL
333+
axiosPostStub.resolves({
334+
data: { contextResult: { documentContext: { queryOutputMap: { SEMANTIC: [] } } } },
335+
})
336+
337+
// Test us-west-2
338+
search1.invoke({ query: 'test' }).catch(() => {}) // Ignore validation errors
339+
// Test eu-west-1
340+
search2.invoke({ query: 'test' }).catch(() => {}) // Ignore validation errors
341+
342+
// The endpoint construction is tested indirectly through the invoke method tests above
343+
})
344+
})
345+
})

0 commit comments

Comments
 (0)