Skip to content

Commit ee55571

Browse files
Copilotedburns
andauthored
Address PR review feedback on cleanup and tests
Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/38b90523-b5be-4dbd-9d33-36361110399b Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 0a2157c commit ee55571

4 files changed

Lines changed: 66 additions & 18 deletions

File tree

src/main/java/com/github/copilot/sdk/CliServerManager.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,7 @@ private Integer waitForPortAnnouncement(Process process) throws IOException {
230230
if (line == null) {
231231
awaitStderrReader();
232232
String stderr = getStderrOutput();
233-
throw new IOException(
234-
CopilotClient.formatCliExitedMessage("CLI process exited unexpectedly.", stderr));
233+
throw new IOException(formatCliExitedMessage("CLI process exited unexpectedly.", stderr));
235234
}
236235

237236
Matcher matcher = portPattern.matcher(line);
@@ -268,6 +267,13 @@ private void clearStderrBuffer() {
268267
}
269268
}
270269

270+
static String formatCliExitedMessage(String message, String stderrOutput) {
271+
if (stderrOutput == null || stderrOutput.isEmpty()) {
272+
return message;
273+
}
274+
return message + "\nstderr: " + stderrOutput;
275+
}
276+
271277
private List<String> resolveCliCommand(String cliPath, List<String> args) {
272278
boolean isJsFile = cliPath.toLowerCase().endsWith(".js");
273279

src/main/java/com/github/copilot/sdk/CopilotClient.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public final class CopilotClient implements AutoCloseable {
7676
* shutdown via {@link #stop()}.
7777
*/
7878
public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10;
79+
private static final long FORCE_KILL_TIMEOUT_SECONDS = 10;
7980
private final CopilotClientOptions options;
8081
private final CliServerManager serverManager;
8182
private final LifecycleEventManager lifecycleManager = new LifecycleEventManager();
@@ -216,20 +217,13 @@ private Connection startCoreBody() {
216217
} catch (Exception e) {
217218
String stderr = serverManager.getStderrOutput();
218219
if (!stderr.isEmpty()) {
219-
throw new CompletionException(
220-
new IOException(formatCliExitedMessage("CLI process exited unexpectedly.", stderr), e));
220+
throw new CompletionException(new IOException(
221+
CliServerManager.formatCliExitedMessage("CLI process exited unexpectedly.", stderr), e));
221222
}
222223
throw new CompletionException(e);
223224
}
224225
}
225226

226-
static String formatCliExitedMessage(String message, String stderrOutput) {
227-
if (stderrOutput == null || stderrOutput.isEmpty()) {
228-
return message;
229-
}
230-
return message + "\nstderr: " + stderrOutput;
231-
}
232-
233227
private static final int MIN_PROTOCOL_VERSION = 2;
234228
private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601;
235229

@@ -360,8 +354,14 @@ private CompletableFuture<Void> cleanupConnection() {
360354
if (connection.process != null) {
361355
try {
362356
if (connection.process.isAlive()) {
363-
connection.process.destroyForcibly().waitFor();
357+
Process destroyedProcess = connection.process.destroyForcibly();
358+
if (!destroyedProcess.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
359+
LOG.fine("Process did not terminate within force kill timeout");
360+
}
364361
}
362+
} catch (InterruptedException e) {
363+
Thread.currentThread().interrupt();
364+
LOG.log(Level.FINE, "Interrupted while killing process", e);
365365
} catch (Exception e) {
366366
LOG.log(Level.FINE, "Error killing process", e);
367367
}

src/test/java/com/github/copilot/sdk/PermissionsTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,8 @@ void testShouldWaitForSlowPermissionHandler() throws Exception {
456456
v -> new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED));
457457
})).get();
458458

459-
// Use send (non-blocking) so we can interact with the handler
459+
// Capture the sendAndWait future before awaiting it so we can interact with the
460+
// handler
460461
CompletableFuture<AssistantMessageEvent> responseFuture = session
461462
.sendAndWait(new MessageOptions().setPrompt("Run 'echo slow_handler_test'"));
462463

src/test/java/com/github/copilot/sdk/ToolsTest.java

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import java.util.List;
1414
import java.util.Map;
1515
import java.util.concurrent.CompletableFuture;
16+
import java.util.concurrent.CountDownLatch;
1617
import java.util.concurrent.TimeUnit;
18+
import java.util.concurrent.atomic.AtomicBoolean;
19+
import java.util.concurrent.atomic.AtomicInteger;
1720

1821
import org.junit.jupiter.api.AfterAll;
1922
import 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

Comments
 (0)