返回 DeepSeek-Reasonix
runtime_status_test.go
根目录 / internal / control / runtime_status_test.go
1 package control
2
3 import (
4 "context"
5 "testing"
6 "time"
7
8 "reasonix/internal/event"
9 )
10
11 type approvalBlockingRunner struct {
12 c *Controller
13 }
14
15 func (r *approvalBlockingRunner) Run(ctx context.Context, _ string) error {
16 _, _, err := gateApprover{c: r.c}.Approve(ctx, "bash", "go test ./...", nil)
17 return err
18 }
19
20 type askBlockingRunner struct {
21 c *Controller
22 }
23
24 func (r *askBlockingRunner) Run(ctx context.Context, _ string) error {
25 _, err := r.c.Ask(ctx, []event.AskQuestion{{
26 ID: "choice",
27 Prompt: "Pick one",
28 Options: []event.AskOption{{Label: "A"}, {Label: "B"}},
29 }})
30 return err
31 }
32
33 func TestCancelClearsPendingApprovalRuntimeStatus(t *testing.T) {
34 approvals := make(chan event.Approval, 1)
35 done := make(chan event.Event, 1)
36 c := New(Options{Sink: event.FuncSink(func(e event.Event) {
37 switch e.Kind {
38 case event.ApprovalRequest:
39 approvals <- e.Approval
40 case event.TurnDone:
41 done <- e
42 }
43 })})
44 runner := &approvalBlockingRunner{c: c}
45 c.runner = runner
46
47 c.Send("needs approval")
48 select {
49 case <-approvals:
50 case <-time.After(30 * time.Second):
51 t.Fatal("timed out waiting for approval request")
52 }
53 if st := c.RuntimeStatus(); !st.Running || !st.PendingPrompt || !st.Cancellable || st.CancelRequested {
54 t.Fatalf("status before cancel = %+v, want running pending cancellable", st)
55 }
56
57 c.Cancel()
58 c.Cancel()
59 assertCancelClearedPendingRuntimeStatus(t, c.RuntimeStatus())
60 if e := waitTurnDoneEvent(t, done); !e.Cancelled {
61 t.Fatal("cancelled turn_done event was not marked as user-cancelled")
62 }
63 // TurnDone is emitted inside the finishing window; Running() (and the
64 // RuntimeStatus it feeds) stays true until finishGuardedTurn's deferred
65 // clear runs. Wait for the gate to reopen before asserting idle.
66 waitIdle(t, c)
67 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
68 t.Fatalf("status after turn done = %+v, want idle", st)
69 }
70 }
71
72 func TestCancelClearsPendingAskRuntimeStatus(t *testing.T) {
73 asks := make(chan event.Ask, 1)
74 done := make(chan event.Event, 1)
75 c := New(Options{Sink: event.FuncSink(func(e event.Event) {
76 switch e.Kind {
77 case event.AskRequest:
78 asks <- e.Ask
79 case event.TurnDone:
80 done <- e
81 }
82 })})
83 runner := &askBlockingRunner{c: c}
84 c.runner = runner
85
86 c.Send("ask user")
87 select {
88 case <-asks:
89 case <-time.After(30 * time.Second):
90 t.Fatal("timed out waiting for ask request")
91 }
92 if st := c.RuntimeStatus(); !st.Running || !st.PendingPrompt || !st.Cancellable || st.CancelRequested {
93 t.Fatalf("status before cancel = %+v, want running pending cancellable", st)
94 }
95
96 c.Cancel()
97 assertCancelClearedPendingRuntimeStatus(t, c.RuntimeStatus())
98 waitTurnDoneEvent(t, done)
99 // TurnDone is emitted inside the finishing window; Running() (and the
100 // RuntimeStatus it feeds) stays true until finishGuardedTurn's deferred
101 // clear runs. Wait for the gate to reopen before asserting idle.
102 waitIdle(t, c)
103 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
104 t.Fatalf("status after turn done = %+v, want idle", st)
105 }
106 }
107
108 func TestCloseCancelsPendingAskRuntimeStatus(t *testing.T) {
109 asks := make(chan event.Ask, 1)
110 done := make(chan event.Event, 1)
111 c := New(Options{Sink: event.FuncSink(func(e event.Event) {
112 switch e.Kind {
113 case event.AskRequest:
114 asks <- e.Ask
115 case event.TurnDone:
116 done <- e
117 }
118 })})
119 c.runner = &askBlockingRunner{c: c}
120
121 c.Send("ask user")
122 select {
123 case <-asks:
124 case <-time.After(time.Second):
125 t.Fatal("timed out waiting for ask request")
126 }
127
128 c.Close()
129 select {
130 case e := <-done:
131 if !e.Cancelled {
132 t.Fatal("closed turn_done event was not marked as cancelled")
133 }
134 case <-time.After(time.Second):
135 c.Cancel()
136 t.Fatal("Close did not cancel the pending ask waiter")
137 }
138 waitIdle(t, c)
139 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
140 t.Fatalf("status after Close = %+v, want idle", st)
141 }
142 }
143
144 func TestCloseDoesNotResurrectFinishingState(t *testing.T) {
145 turnStarted := make(chan struct{})
146 turnDoneEntered := make(chan struct{}, 1)
147 releaseTurnDone := make(chan struct{})
148 c := New(Options{Sink: holdFinishingWindow(releaseTurnDone, turnDoneEntered, nil)})
149
150 c.runGuarded(func(ctx context.Context) error {
151 close(turnStarted)
152 <-ctx.Done()
153 return ctx.Err()
154 })
155 <-turnStarted
156
157 c.Close()
158 select {
159 case <-turnDoneEntered:
160 case <-time.After(time.Second):
161 c.Cancel()
162 t.Fatal("Close did not cancel the active turn")
163 }
164 defer close(releaseTurnDone)
165
166 if st := c.RuntimeStatus(); st.Running || st.PendingPrompt || st.Cancellable || st.CancelRequested {
167 t.Fatalf("closed controller resurrected active state during TurnDone delivery: %+v", st)
168 }
169 }
170
171 func assertCancelClearedPendingRuntimeStatus(t *testing.T, st RuntimeStatus) {
172 t.Helper()
173 if st.PendingPrompt {
174 t.Fatalf("status immediately after cancel = %+v, want pending prompt cleared", st)
175 }
176 if st.Running {
177 if !st.Cancellable || !st.CancelRequested {
178 t.Fatalf("status immediately after cancel = %+v, want running cancelling without pending prompt", st)
179 }
180 return
181 }
182 if st.Cancellable || st.CancelRequested {
183 t.Fatalf("status immediately after cancel = %+v, want idle when turn already completed", st)
184 }
185 }
186
187 func waitTurnDoneEvent(t *testing.T, done <-chan event.Event) event.Event {
188 t.Helper()
189 select {
190 case e := <-done:
191 if e.Kind != event.TurnDone {
192 t.Fatalf("event = %v, want TurnDone", e.Kind)
193 }
194 return e
195 case <-time.After(30 * time.Second):
196 t.Fatal("timed out waiting for turn_done")
197 }
198 return event.Event{}
199 }
200
200 lines GO