1313import java .util .List ;
1414import java .util .Map ;
1515import java .util .concurrent .CompletableFuture ;
16+ import java .util .concurrent .CountDownLatch ;
1617import java .util .concurrent .TimeUnit ;
18+ import java .util .concurrent .atomic .AtomicBoolean ;
19+ import java .util .concurrent .atomic .AtomicInteger ;
1720
1821import org .junit .jupiter .api .AfterAll ;
1922import org .junit .jupiter .api .BeforeAll ;
@@ -374,6 +377,10 @@ void testShouldExecuteMultipleCustomToolsInParallelSingleTurn() throws Exception
374377
375378 var toolACalled = new CompletableFuture <String >();
376379 var toolBCalled = new CompletableFuture <String >();
380+ var handlersStarted = new CountDownLatch (2 );
381+ var releaseHandlers = new CountDownLatch (1 );
382+ var activeHandlers = new AtomicInteger ();
383+ var handlersOverlapped = new AtomicBoolean (false );
377384
378385 Map <String , Object > cityParams = Map .of ("type" , "object" , "properties" ,
379386 Map .of ("city" , Map .of ("type" , "string" , "description" , "City name" )), "required" , List .of ("city" ));
@@ -385,28 +392,36 @@ void testShouldExecuteMultipleCustomToolsInParallelSingleTurn() throws Exception
385392 (invocation ) -> {
386393 String city = (String ) invocation .getArguments ().get ("city" );
387394 toolACalled .complete (city );
388- return CompletableFuture .completedFuture ("CITY_" + city .toUpperCase ());
395+ return executeParallelHandler (city , "CITY_" , handlersStarted , releaseHandlers , activeHandlers ,
396+ handlersOverlapped );
389397 });
390398
391399 ToolDefinition lookupCountry = ToolDefinition .create ("lookup_country" , "Looks up country information" ,
392400 countryParams , (invocation ) -> {
393401 String country = (String ) invocation .getArguments ().get ("country" );
394402 toolBCalled .complete (country );
395- return CompletableFuture .completedFuture ("COUNTRY_" + country .toUpperCase ());
403+ return executeParallelHandler (country , "COUNTRY_" , handlersStarted , releaseHandlers , activeHandlers ,
404+ handlersOverlapped );
396405 });
397406
398407 try (CopilotClient client = ctx .createClient ()) {
399408 CopilotSession session = client .createSession (new SessionConfig ()
400409 .setTools (List .of (lookupCity , lookupCountry )).setOnPermissionRequest (PermissionHandler .APPROVE_ALL ))
401410 .get ();
402411
403- AssistantMessageEvent response = session .sendAndWait (new MessageOptions ().setPrompt (
404- "Use lookup_city with 'Paris' and lookup_country with 'France' at the same time, then combine both results in your reply." ))
405- .get (60 , TimeUnit .SECONDS );
412+ CompletableFuture <AssistantMessageEvent > responseFuture = session
413+ .sendAndWait (new MessageOptions ().setPrompt (
414+ "Use lookup_city with 'Paris' and lookup_country with 'France' at the same time, then combine both results in your reply." ));
415+
416+ assertTrue (handlersStarted .await (10 , TimeUnit .SECONDS ), "Both tool handlers should start" );
417+ releaseHandlers .countDown ();
418+
419+ AssistantMessageEvent response = responseFuture .get (60 , TimeUnit .SECONDS );
406420
407421 // Both tools should have been called
408422 assertEquals ("Paris" , toolACalled .get (10 , TimeUnit .SECONDS ));
409423 assertEquals ("France" , toolBCalled .get (10 , TimeUnit .SECONDS ));
424+ assertTrue (handlersOverlapped .get (), "Tool handlers should overlap in execution" );
410425
411426 assertNotNull (response );
412427 String content = response .getData ().content ();
@@ -417,6 +432,32 @@ void testShouldExecuteMultipleCustomToolsInParallelSingleTurn() throws Exception
417432 }
418433 }
419434
435+ private CompletableFuture <Object > executeParallelHandler (String value , String prefix ,
436+ CountDownLatch handlersStarted , CountDownLatch releaseHandlers , AtomicInteger activeHandlers ,
437+ AtomicBoolean handlersOverlapped ) {
438+ int currentActive = activeHandlers .incrementAndGet ();
439+ if (currentActive > 1 ) {
440+ handlersOverlapped .set (true );
441+ }
442+
443+ handlersStarted .countDown ();
444+ try {
445+ if (!handlersStarted .await (10 , TimeUnit .SECONDS )) {
446+ return CompletableFuture .failedFuture (new IllegalStateException ("Tool handlers did not overlap" ));
447+ }
448+ if (!releaseHandlers .await (10 , TimeUnit .SECONDS )) {
449+ return CompletableFuture
450+ .failedFuture (new IllegalStateException ("Timed out waiting to release handlers" ));
451+ }
452+ return CompletableFuture .completedFuture (prefix + value .toUpperCase ());
453+ } catch (InterruptedException e ) {
454+ Thread .currentThread ().interrupt ();
455+ return CompletableFuture .failedFuture (e );
456+ } finally {
457+ activeHandlers .decrementAndGet ();
458+ }
459+ }
460+
420461 /**
421462 * Verifies that excludedTools are respected even when also listed in
422463 * availableTools.
0 commit comments