Files
2026-07-13 13:00:08 +08:00

99 lines
2.5 KiB
Go

package plugin
import (
"context"
"testing"
"time"
)
type discardWriteCloser struct{}
func (discardWriteCloser) Write(p []byte) (int, error) { return len(p), nil }
func (discardWriteCloser) Close() error { return nil }
// TestStdioCallReturnsOnContextCancel pins that a stdio call unblocks when its
// context is cancelled even though the server never replies. The stdio child is
// bound to the session, not the turn, so without this a hung server would hang a
// cancelled turn forever. No reader goroutine runs here, so the reply never
// arrives — only ctx cancellation can return the call.
func TestStdioCallReturnsOnContextCancel(t *testing.T) {
tr := &stdioTransport{
name: "hung",
stdin: discardWriteCloser{},
pending: map[int]chan rpcResponse{},
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
_, err := tr.call(ctx, "tools/call", map[string]any{})
done <- err
}()
time.Sleep(100 * time.Millisecond) // let the call park in its select
cancel()
select {
case err := <-done:
if err == nil {
t.Fatal("cancelled call returned nil error")
}
case <-time.After(2 * time.Second):
t.Fatal("stdio call did not return within 2s of ctx cancel — a hung server hangs the turn")
}
}
func TestStdioCallRespectsExistingDeadline(t *testing.T) {
tr := &stdioTransport{
name: "server",
stdin: discardWriteCloser{},
pending: map[int]chan rpcResponse{},
}
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
done := make(chan error, 1)
go func() {
_, err := tr.call(ctx, "tools/call", map[string]any{})
done <- err
}()
select {
case err := <-done:
if err == nil {
t.Fatal("timed-out call returned nil error")
}
case <-time.After(1 * time.Second):
t.Fatal("stdio call did not return within caller deadline")
}
}
func TestStdioCallCancelReturnsContextCanceled(t *testing.T) {
tr := &stdioTransport{
name: "slow-server",
stdin: discardWriteCloser{},
pending: map[int]chan rpcResponse{},
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
_, err := tr.call(ctx, "tools/call", map[string]any{})
done <- err
}()
time.Sleep(200 * time.Millisecond)
cancel()
select {
case err := <-done:
if err == nil {
t.Fatal("cancelled call returned nil error")
}
if err != context.Canceled {
t.Fatalf("expected context.Canceled, got: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("stdio call did not return within 2s of cancel")
}
}