forked from github/copilot-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjsonrpc2_test.go
More file actions
187 lines (148 loc) · 4.87 KB
/
Copy pathjsonrpc2_test.go
File metadata and controls
187 lines (148 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package jsonrpc2
import (
"errors"
"io"
"sync"
"testing"
"time"
)
func TestOnCloseCalledOnUnexpectedExit(t *testing.T) {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
defer stdinR.Close()
client := NewClient(stdinW, stdoutR)
var called bool
var mu sync.Mutex
client.SetOnClose(func() {
mu.Lock()
called = true
mu.Unlock()
})
client.Start()
// Simulate unexpected process death by closing the stdout writer
stdoutW.Close()
// Wait for readLoop to detect the close and invoke the callback
time.Sleep(200 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
if !called {
t.Error("expected onClose to be called when read loop exits unexpectedly")
}
}
func TestOnCloseNotCalledOnIntentionalStop(t *testing.T) {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
defer stdinR.Close()
defer stdoutW.Close()
client := NewClient(stdinW, stdoutR)
var called bool
var mu sync.Mutex
client.SetOnClose(func() {
mu.Lock()
called = true
mu.Unlock()
})
client.Start()
// Intentional stop — should set running=false before closing stdout,
// so the readLoop should NOT invoke onClose.
client.Stop()
time.Sleep(200 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
if called {
t.Error("onClose should not be called on intentional Stop()")
}
}
// TestSetProcessDone_ErrorAvailableImmediately validates that getProcessError()
// returns the correct error immediately after processDone is closed.
// The current implementation stores a pointer to the process error
// synchronously when the processDone channel is closed, so callers should
// never observe a nil error after the channel has been closed.
func TestSetProcessDone_ErrorAvailableImmediately(t *testing.T) {
misses := 0
const iterations = 1000
for i := 0; i < iterations; i++ {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
client := NewClient(stdinW, stdoutR)
done := make(chan struct{})
processErr := errors.New("CLI process exited: exit status 1")
client.SetProcessDone(done, &processErr)
// Simulate process exit: error is already set, close the channel.
close(done)
// Do NOT yield to the scheduler — check immediately.
// In the current code the goroutine inside SetProcessDone may not
// have copied the error to client.processError yet.
if err := client.getProcessError(); err == nil {
misses++
}
stdinR.Close()
stdinW.Close()
stdoutR.Close()
stdoutW.Close()
}
if misses > 0 {
t.Errorf("SetProcessDone regression: getProcessError() returned nil %d/%d times "+
"immediately after processDone was closed, even though the error pointer "+
"should be stored synchronously.", misses, iterations)
}
}
// TestSetProcessDone_RequestMissesProcessError validates that the Request()
// method returns the specific process error instead of the generic
// "process exited unexpectedly" message once processDone has been closed.
func TestSetProcessDone_RequestMissesProcessError(t *testing.T) {
misses := 0
const iterations = 100
for i := 0; i < iterations; i++ {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
client := NewClient(stdinW, stdoutR)
client.Start()
done := make(chan struct{})
processErr := errors.New("CLI process exited: authentication failed")
client.SetProcessDone(done, &processErr)
// Simulate process exit.
close(done)
// Close the writer so the readLoop can exit.
stdoutW.Close()
// Make a request — should get the specific process error.
_, err := client.Request("test.method", nil)
if err != nil && err.Error() == "process exited unexpectedly" {
misses++
}
client.Stop()
stdinR.Close()
stdinW.Close()
stdoutR.Close()
}
if misses > 0 {
t.Errorf("Request() bug: returned generic 'process exited unexpectedly' %d/%d times "+
"instead of the actual process error after process exit; the process "+
"error was not correctly propagated from SetProcessDone.", misses, iterations)
}
}
// TestSetProcessDone_ErrorAvailableImmediately verifies that the process error
// is available as soon as the done channel is closed, matching the
// pointer-based implementation where no asynchronous copy is required.
func TestSetProcessDone_ErrorCopiedEventually(t *testing.T) {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
defer stdinR.Close()
defer stdinW.Close()
defer stdoutR.Close()
defer stdoutW.Close()
client := NewClient(stdinW, stdoutR)
done := make(chan struct{})
processErr := errors.New("CLI process exited: version mismatch")
client.SetProcessDone(done, &processErr)
// Close the channel: the process error should now be observable immediately,
// without needing to yield to another goroutine.
close(done)
err := client.getProcessError()
if err == nil {
t.Fatal("expected process error to be available immediately after done is closed, got nil")
}
if err.Error() != processErr.Error() {
t.Errorf("expected %q, got %q", processErr.Error(), err.Error())
}
}