Skip to content

Commit d8f785c

Browse files
authored
test(amazonq): add integ tests for built-in tools (aws#1952)
1 parent d3b65d0 commit d8f785c

File tree

5 files changed

+385
-23
lines changed

5 files changed

+385
-23
lines changed

integration-tests/q-agentic-chat-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
"chai": "^4.3.7",
2121
"chai-as-promised": "^7.1.1",
2222
"jose": "^5.10.0",
23+
"json-rpc-2.0": "^1.7.1",
2324
"mocha": "^11.0.1",
24-
"ts-lsp-client": "^1.0.3",
2525
"typescript": "^5.0.0",
2626
"yauzl-promise": "^4.0.0"
2727
}

integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts

Lines changed: 251 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import * as chaiAsPromised from 'chai-as-promised'
44
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
55
import { describe } from 'node:test'
66
import * as path from 'path'
7-
import { JSONRPCEndpoint, LspClient } from 'ts-lsp-client'
7+
import { JSONRPCEndpoint, LspClient } from './lspClient'
88
import { pathToFileURL } from 'url'
99
import * as crypto from 'crypto'
1010
import { EncryptionInitialization } from '@aws/lsp-core'
1111
import { authenticateServer, decryptObjectWithKey, encryptObjectWithKey } from './testUtils'
1212
import { ChatParams, ChatResult } from '@aws/language-server-runtimes/protocol'
13+
import * as fs from 'fs'
1314

1415
chai.use(chaiAsPromised)
1516

