|
10 | 10 | import java.util.concurrent.CopyOnWriteArrayList; |
11 | 11 | import java.util.concurrent.CountDownLatch; |
12 | 12 | import java.util.concurrent.TimeUnit; |
| 13 | +import java.util.concurrent.atomic.AtomicBoolean; |
13 | 14 | import java.util.concurrent.atomic.AtomicReference; |
14 | 15 |
|
15 | 16 | import com.agentclientprotocol.sdk.client.AcpAsyncClient; |
@@ -514,6 +515,56 @@ void executeWithCommandBuilderWorks() throws Exception { |
514 | 515 | agent.closeGracefully(); |
515 | 516 | } |
516 | 517 |
|
| 518 | + @Test |
| 519 | + void executeReleasesTerminalBeforeReturning() throws Exception { |
| 520 | + // This test verifies the fix for the race condition where releaseTerminal |
| 521 | + // was called with fire-and-forget .subscribe() instead of being awaited |
| 522 | + AtomicBoolean releaseCalledBeforeReturn = new AtomicBoolean(false); |
| 523 | + AtomicBoolean executeReturned = new AtomicBoolean(false); |
| 524 | + |
| 525 | + AcpSyncAgent agent = AcpAgent.sync(transportPair.agentTransport()) |
| 526 | + .requestTimeout(TIMEOUT) |
| 527 | + .initializeHandler(req -> InitializeResponse.ok()) |
| 528 | + .newSessionHandler(req -> new NewSessionResponse("release-test", null, null)) |
| 529 | + .promptHandler((request, context) -> { |
| 530 | + context.execute("echo", "test"); |
| 531 | + // After execute() returns, releaseTerminal should have been called |
| 532 | + executeReturned.set(true); |
| 533 | + return PromptResponse.endTurn(); |
| 534 | + }) |
| 535 | + .build(); |
| 536 | + |
| 537 | + AcpAsyncClient client = AcpClient.async(transportPair.clientTransport()) |
| 538 | + .requestTimeout(TIMEOUT) |
| 539 | + .createTerminalHandler(req -> Mono.just(new CreateTerminalResponse("term-rel"))) |
| 540 | + .waitForTerminalExitHandler(req -> Mono.just(new WaitForTerminalExitResponse(0, null))) |
| 541 | + .terminalOutputHandler(req -> Mono.just(new TerminalOutputResponse("", false, null))) |
| 542 | + .releaseTerminalHandler(req -> { |
| 543 | + // Release should be called BEFORE execute() returns |
| 544 | + if (!executeReturned.get()) { |
| 545 | + releaseCalledBeforeReturn.set(true); |
| 546 | + } |
| 547 | + return Mono.just(new ReleaseTerminalResponse()); |
| 548 | + }) |
| 549 | + .build(); |
| 550 | + |
| 551 | + agent.start(); |
| 552 | + Thread.sleep(100); |
| 553 | + |
| 554 | + ClientCapabilities clientCaps = new ClientCapabilities(null, true); |
| 555 | + client.initialize(new InitializeRequest(1, clientCaps)).block(TIMEOUT); |
| 556 | + client.newSession(new NewSessionRequest("/workspace", List.of())).block(TIMEOUT); |
| 557 | + client.prompt(new PromptRequest("release-test", List.of(new TextContent("test")))).block(TIMEOUT); |
| 558 | + |
| 559 | + // Verify that releaseTerminal was called before execute() returned |
| 560 | + assertThat(releaseCalledBeforeReturn.get()) |
| 561 | + .as("releaseTerminal should be called before execute() returns (not fire-and-forget)") |
| 562 | + .isTrue(); |
| 563 | + |
| 564 | + client.closeGracefully().block(TIMEOUT); |
| 565 | + agent.closeGracefully(); |
| 566 | + } |
| 567 | + |
517 | 568 | // ======================================================================== |
518 | 569 | // Factory method tests |
519 | 570 | // ======================================================================== |
|
0 commit comments