99 lines
2.5 KiB
Go
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")
|
|
}
|
|
}
|