返回 DeepSeek-Reasonix
runtime_status_test.go
根目录 / internal / control / runtime_status_test.go
1 package control
2
3 import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "reasonix/internal/event"
10 )
11
12 type approvalBlockingRunner struct {
13 c *Controller
14 }
15
16 func (r *approvalBlockingRunner) Run(ctx context.Context, _ string) error {
17 _, _, err := gateApprover{c: r.c}.Approve(ctx, "bash", "go test ./...", nil)
18 return err
19 }
20
21 type askBlockingRunner struct {
22 c *Controller
23 }
24
25 func (r *askBlockingRunner) Run(ctx context.Context, _ string) error {
26 _, err := r.c.Ask(ctx, []event.AskQuestion{{
27 ID: "choice",
28 Prompt: "Pick one",
29 Options: []event.AskOption{{Label: "A"}, {Label: "B"}},
30 }})
31 return err
32 }
33
34 func TestCancelClearsPendingApprovalRuntimeStatus(t *testing.T) {
35 approvals := make(chan event.Approval, 1)
36 done := make(chan event.Event, 1)
37 c := newOwnedTestController(t, Options{Sink: event.FuncSink(func(e event.Event) {
38 switch e.Kind {
39 case event.ApprovalRequest:
40 approvals <- e.Approval
41 case event.TurnDone:
42 done <- e
43 }
44 })})
45 runner := &approvalBlockingRunner{c: c}
46 c.runner = runner
47
48 c.Send("needs approval")
49 select {
50 case <-approvals:
51 case <-time.After(30 * time.Second):
52 t.Fatal("timed out waiting for approval request")
53 }
54 if st := c.RuntimeStatus(); !st.Running || !st.PendingPrompt || !st.Cancellable || st.CancelRequested {
55 t.Fatalf("status before cancel = %+v, want running pending cancellable", st)
56 }
57
58 c.Cancel()
59 c.Cancel()
60 assertCancelClearedPendingRuntimeStatus(t, c.RuntimeStatus())
61 if e := waitTurnDoneEvent(t, done); !e.Cancelled {
62 t.Fatal("cancelled turn_done event was not marked as user-cancelled")
63 }
64 // TurnDone is emitted inside the finishing window; Running() (and the
65 // RuntimeStatus it feeds) stays true until finishGuardedTurn's deferred
66 // clear runs. Wait for the gate to reopen before asserting idle.
67 waitIdle(t, c)
68 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
69 t.Fatalf("status after turn done = %+v, want idle", st)
70 }
71 }
72
73 func TestCancelClearsPendingAskRuntimeStatus(t *testing.T) {
74 asks := make(chan event.Ask, 1)
75 done := make(chan event.Event, 1)
76 c := newOwnedTestController(t, Options{Sink: event.FuncSink(func(e event.Event) {
77 switch e.Kind {
78 case event.AskRequest:
79 asks <- e.Ask
80 case event.TurnDone:
81 done <- e
82 }
83 })})
84 runner := &askBlockingRunner{c: c}
85 c.runner = runner
86
87 c.Send("ask user")
88 select {
89 case <-asks:
90 case <-time.After(30 * time.Second):
91 t.Fatal("timed out waiting for ask request")
92 }
93 if st := c.RuntimeStatus(); !st.Running || !st.PendingPrompt || !st.Cancellable || st.CancelRequested {
94 t.Fatalf("status before cancel = %+v, want running pending cancellable", st)
95 }
96
97 c.Cancel()
98 assertCancelClearedPendingRuntimeStatus(t, c.RuntimeStatus())
99 waitTurnDoneEvent(t, done)
100 // TurnDone is emitted inside the finishing window; Running() (and the
101 // RuntimeStatus it feeds) stays true until finishGuardedTurn's deferred
102 // clear runs. Wait for the gate to reopen before asserting idle.
103 waitIdle(t, c)
104 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
105 t.Fatalf("status after turn done = %+v, want idle", st)
106 }
107 }
108
109 func TestCloseCancelsPendingAskRuntimeStatus(t *testing.T) {
110 asks := make(chan event.Ask, 1)
111 done := make(chan event.Event, 1)
112 c := newOwnedTestController(t, Options{Sink: event.FuncSink(func(e event.Event) {
113 switch e.Kind {
114 case event.AskRequest:
115 asks <- e.Ask
116 case event.TurnDone:
117 done <- e
118 }
119 })})
120 c.runner = &askBlockingRunner{c: c}
121
122 c.Send("ask user")
123 select {
124 case <-asks:
125 case <-time.After(time.Second):
126 t.Fatal("timed out waiting for ask request")
127 }
128
129 c.Close()
130 select {
131 case e := <-done:
132 if !e.Cancelled {
133 t.Fatal("closed turn_done event was not marked as cancelled")
134 }
135 case <-time.After(time.Second):
136 c.Cancel()
137 t.Fatal("Close did not cancel the pending ask waiter")
138 }
139 waitIdle(t, c)
140 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
141 t.Fatalf("status after Close = %+v, want idle", st)
142 }
143 }
144
145 func TestCloseDoesNotResurrectFinishingState(t *testing.T) {
146 turnStarted := make(chan struct{})
147 turnDoneEntered := make(chan struct{}, 1)
148 releaseTurnDone := make(chan struct{})
149 c := newOwnedTestController(t, Options{Sink: holdFinishingWindow(releaseTurnDone, turnDoneEntered, nil)})
150
151 c.runGuarded(func(ctx context.Context) error {
152 close(turnStarted)
153 <-ctx.Done()
154 return ctx.Err()
155 })
156 <-turnStarted
157
158 c.Close()
159 select {
160 case <-turnDoneEntered:
161 case <-time.After(time.Second):
162 c.Cancel()
163 t.Fatal("Close did not cancel the active turn")
164 }
165 defer close(releaseTurnDone)
166
167 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
168 t.Fatalf("closed controller resurrected active state during TurnDone delivery: %+v", st)
169 }
170 }
171
172 func TestTurnFinishingDoneClosesAfterTurnDoneFanout(t *testing.T) {
173 turnDoneEntered := make(chan struct{}, 1)
174 releaseTurnDone := make(chan struct{})
175 c := newOwnedTestController(t, Options{Sink: holdFinishingWindow(releaseTurnDone, turnDoneEntered, nil)})
176 t.Cleanup(c.Close)
177
178 c.runGuarded(func(context.Context) error { return nil })
179 select {
180 case <-turnDoneEntered:
181 case <-time.After(time.Second):
182 t.Fatal("TurnDone delivery did not enter the finishing window")
183 }
184
185 done, ok := c.TurnFinishingDone()
186 if !ok || done == nil {
187 close(releaseTurnDone)
188 t.Fatal("controller did not expose its active finishing boundary")
189 }
190 select {
191 case <-done:
192 close(releaseTurnDone)
193 t.Fatal("finishing boundary closed before TurnDone fan-out returned")
194 default:
195 }
196
197 close(releaseTurnDone)
198 select {
199 case <-done:
200 case <-time.After(time.Second):
201 t.Fatal("finishing boundary did not close after TurnDone fan-out")
202 }
203 if _, ok := c.TurnFinishingDone(); ok {
204 t.Fatal("controller retained a stale finishing boundary")
205 }
206 }
207
208 func TestTurnIdleDoneCoversExecutionAndTurnDoneFanout(t *testing.T) {
209 turnStarted := make(chan struct{})
210 releaseTurn := make(chan struct{})
211 turnDoneEntered := make(chan struct{}, 1)
212 releaseTurnDone := make(chan struct{})
213 c := newOwnedTestController(t, Options{Sink: holdFinishingWindow(releaseTurnDone, turnDoneEntered, nil)})
214 t.Cleanup(c.Close)
215
216 c.runGuarded(func(context.Context) error {
217 close(turnStarted)
218 <-releaseTurn
219 return nil
220 })
221 select {
222 case <-turnStarted:
223 case <-time.After(time.Second):
224 close(releaseTurn)
225 t.Fatal("turn did not start")
226 }
227
228 done, ok := c.TurnIdleDone()
229 if !ok || done == nil {
230 close(releaseTurn)
231 t.Fatal("controller did not expose its active idle boundary")
232 }
233 close(releaseTurn)
234 select {
235 case <-turnDoneEntered:
236 case <-time.After(time.Second):
237 t.Fatal("TurnDone delivery did not enter the finishing window")
238 }
239 select {
240 case <-done:
241 close(releaseTurnDone)
242 t.Fatal("idle boundary closed before TurnDone fan-out returned")
243 default:
244 }
245
246 close(releaseTurnDone)
247 select {
248 case <-done:
249 case <-time.After(time.Second):
250 t.Fatal("idle boundary did not close after TurnDone fan-out")
251 }
252 if _, ok := c.TurnIdleDone(); ok {
253 t.Fatal("controller retained a stale idle boundary")
254 }
255 }
256
257 func TestTurnIdleDoneStaysOpenAcrossParkedTurn(t *testing.T) {
258 firstStarted := make(chan struct{})
259 releaseFirst := make(chan struct{})
260 firstTurnDoneEntered := make(chan struct{})
261 releaseFirstTurnDone := make(chan struct{})
262 secondStarted := make(chan struct{})
263 releaseSecond := make(chan struct{})
264 var firstTurnDone sync.Once
265 c := newOwnedTestController(t, Options{Sink: event.FuncSink(func(e event.Event) {
266 if e.Kind == event.TurnDone {
267 firstTurnDone.Do(func() {
268 close(firstTurnDoneEntered)
269 <-releaseFirstTurnDone
270 })
271 }
272 })})
273 t.Cleanup(c.Close)
274
275 c.runGuarded(func(context.Context) error {
276 close(firstStarted)
277 <-releaseFirst
278 return nil
279 })
280 select {
281 case <-firstStarted:
282 case <-time.After(time.Second):
283 close(releaseFirst)
284 t.Fatal("first turn did not start")
285 }
286 done, ok := c.TurnIdleDone()
287 if !ok || done == nil {
288 close(releaseFirst)
289 t.Fatal("controller did not expose the first turn's idle boundary")
290 }
291
292 close(releaseFirst)
293 select {
294 case <-firstTurnDoneEntered:
295 case <-time.After(time.Second):
296 close(releaseFirstTurnDone)
297 t.Fatal("first TurnDone did not enter the finishing window")
298 }
299 if got := c.runGuarded(func(context.Context) error {
300 close(secondStarted)
301 <-releaseSecond
302 return nil
303 }); got != turnParked {
304 close(releaseFirstTurnDone)
305 t.Fatalf("turn admitted during finishing = %v, want parked", got)
306 }
307 close(releaseFirstTurnDone)
308 select {
309 case <-secondStarted:
310 case <-time.After(time.Second):
311 close(releaseSecond)
312 t.Fatal("parked turn did not start after finishing completed")
313 }
314 select {
315 case <-done:
316 close(releaseSecond)
317 t.Fatal("idle boundary closed between the finishing and parked turns")
318 default:
319 }
320
321 close(releaseSecond)
322 select {
323 case <-done:
324 case <-time.After(time.Second):
325 t.Fatal("idle boundary did not close after the parked turn completed")
326 }
327 }
328
329 func assertCancelClearedPendingRuntimeStatus(t *testing.T, st RuntimeStatus) {
330 t.Helper()
331 if st.PendingPrompt {
332 t.Fatalf("status immediately after cancel = %+v, want pending prompt cleared", st)
333 }
334 if st.Running {
335 if !st.Cancellable || !st.CancelRequested {
336 t.Fatalf("status immediately after cancel = %+v, want running cancelling without pending prompt", st)
337 }
338 return
339 }
340 if st.Cancellable || st.CancelRequested {
341 t.Fatalf("status immediately after cancel = %+v, want idle when turn already completed", st)
342 }
343 }
344
345 func waitTurnDoneEvent(t *testing.T, done <-chan event.Event) event.Event {
346 t.Helper()
347 select {
348 case e := <-done:
349 if e.Kind != event.TurnDone {
350 t.Fatalf("event = %v, want TurnDone", e.Kind)
351 }
352 return e
353 case <-time.After(30 * time.Second):
354 t.Fatal("timed out waiting for turn_done")
355 }
356 return event.Event{}
357 }
358
358 lines GO