@@ -4,12 +4,13 @@ import * as chaiAsPromised from 'chai-as-promised'
44import { ChildProcessWithoutNullStreams , spawn } from 'child_process'
55import { describe } from 'node:test'
66import * as path from 'path'
7- import { JSONRPCEndpoint , LspClient } from 'ts-lsp-client '
7+ import { JSONRPCEndpoint , LspClient } from './lspClient '
88import { pathToFileURL } from 'url'
99import * as crypto from 'crypto'
1010import { EncryptionInitialization } from '@aws/lsp-core'
1111import { authenticateServer , decryptObjectWithKey , encryptObjectWithKey } from './testUtils'
1212import { ChatParams , ChatResult } from '@aws/language-server-runtimes/protocol'
13+ import * as fs from 'fs'
1314
1415chai . 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 = / \{ " j s o n r p c " : " 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