@@ -26,6 +27,11 @@ describe('Q Agentic Chat Server Integration Tests', async () => {
2627
let testSsoStartUrl: string
2728
let testProfileArn: string
2829

30+
let tabId: string
31+
let partialResultToken: string
32+
33+
let serverLogs: string[] = []
34+
2935
before(async () => {
3036
testSsoToken = process.env.TEST_SSO_TOKEN || ''
3137
testSsoStartUrl = process.env.TEST_SSO_START_URL || ''
@@ -50,15 +56,21 @@ describe('Q Agentic Chat Server Integration Tests', async () => {
5056
stdio: 'pipe',
5157
}
5258
)
59+
serverProcess.stdout.on('data', (data: Buffer) => {
60+
const message = data.toString()
61+
if (process.env.DEBUG) {
62+
console.log(message)
63+
}
64+
serverLogs.push(message)
65+
})
5366

54-
if (process.env.DEBUG) {
55-
serverProcess.stdout.on('data', data => {
56-
console.log(data.toString())
57-
})
58-
serverProcess.stderr.on('data', data => {
59-
console.error(data.toString())
60-
})
61-
}
67+
serverProcess.stderr.on('data', (data: Buffer) => {
68+
const message = data.toString()
69+
if (process.env.DEBUG) {
70+
console.error(message)
71+
}
72+
serverLogs.push(`STDERR: ${message}`)
73+
})
6274

6375
encryptionKey = Buffer.from(crypto.randomBytes(32)).toString('base64')
6476
const encryptionDetails: EncryptionInitialization = {
@@ -113,20 +125,248 @@ describe('Q Agentic Chat Server Integration Tests', async () => {
113125
expect(result.capabilities).to.exist
114126
})
115127

128+
beforeEach(() => {
129+
tabId = crypto.randomUUID()
130+
partialResultToken = crypto.randomUUID()
131+
})
132+
116133
after(async () => {
117134
client.exit()
118135
})
119136

137+
afterEach(function (this: Mocha.Context) {
138+
if (this.currentTest?.state === 'failed') {
139+
console.log('\n=== SERVER LOGS ON FAILURE ===')
140+
console.log(serverLogs.join(''))
141+
console.log('=== END SERVER LOGS ===\n')
142+
}
143+
serverLogs = []
144+
})
145+
120146
it('responds to chat prompt', async () => {
121147
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
122-
{ tabId: 'tab-id', prompt: { prompt: 'Hello' } },
148+
{ tabId, prompt: { prompt: 'Hello' } },
123149
encryptionKey
124150
)
125-
const result = await endpoint.send('aws/chat/sendChatPrompt', { message: encryptedMessage })
151+
const result = await client.sendChatPrompt({ message: encryptedMessage })
126152
const decryptedResult = await decryptObjectWithKey<ChatResult>(result, encryptionKey)
127153

128154
expect(decryptedResult).to.have.property('messageId')
129155
expect(decryptedResult).to.have.property('body')
130156
expect(decryptedResult.body).to.not.be.empty
131157
})
158+
159+
it('reads file contents using fsRead tool', async () => {
160+
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
161+
{
162+
tabId,
163+
prompt: { prompt: 'Read the contents of the test.py file using the fsRead tool.' },
164+
},
165+
encryptionKey
166+
)
167+
const result = await client.sendChatPrompt({ message: encryptedMessage })
168+
const decryptedResult = await decryptObjectWithKey<ChatResult>(result, encryptionKey)
169+
170+
expect(decryptedResult.additionalMessages).to.be.an('array')
171+
const fsReadMessage = decryptedResult.additionalMessages?.find(
172+
msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 file read'
173+
)
174+
expect(fsReadMessage).to.exist
175+
expect(fsReadMessage?.fileList?.filePaths).to.include.members([path.join(rootPath, 'test.py')])
176+
expect(fsReadMessage?.messageId?.startsWith('tooluse_')).to.be.true
177+
})
178+
179+
it('lists directory contents using listDirectory tool', async () => {
180+
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
181+
{
182+
tabId,
183+
prompt: { prompt: 'List the contents of the current directory using the listDirectory tool.' },
184+
},
185+
encryptionKey
186+
)
187+
const result = await client.sendChatPrompt({ message: encryptedMessage })
188+
const decryptedResult = await decryptObjectWithKey<ChatResult>(result, encryptionKey)
189+
190+
expect(decryptedResult.additionalMessages).to.be.an('array')
191+
const listDirectoryMessage = decryptedResult.additionalMessages?.find(
192+
msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory listed'
193+
)
194+
expect(listDirectoryMessage).to.exist
195+
expect(listDirectoryMessage?.fileList?.filePaths).to.include.members([rootPath])
196+
expect(listDirectoryMessage?.messageId?.startsWith('tooluse_')).to.be.true
197+
})
198+
199+
it('executes bash command using executeBash tool', async () => {
200+
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
201+
{
202+
tabId,
203+
prompt: { prompt: 'Execute ls command using the executeBash tool.' },
204+
},
205+
encryptionKey
206+
)
207+
const result = await client.sendChatPrompt({ message: encryptedMessage })
208+
const decryptedResult = await decryptObjectWithKey<ChatResult>(result, encryptionKey)
209+
210+
expect(decryptedResult.additionalMessages).to.be.an('array')
211+
const executeBashMessage = decryptedResult.additionalMessages?.find(
212+
msg => msg.type === 'tool' && msg.body?.startsWith('```') && msg.body?.endsWith('```')
213+
)
214+
expect(executeBashMessage).to.exist
215+
expect(executeBashMessage?.body).to.include('test.py')
216+
expect(executeBashMessage?.body).to.include('test.ts')
217+
})
218+
219+
it('waits for user acceptance when executing mutable bash commands', async () => {
220+
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
221+
{
222+
tabId,
223+
prompt: {
224+
prompt: `Run this command using the executeBash tool: \`date > timestamp.txt && echo "Timestamp saved"\``,
225+
},
226+
},
227+
encryptionKey
228+
)
229+
230+
const toolUseIdPromise = new Promise<string>((resolve, reject) => {
231+
const timeout = setTimeout(() => {
232+
reject(new Error('Timeout waiting for executeBash tool use ID'))
233+
}, 10000) // 10 second timeout
234+
235+
const dataHandler = async (data: Buffer) => {
236+
const message = data.toString()
237+
try {
238+
const jsonRegex = /\{"jsonrpc":"2\.0".*?\}(?=\n|$)/g
239+
const matches = message.match(jsonRegex) ?? []
240+
for (const match of matches) {
241+
const obj = JSON.parse(match)
242+
if (obj.method !== '$/progress' || obj.params.token !== partialResultToken) {
243+
continue
244+
}
245+
const decryptedValue = await decryptObjectWithKey<ChatResult>(obj.params.value, encryptionKey)
246+
const executeBashMessage = decryptedValue.additionalMessages?.find(
247+
m => m.type === 'tool' && m.header?.body === 'shell'
248+
)
249+
if (!executeBashMessage?.messageId) {
250+
continue
251+
}
252+
resolve(executeBashMessage.messageId)
253+
serverProcess.stdout.removeListener('data', dataHandler)
254+
clearTimeout(timeout)
255+
}
256+
} catch (err) {
257+
// Continue even if regex matching fails
258+
}
259+
}
260+
serverProcess.stdout.on('data', dataHandler)
261+
})
262+
263+
// Start the chat but don't await it yet
264+
const chatPromise = client.sendChatPrompt({ message: encryptedMessage, partialResultToken })
265+
const toolUseId = await toolUseIdPromise
266+
267+
// Simulate button click
268+
const buttonClickResult = await client.buttonClick({
269+
tabId,
270+
buttonId: 'run-shell-command',
271+
messageId: toolUseId,
272+
})
273+
expect(buttonClickResult.success).to.be.true
274+
275+
const chatResult = await chatPromise
276+
const decryptedResult = await decryptObjectWithKey<ChatResult>(chatResult, encryptionKey)
277+
278+
expect(decryptedResult.additionalMessages).to.be.an('array')
279+
const executeBashMessage = decryptedResult.additionalMessages?.find(
280+
msg => msg.type === 'tool' && msg.messageId === toolUseId
281+
)
282+
expect(executeBashMessage).to.exist
283+
expect(executeBashMessage?.body).to.include('Timestamp saved')
284+
})
285+
286+
it('writes to a file using fsWrite tool', async () => {
287+
const fileName = 'testWrite.txt'
288+
const filePath = path.join(rootPath, fileName)
289+
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
290+
{
291+
tabId,
292+
prompt: { prompt: `Write "Hello World" to ${filePath} using the fsWrite tool.` },
293+
},
294+
encryptionKey
295+
)
296+
const result = await client.sendChatPrompt({ message: encryptedMessage })
297+
const decryptedResult = await decryptObjectWithKey<ChatResult>(result, encryptionKey)
298+
299+
expect(decryptedResult.additionalMessages).to.be.an('array')
300+
const fsWriteMessage = decryptedResult.additionalMessages?.find(
301+
msg => msg.type === 'tool' && msg.header?.buttons?.[0].id === 'undo-changes'
302+
)
303+
expect(fsWriteMessage).to.exist
304+
expect(fsWriteMessage?.messageId?.startsWith('tooluse_')).to.be.true
305+
expect(fsWriteMessage?.header?.fileList?.filePaths).to.include.members([fileName])
306+
expect(fsWriteMessage?.header?.fileList?.details?.[fileName]?.changes).to.deep.equal({ added: 1, deleted: 0 })
307+
expect(fsWriteMessage?.header?.fileList?.details?.[fileName]?.description).to.equal(filePath)
308+
309+
// Verify the file was created
310+
expect(fs.existsSync(filePath)).to.be.true
311+
fs.rmSync(filePath, { force: true }) // Clean up the file after test
312+
})
313+
314+
it('replaces file content using fsReplace tool', async () => {
315+
const fileName = 'testReplace.txt'
316+
const filePath = path.join(rootPath, fileName)
317+
const originalContent = 'Hello World\nThis is a test file\nEnd of file'
318+
319+
// Create initial file
320+
fs.writeFileSync(filePath, originalContent)
321+
322+
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
323+
{
324+
tabId,
325+
prompt: {
326+
prompt: `Replace "Hello World" with "Goodbye World" and "test file" with "sample file" in ${filePath} using the fsReplace tool.`,
327+
},
328+
},
329+
encryptionKey
330+
)
331+
const result = await client.sendChatPrompt({ message: encryptedMessage })
332+
const decryptedResult = await decryptObjectWithKey<ChatResult>(result, encryptionKey)
333+
334+
expect(decryptedResult.additionalMessages).to.be.an('array')
335+
const fsReplaceMessage = decryptedResult.additionalMessages?.find(
336+
msg => msg.type === 'tool' && msg.header?.buttons?.[0].id === 'undo-changes'
337+
)
338+
expect(fsReplaceMessage).to.exist
339+
expect(fsReplaceMessage?.messageId?.startsWith('tooluse_')).to.be.true
340+
expect(fsReplaceMessage?.header?.fileList?.filePaths).to.include.members([fileName])
341+
expect(fsReplaceMessage?.header?.fileList?.details?.[fileName]?.description).to.equal(filePath)
342+
343+
// Verify the file content was replaced
344+
const updatedContent = fs.readFileSync(filePath, 'utf8')
345+
expect(updatedContent).to.include('Goodbye World')
346+
expect(updatedContent).to.include('sample file')
347+
expect(updatedContent).to.not.include('Hello World')
348+
expect(updatedContent).to.not.include('test file')
349+
350+
fs.rmSync(filePath, { force: true }) // Clean up
351+
})
352+
353+
it('searches for files using fileSearch tool', async () => {
354+
const encryptedMessage = await encryptObjectWithKey<ChatParams>(
355+
{
356+
tabId,
357+
prompt: { prompt: 'Search for files with "test" in the name using the fileSearch tool.' },
358+
},
359+
encryptionKey
360+
)
361+
const result = await client.sendChatPrompt({ message: encryptedMessage })
362+
const decryptedResult = await decryptObjectWithKey<ChatResult>(result, encryptionKey)
363+
364+
expect(decryptedResult.additionalMessages).to.be.an('array')
365+
const fileSearchMessage = decryptedResult.additionalMessages?.find(
366+
msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory searched'
367+
)
368+
expect(fileSearchMessage).to.exist
369+
expect(fileSearchMessage?.messageId?.startsWith('tooluse_')).to.be.true
370+
expect(fileSearchMessage?.fileList?.filePaths).to.include.members([rootPath])
371+
})
132372
})

0 commit comments

Comments
 (0)