返回 DeepSeek-Reasonix
tab_event_sink_test.go
根目录 / desktop / tab_event_sink_test.go
1 package main
2
3 import (
4 "context"
5 "strconv"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "reasonix/internal/event"
11 )
12
13 type closeTrackingSink struct {
14 closed atomic.Bool
15 }
16
17 func (s *closeTrackingSink) Emit(event.Event) {}
18
19 func (s *closeTrackingSink) Close() {
20 s.closed.Store(true)
21 }
22
23 type blockingCloseTrackingSink struct {
24 closeTrackingSink
25 entered chan struct{}
26 release chan struct{}
27 }
28
29 func (s *blockingCloseTrackingSink) Emit(event.Event) {
30 close(s.entered)
31 <-s.release
32 }
33
34 func TestTabEventSinkSetBotSinkClosesPreviousSink(t *testing.T) {
35 sink := &tabEventSink{}
36 first := &closeTrackingSink{}
37 second := &closeTrackingSink{}
38
39 sink.SetBotSink(first)
40 if first.closed.Load() {
41 t.Fatal("newly attached sink was closed")
42 }
43
44 sink.SetBotSink(second)
45 if !first.closed.Load() {
46 t.Fatal("previous sink was not closed when replaced")
47 }
48 if second.closed.Load() {
49 t.Fatal("replacement sink was closed too early")
50 }
51
52 sink.SetBotSink(nil)
53 if !second.closed.Load() {
54 t.Fatal("second sink was not closed when cleared")
55 }
56 }
57
58 func TestTabEventSinkOldTurnDoneDoesNotClearReplacement(t *testing.T) {
59 sink := &tabEventSink{}
60 old := &blockingCloseTrackingSink{
61 entered: make(chan struct{}),
62 release: make(chan struct{}),
63 }
64 replacement := &closeTrackingSink{}
65
66 if !sink.tryBeginTurn() {
67 t.Fatal("failed to reserve initial turn")
68 }
69 sink.SetBotSink(old)
70 done := make(chan struct{})
71 go func() {
72 sink.Emit(event.Event{Kind: event.TurnDone})
73 close(done)
74 }()
75 select {
76 case <-old.entered:
77 case <-time.After(500 * time.Millisecond):
78 t.Fatal("old forwarder did not receive TurnDone")
79 }
80
81 sink.SetBotSink(replacement)
82 if sink.tryBeginTurn() {
83 t.Fatal("new turn admitted before old TurnDone completed")
84 }
85 close(old.release)
86 select {
87 case <-done:
88 case <-time.After(500 * time.Millisecond):
89 t.Fatal("TurnDone did not finish")
90 }
91
92 if replacement.closed.Load() {
93 t.Fatal("old TurnDone cleared the replacement forwarder")
94 }
95 got, _ := sink.botSinkSnapshot()
96 if got != replacement {
97 t.Fatalf("attached forwarder = %T, want replacement", got)
98 }
99 if !sink.tryBeginTurn() {
100 t.Fatal("next turn was not admitted after TurnDone completed")
101 }
102 sink.cancelTurnStart()
103 sink.SetBotSink(nil)
104 }
105
106 func TestTabEventSinkDoesNotBlockOnRuntimeEventsEmit(t *testing.T) {
107 entered := make(chan struct{})
108 release := make(chan struct{})
109 delivered := make(chan string, 2)
110 var calls atomic.Int32
111
112 sink := &tabEventSink{tabID: "tab", ctx: context.Background()}
113 sink.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) {
114 if name != eventChannel {
115 t.Errorf("event name = %q, want %q", name, eventChannel)
116 }
117 if len(payload) != 1 {
118 t.Errorf("payload count = %d, want 1", len(payload))
119 return
120 }
121 wire, ok := payload[0].(wireEventTab)
122 if !ok {
123 t.Errorf("payload type = %T, want wireEventTab", payload[0])
124 return
125 }
126 delivered <- wire.Text
127 if calls.Add(1) == 1 {
128 close(entered)
129 <-release
130 }
131 }
132
133 wrapped := event.Sync(sink)
134 wrapped.Emit(event.Event{Kind: event.Text, Text: "one"})
135
136 select {
137 case <-entered:
138 case <-time.After(500 * time.Millisecond):
139 t.Fatal("first runtime emit did not start")
140 }
141
142 done := make(chan struct{})
143 go func() {
144 wrapped.Emit(event.Event{Kind: event.Text, Text: "two"})
145 close(done)
146 }()
147 select {
148 case <-done:
149 case <-time.After(500 * time.Millisecond):
150 t.Fatal("second event blocked behind runtime EventsEmit")
151 }
152
153 close(release)
154 if got := <-delivered; got != "one" {
155 t.Fatalf("first delivered event = %q, want one", got)
156 }
157 select {
158 case got := <-delivered:
159 if got != "two" {
160 t.Fatalf("second delivered event = %q, want two", got)
161 }
162 case <-time.After(500 * time.Millisecond):
163 t.Fatal("second queued event was not delivered")
164 }
165 }
166
167 func TestEmitProjectTreeChangedDoesNotBlockOnRuntimeEventsEmit(t *testing.T) {
168 entered := make(chan struct{})
169 release := make(chan struct{})
170 var calls atomic.Int32
171
172 app := &App{ctx: context.Background()}
173 app.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) {
174 if name != "project-tree:changed" {
175 t.Errorf("event name = %q, want project-tree:changed", name)
176 }
177 if len(payload) != 0 {
178 t.Errorf("payload count = %d, want 0", len(payload))
179 }
180 if calls.Add(1) == 1 {
181 close(entered)
182 <-release
183 }
184 }
185
186 app.emitProjectTreeChanged()
187 select {
188 case <-entered:
189 case <-time.After(500 * time.Millisecond):
190 t.Fatal("first project tree runtime emit did not start")
191 }
192
193 done := make(chan struct{})
194 go func() {
195 app.emitProjectTreeChanged()
196 close(done)
197 }()
198 select {
199 case <-done:
200 case <-time.After(500 * time.Millisecond):
201 t.Fatal("project tree event blocked behind runtime EventsEmit")
202 }
203
204 close(release)
205 deadline := time.Now().Add(500 * time.Millisecond)
206 for time.Now().Before(deadline) {
207 if calls.Load() >= 2 {
208 return
209 }
210 time.Sleep(5 * time.Millisecond)
211 }
212 t.Fatalf("runtime emit calls = %d, want at least 2", calls.Load())
213 }
214
215 func TestAsyncRuntimeEmitterDrainsBacklogInOrder(t *testing.T) {
216 const backlog = 256
217
218 entered := make(chan struct{})
219 release := make(chan struct{})
220 delivered := make(chan string, backlog)
221 var calls atomic.Int32
222
223 emitter := &asyncRuntimeEmitter{}
224 emitter.emit = func(_ context.Context, _ string, payload ...interface{}) {
225 if len(payload) != 1 {
226 t.Errorf("payload count = %d, want 1", len(payload))
227 return
228 }
229 value, ok := payload[0].(string)
230 if !ok {
231 t.Errorf("payload type = %T, want string", payload[0])
232 return
233 }
234 delivered <- value
235 if calls.Add(1) == 1 {
236 close(entered)
237 <-release
238 }
239 }
240
241 ctx := context.Background()
242 for i := 0; i < backlog; i++ {
243 emitter.Emit(ctx, "agent:event", strconv.Itoa(i))
244 }
245
246 select {
247 case <-entered:
248 case <-time.After(500 * time.Millisecond):
249 t.Fatal("first runtime emit did not start")
250 }
251 close(release)
252
253 for i := 0; i < backlog; i++ {
254 select {
255 case got := <-delivered:
256 if want := strconv.Itoa(i); got != want {
257 t.Fatalf("delivered[%d] = %q, want %q", i, got, want)
258 }
259 case <-time.After(500 * time.Millisecond):
260 t.Fatalf("timed out waiting for delivered event %d", i)
261 }
262 }
263 }
264
264 lines GO