返回 DeepSeek-Reasonix
yolo_memory_test.go
根目录 / internal / control / yolo_memory_test.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "testing"
7 "time"
8
9 "reasonix/internal/event"
10 "reasonix/internal/permission"
11 )
12
13 func TestMemoryApprovalStillPromptsUnderAsk(t *testing.T) {
14 approvalRequests := make(chan event.Approval, 1)
15 c := newOwnedTestController(t, Options{
16 Sink: event.FuncSink(func(e event.Event) {
17 if e.Kind == event.ApprovalRequest {
18 approvalRequests <- e.Approval
19 }
20 }),
21 })
22 c.SetToolApprovalMode(ToolApprovalAsk)
23
24 done := make(chan bool, 1)
25 errs := make(chan error, 1)
26 go func() {
27 allow, _, err := c.requestApproval(context.Background(), "remember", "", nil)
28 if err != nil {
29 errs <- err
30 return
31 }
32 done <- allow
33 }()
34
35 var approval event.Approval
36 select {
37 case approval = <-approvalRequests:
38 case <-time.After(30 * time.Second):
39 t.Fatal("memory approval request was not emitted under ask approval")
40 }
41 if approval.Tool != "remember" {
42 t.Fatalf("approval tool = %q, want remember", approval.Tool)
43 }
44
45 if !c.PendingPrompt() {
46 t.Fatal("memory approval must remain pending under ask")
47 }
48
49 c.Approve(approval.ID, true, true, false)
50 select {
51 case err := <-errs:
52 t.Fatalf("requestApproval: %v", err)
53 case allow := <-done:
54 if !allow {
55 t.Fatal("manual approval should allow memory write")
56 }
57 case <-time.After(30 * time.Second):
58 t.Fatal("memory approval stayed blocked after Approve")
59 }
60 }
61
62 func TestAutoAndYoloAllowMemoryWithoutPrompt(t *testing.T) {
63 feedback := json.RawMessage(`{"name":"python-env-cross-project","type":"feedback","description":"cross-project Python env","body":"Embedded Python is unreliable."}`)
64 for _, mode := range []string{ToolApprovalAuto, ToolApprovalYolo} {
65 t.Run(mode, func(t *testing.T) {
66 var approvalRequested bool
67 c := newOwnedTestController(t, Options{
68 Sink: event.FuncSink(func(e event.Event) {
69 if e.Kind == event.ApprovalRequest {
70 approvalRequested = true
71 }
72 }),
73 })
74 c.SetToolApprovalMode(mode)
75
76 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
77 defer cancel()
78 for _, toolName := range []string{"remember", "forget"} {
79 allow, remember, err := c.requestApproval(ctx, toolName, "", feedback)
80 if err != nil || !allow || remember {
81 t.Fatalf("requestApproval(%s) in %s = (%v,%v,%v), want allow without persist", toolName, mode, allow, remember, err)
82 }
83 }
84
85 gate := c.newInteractiveGate()
86 for _, toolName := range []string{"remember", "forget"} {
87 allow, reason, err := gate.Check(context.Background(), toolName, feedback, false)
88 if err != nil || !allow {
89 t.Fatalf("interactive gate %s under %s = (%v,%q,%v), want allow", toolName, mode, allow, reason, err)
90 }
91 if got := gate.Policy.Decide(toolName, false, feedback); got != permission.Allow {
92 t.Fatalf("%s policy under %s = %v, want allow", toolName, mode, got)
93 }
94 }
95 if approvalRequested {
96 t.Fatalf("%s must not emit a remember/forget approval prompt", mode)
97 }
98 })
99 }
100 }
101
102 func TestToolApprovalModeAutoPreservesExplicitMemoryAsk(t *testing.T) {
103 approvalRequests := make(chan event.Approval, 1)
104 c := newOwnedTestController(t, Options{
105 Policy: permission.New("ask", nil, []string{"remember"}, nil),
106 Sink: event.FuncSink(func(e event.Event) {
107 if e.Kind == event.ApprovalRequest {
108 approvalRequests <- e.Approval
109 }
110 }),
111 })
112 c.SetToolApprovalMode(ToolApprovalAuto)
113 gate := c.newInteractiveGate()
114 args := json.RawMessage(`{"name":"explicit-review","description":"review","body":"ask first"}`)
115 if got := gate.Policy.Decide("remember", false, args); got != permission.Ask {
116 t.Fatalf("explicit remember ask under auto = %v, want ask", got)
117 }
118
119 done := make(chan bool, 1)
120 go func() {
121 allow, _, err := gate.Check(context.Background(), "remember", args, false)
122 if err != nil {
123 done <- false
124 return
125 }
126 done <- allow
127 }()
128 var approval event.Approval
129 select {
130 case approval = <-approvalRequests:
131 case <-time.After(30 * time.Second):
132 t.Fatal("explicit memory ask was not emitted under auto")
133 }
134 c.Approve(approval.ID, true, false, false)
135 select {
136 case allow := <-done:
137 if !allow {
138 t.Fatal("manual approval should allow the explicit memory ask")
139 }
140 case <-time.After(30 * time.Second):
141 t.Fatal("explicit memory ask stayed blocked after approval")
142 }
143 }
144
145 func TestToolApprovalModeYoloBypassesMemoryAskAndHonorsDeny(t *testing.T) {
146 var approvalRequested bool
147 c := newOwnedTestController(t, Options{
148 Policy: permission.New("ask", nil, []string{"forget"}, []string{"remember"}),
149 Sink: event.FuncSink(func(e event.Event) {
150 if e.Kind == event.ApprovalRequest {
151 approvalRequested = true
152 }
153 }),
154 })
155 c.SetToolApprovalMode(ToolApprovalYolo)
156
157 gate := c.newInteractiveGate()
158 if got := gate.Policy.Decide("forget", false, json.RawMessage(`{}`)); got != permission.Ask {
159 t.Fatalf("explicit forget rule under yolo mode = %v, want ask before bypass", got)
160 }
161 if got := gate.Policy.Decide("bash", false, json.RawMessage(`{"command":"go test ./..."}`)); got != permission.Allow {
162 t.Fatalf("regular tool under yolo mode = %v, want allow", got)
163 }
164 allow, reason, err := gate.Check(context.Background(), "forget", json.RawMessage(`{"id":"old-fact"}`), false)
165 if err != nil || !allow {
166 t.Fatalf("explicit forget ask under yolo = (%v,%q,%v), want bypass", allow, reason, err)
167 }
168 if approvalRequested {
169 t.Fatal("YOLO must bypass an explicit remember/forget ask without emitting a prompt")
170 }
171 allow, _, err = gate.Check(context.Background(), "remember", json.RawMessage(`{"name":"blocked","body":"no"}`), false)
172 if err != nil || allow {
173 t.Fatalf("denied remember under yolo = (%v,%v), want deny", allow, err)
174 }
175 }
176
177 func TestSetAutoApproveToolsDoesNotResolvePendingMemoryApproval(t *testing.T) {
178 approvalRequests := make(chan event.Approval, 1)
179 c := newOwnedTestController(t, Options{
180 Sink: event.FuncSink(func(e event.Event) {
181 if e.Kind == event.ApprovalRequest {
182 approvalRequests <- e.Approval
183 }
184 }),
185 })
186
187 done := make(chan bool, 1)
188 errs := make(chan error, 1)
189 go func() {
190 allow, _, err := c.requestApproval(context.Background(), "forget", "", nil)
191 if err != nil {
192 errs <- err
193 return
194 }
195 done <- allow
196 }()
197
198 var approval event.Approval
199 select {
200 case approval = <-approvalRequests:
201 case <-time.After(30 * time.Second):
202 t.Fatal("memory approval request was not emitted")
203 }
204
205 c.SetAutoApproveTools(true)
206
207 select {
208 case err := <-errs:
209 t.Fatalf("requestApproval: %v", err)
210 case allow := <-done:
211 t.Fatalf("legacy permission change answered pending memory approval: allow=%v", allow)
212 case <-time.After(50 * time.Millisecond):
213 }
214 c.Approve(approval.ID, true, false, false)
215 if allow := <-done; !allow {
216 t.Fatal("manual approval should allow memory write")
217 }
218 }
219
220 func TestToolApprovalModeAutoDrainsPendingMemoryFallback(t *testing.T) {
221 approvalRequests := make(chan event.Approval, 1)
222 c := newOwnedTestController(t, Options{
223 Sink: event.FuncSink(func(e event.Event) {
224 if e.Kind == event.ApprovalRequest {
225 approvalRequests <- e.Approval
226 }
227 }),
228 })
229
230 done := make(chan bool, 1)
231 go func() {
232 allow, _, _ := c.requestApproval(context.Background(), "forget", "", nil)
233 done <- allow
234 }()
235 var approval event.Approval
236 select {
237 case approval = <-approvalRequests:
238 case <-time.After(30 * time.Second):
239 t.Fatal("memory approval request was not emitted")
240 }
241
242 drained := c.ApplyToolApprovalMode(ToolApprovalAuto)
243 if len(drained) != 0 {
244 t.Fatalf("workspace preset resolved old memory approval: %v", drained)
245 }
246 select {
247 case allow := <-done:
248 t.Fatalf("preset switch resolved pending memory approval: allow=%v", allow)
249 default:
250 }
251 c.Approve(approval.ID, false, false, false)
252 select {
253 case allow := <-done:
254 if allow {
255 t.Fatal("cleanup denial unexpectedly allowed memory operation")
256 }
257 case <-time.After(30 * time.Second):
258 t.Fatal("pending memory approval did not resolve after denial")
259 }
260 }
261
262 func TestToolApprovalModeAutoKeepsPendingExplicitMemoryAsk(t *testing.T) {
263 approvalRequests := make(chan event.Approval, 1)
264 c := newOwnedTestController(t, Options{
265 Policy: permission.New("ask", nil, []string{"forget"}, nil),
266 Sink: event.FuncSink(func(e event.Event) {
267 if e.Kind == event.ApprovalRequest {
268 approvalRequests <- e.Approval
269 }
270 }),
271 })
272
273 done := make(chan bool, 1)
274 go func() {
275 allow, _, _ := c.requestApproval(context.Background(), "forget", "", nil)
276 done <- allow
277 }()
278 var approval event.Approval
279 select {
280 case approval = <-approvalRequests:
281 case <-time.After(30 * time.Second):
282 t.Fatal("explicit memory ask was not emitted")
283 }
284
285 if drained := c.ApplyToolApprovalMode(ToolApprovalAuto); len(drained) != 0 {
286 t.Fatalf("auto drained explicit memory ask ids = %v, want none", drained)
287 }
288 approvals, _ := c.approval.snapshotPrompts()
289 if len(approvals) != 1 || approvals[0].ID != approval.ID {
290 t.Fatalf("pending approvals after auto = %+v, want %s", approvals, approval.ID)
291 }
292 c.Approve(approval.ID, true, false, false)
293 select {
294 case allow := <-done:
295 if !allow {
296 t.Fatal("manual approval should allow the explicit memory ask")
297 }
298 case <-time.After(30 * time.Second):
299 t.Fatal("explicit memory ask stayed blocked after approval")
300 }
301 }
302
302 lines GO