Skip to content

fix: stop StdioTransportTest background sender when send is rejected during close - #138

Merged
Rizzen merged 1 commit into
masterfrom
aefimov/fix/stdio-transport-test-close-race
Oct 1, 2026
Merged

Rizzen merged 1 commit into
masterfrom
aefimov/fix/stdio-transport-test-close-race

Conversation

@wayfarer-rus

@wayfarer-rus wayfarer-rus commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

master CI fails at random in StdioTransportTest with kotlinx.coroutines.channels.ClosedSendChannelException. The tests that fail all use the testWhileBackgroundSend helper:

  • run 36719413183: should handle close of source gracefully (StdioTransportTest.kt:184)
  • run 35128386531: should handle close of sink gracefully (StdioTransportTest.kt:176)
  • run 35128246861: should cancel scope gracefully (StdioTransportTest.kt:200)

Root cause

This is a race in the test helper. It is not a bug in StdioTransport.

StdioTransport.close() sets CLOSING, closes the send queue, runs the close handler (closes the Source and Sink), cancels the child scope, and then sets CLOSED. Since #127, send() calls sendChannel.trySend(encoded).getOrThrow(). It therefore throws ClosedSendChannelException as soon as shutdown begins, while the state is still CLOSING. The Transport.send KDoc documents this behavior.

testWhileBackgroundSend sends in a loop until the state is CLOSED. It does not expect the rejection during CLOSING. In three tests, close() runs on a Dispatchers.IO thread (read job, write job, or scope cancellation). If the loop sends in the CLOSING window, the exception goes out of launch and fails runBlocking. The window is very short, so the failure occurs only on a busy runner. should handle close of transport gracefully cannot fail this way, because it calls close() on the same thread as the loop.

Fix

Test-only change in acp/src/jvmTest/kotlin/com/agentclientprotocol/transport/StdioTransportTest.kt:

  • testWhileBackgroundSend stops its loop when send() throws ClosedSendChannelException. The state != CLOSED loop condition stays.
  • The helper gives its send job to the block, so a test can wait for the loop to end. The existing call sites do not change.
  • The fixture sink has a beforeSinkClose hook. The default hook does nothing.
  • New regression test should stop background send when send is rejected while closing. A CountDownLatch in the sink hook holds the transport in CLOSING, so the background loop always sends in the rejected-send window. The test also asserts that a direct send() in CLOSING throws ClosedSendChannelException, then releases the latch and expects CLOSED. The latch is released in finally on all paths. The test uses no sleeps.

StdioTransport behavior does not change. Sends continue to throw during CLOSING and CLOSED. There are no public API or .api dump changes.

Verification

Red, with the regression test and fixture hook but without the helper catch:

./gradlew :acp:jvmTest --tests 'com.agentclientprotocol.transport.StdioTransportTest.should stop background send*'

Result: fails with ClosedSendChannelException from StdioTransport.send(StdioTransport.kt:168) in the testWhileBackgroundSend send job. This is the same failure as in CI.

Green, with the helper catch:

./gradlew :acp:jvmTest --tests 'com.agentclientprotocol.transport.StdioTransportTest.should stop background send*'
./gradlew :acp:jvmTest --tests com.agentclientprotocol.transport.StdioTransportTest --rerun   # 5 times
./gradlew :acp:jvmTest
./gradlew :acp:check --continue

Results:

  • The regression test passes.
  • The class passes in all 5 runs (13 tests each, 0 failures).
  • :acp:jvmTest passes (147 tests, 0 failures).
  • In :acp:check, apiCheck, jvmTest, jsNodeTest, jsBrowserTest, and wasmJsNodeTest pass. linkDebugTestIosSimulatorArm64 failed locally with MissingXcodeException, because Xcode is not installed on the machine (xcrun xcodebuild -version fails). The iOS tests therefore did not run locally. The change is in jvmTest only. CI covers the iOS targets.

…during close

Co-authored-by: Junie <junie@jetbrains.com>
@Rizzen
Rizzen merged commit 3af219a into master Oct 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants