Skip to content

Commit 214cc52

Browse files
committed
Add/fix StdioClientTransport tests
1 parent 095cf98 commit 214cc52

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.modelcontextprotocol.kotlin.sdk.client.stdio
2+
3+
import io.kotest.matchers.booleans.shouldBeFalse
4+
import io.kotest.matchers.shouldBe
5+
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.test.runTest
8+
import kotlinx.io.Buffer
9+
import kotlin.test.Test
10+
import kotlin.time.Duration.Companion.milliseconds
11+
12+
/**
13+
* Tests for StdioClientTransport error handling: EOF, IO errors, and edge cases.
14+
*/
15+
class StdioClientTransportErrorHandlingTest {
16+
17+
@Test
18+
fun `should continue on stderr EOF`() = runTest {
19+
val stderrBuffer = Buffer()
20+
// Empty stderr = immediate EOF
21+
22+
val inputBuffer = Buffer()
23+
val outputBuffer = Buffer()
24+
25+
val transport = StdioClientTransport(
26+
input = inputBuffer,
27+
output = outputBuffer,
28+
error = stderrBuffer,
29+
)
30+
31+
var closeCalled = false
32+
transport.onClose { closeCalled = true }
33+
34+
transport.start()
35+
delay(100.milliseconds)
36+
37+
// Stderr EOF should not close transport
38+
closeCalled.shouldBeFalse()
39+
40+
transport.close()
41+
}
42+
43+
@Test
44+
fun `should call onClose exactly once on error scenarios`() = runTest {
45+
val stderrBuffer = Buffer()
46+
stderrBuffer.write("FATAL: critical error\n".encodeToByteArray())
47+
48+
val inputBuffer = Buffer()
49+
val outputBuffer = Buffer()
50+
51+
var closeCallCount = 0
52+
53+
val transport = StdioClientTransport(
54+
input = inputBuffer,
55+
output = outputBuffer,
56+
error = stderrBuffer,
57+
classifyStderr = { StdioClientTransport.StderrSeverity.FATAL },
58+
)
59+
60+
transport.onClose { closeCallCount++ }
61+
62+
transport.start()
63+
delay(100.milliseconds)
64+
65+
// Explicit close after error already closed it
66+
transport.close()
67+
68+
closeCallCount shouldBe 1
69+
}
70+
71+
@Test
72+
fun `should handle empty input gracefully`() = runTest {
73+
val inputBuffer = Buffer()
74+
val outputBuffer = Buffer()
75+
76+
val transport = StdioClientTransport(
77+
input = inputBuffer,
78+
output = outputBuffer,
79+
)
80+
81+
var errorCalled = false
82+
transport.onError { errorCalled = true }
83+
84+
transport.start()
85+
delay(100.milliseconds)
86+
87+
// Empty input should close cleanly without error
88+
errorCalled.shouldBeFalse()
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.modelcontextprotocol.kotlin.sdk.client.stdio
2+
3+
import io.kotest.assertions.throwables.shouldThrow
4+
import io.kotest.matchers.shouldBe
5+
import io.kotest.matchers.string.shouldContain
6+
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
7+
import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
8+
import io.modelcontextprotocol.kotlin.sdk.types.toJSON
9+
import kotlinx.coroutines.delay
10+
import kotlinx.coroutines.test.runTest
11+
import kotlinx.io.Buffer
12+
import kotlin.test.Test
13+
import kotlin.time.Duration.Companion.milliseconds
14+
15+
class StdioClientTransportLifecycleTest {
16+
17+
@Test
18+
fun `should throw when started twice`() = runTest {
19+
val transport = createTransport()
20+
21+
transport.start()
22+
23+
val exception = shouldThrow<IllegalStateException> {
24+
transport.start()
25+
}
26+
exception.message shouldContain "already started"
27+
28+
transport.close()
29+
}
30+
31+
@Test
32+
fun `should be idempotent when closed twice`() = runTest {
33+
val transport = createTransport()
34+
35+
transport.start()
36+
transport.close()
37+
38+
// Second close should not throw
39+
transport.close()
40+
}
41+
42+
@Test
43+
fun `should throw when sending before start`() = runTest {
44+
val transport = createTransport()
45+
46+
val exception = shouldThrow<IllegalStateException> {
47+
transport.send(PingRequest().toJSON())
48+
}
49+
exception.message shouldContain "not started"
50+
}
51+
52+
@Test
53+
fun `should throw when sending after close`() = runTest {
54+
val transport = createTransport()
55+
56+
transport.start()
57+
delay(50.milliseconds)
58+
transport.close()
59+
60+
shouldThrow<Exception> {
61+
transport.send(PingRequest().toJSON())
62+
}
63+
}
64+
65+
@Test
66+
fun `should call onClose exactly once`() = runTest {
67+
val transport = createTransport()
68+
69+
var closeCallCount = 0
70+
transport.onClose { closeCallCount++ }
71+
72+
transport.start()
73+
delay(50.milliseconds)
74+
75+
// Multiple close attempts
76+
transport.close()
77+
transport.close()
78+
79+
closeCallCount shouldBe 1
80+
}
81+
82+
private fun createTransport(): StdioClientTransport {
83+
val inputBuffer = Buffer()
84+
val outputBuffer = Buffer()
85+
return StdioClientTransport(
86+
input = inputBuffer,
87+
output = outputBuffer,
88+
)
89+
}
90+
}

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class StdioClientTransportTest : BaseTransportTest() {
3737
error = stderr,
3838
) {
3939
println("💥Ah-oh!, error: \"$it\"")
40-
true
40+
StdioClientTransport.StderrSeverity.FATAL
4141
}
4242

4343
val client = Client(

0 commit comments

Comments
 (0)