返回 DeepSeek-Reasonix
approval_commit_test.go
根目录 / internal / control / approval_commit_test.go
1 package control
2
3 import (
4 "errors"
5 "testing"
6
7 "reasonix/internal/event"
8 "reasonix/internal/permission"
9 )
10
11 type failingPromptAnswerSink struct{ err error }
12
13 func (s failingPromptAnswerSink) Emit(event.Event) {}
14 func (s failingPromptAnswerSink) EmitChecked(e event.Event) error {
15 if e.Kind == event.PromptAnswered {
16 return s.err
17 }
18 return nil
19 }
20
21 func TestApprovalResolutionRemainsPendingWhenPersistenceFails(t *testing.T) {
22 m := newApprovalManager(permission.Policy{}, ToolApprovalAsk, 0)
23 id, reply := m.register("write_file", "settings.json", "test")
24 want := errors.New("ledger unavailable")
25
26 if _, ok, err := m.resolveAfter(id, func(p pendingApproval) error {
27 if p.reply != reply {
28 t.Fatal("persistence callback received a different pending approval")
29 }
30 return want
31 }); !errors.Is(err, want) || ok {
32 t.Fatalf("first resolve = ok:%v err:%v, want retryable persistence failure", ok, err)
33 }
34 if got := m.peek(id); got.reply != reply {
35 t.Fatal("failed persistence removed the pending approval")
36 }
37 if got, ok, err := m.resolveAfter(id, nil); err != nil || !ok || got.reply != reply {
38 t.Fatalf("retry resolve = pending:%+v ok:%v err:%v", got, ok, err)
39 }
40 }
41
42 func TestAskResolutionRemainsPendingWhenPersistenceFails(t *testing.T) {
43 m := newApprovalManager(permission.Policy{}, ToolApprovalAsk, 0)
44 id, reply := m.registerAsk(askProbeQuestions())
45 m.markAskEmitted(id)
46 want := errors.New("ledger unavailable")
47
48 if _, ok, err := m.resolveAskAfter(id, func(p pendingAsk) error {
49 if p.reply != reply {
50 t.Fatal("persistence callback received a different pending ask")
51 }
52 return want
53 }); !errors.Is(err, want) || ok {
54 t.Fatalf("first answer = ok:%v err:%v, want retryable persistence failure", ok, err)
55 }
56 if _, pending := m.snapshotPrompts(); len(pending) != 1 || pending[0].ID != id {
57 t.Fatalf("failed persistence removed ask %q: %+v", id, pending)
58 }
59 if got, ok, err := m.resolveAskAfter(id, nil); err != nil || !ok || got.reply != reply {
60 t.Fatalf("retry answer = pending:%+v ok:%v err:%v", got, ok, err)
61 }
62 }
63
64 func TestClearKindDropsResolutionReservation(t *testing.T) {
65 m := newApprovalManager(permission.Policy{}, ToolApprovalAsk, 0)
66 id, _ := m.registerDecisionKind("recovery", "", "", true, true, "recovery", nil)
67 m.approvalResolutions[id] = newPromptResolution()
68
69 m.clearKind("recovery")
70 if _, ok := m.approvalResolutions[id]; ok {
71 t.Fatal("clearKind left a stale two-phase resolution reservation")
72 }
73 }
74
75 func TestConcurrentDuplicateAskWaitsForSamePersistenceResult(t *testing.T) {
76 m := newApprovalManager(permission.Policy{}, ToolApprovalAsk, 0)
77 id, _ := m.registerAsk(askProbeQuestions())
78 m.markAskEmitted(id)
79 want := errors.New("ledger unavailable")
80 entered := make(chan struct{})
81 release := make(chan struct{})
82 firstDone := make(chan error, 1)
83 secondDone := make(chan error, 1)
84
85 go func() {
86 _, _, err := m.resolveAskAfter(id, func(pendingAsk) error {
87 close(entered)
88 <-release
89 return want
90 })
91 firstDone <- err
92 }()
93 <-entered
94 m.mu.Lock()
95 attempt := m.askResolutions[id]
96 m.mu.Unlock()
97 if attempt == nil {
98 t.Fatal("first answer did not reserve the prompt")
99 }
100 go func() {
101 _, _, err := m.resolveAskAfter(id, nil)
102 secondDone <- err
103 }()
104 <-attempt.joined
105 close(release)
106 if err := <-firstDone; !errors.Is(err, want) {
107 t.Fatalf("first answer error = %v, want %v", err, want)
108 }
109 if err := <-secondDone; !errors.Is(err, want) {
110 t.Fatalf("duplicate answer error = %v, want same %v", err, want)
111 }
112 if _, pending := m.snapshotPrompts(); len(pending) != 1 || pending[0].ID != id {
113 t.Fatalf("failed duplicate transaction did not remain retryable: %+v", pending)
114 }
115 }
116
117 func TestApproveCheckedReturnsPersistenceFailureWithoutReleasingTool(t *testing.T) {
118 want := errors.New("ledger unavailable")
119 c := &Controller{
120 sink: failingPromptAnswerSink{err: want},
121 approval: newApprovalManager(permission.Policy{}, ToolApprovalAsk, 0),
122 }
123 id, reply := c.approval.register("bash", "write output", "test")
124 if err := c.approveChecked(id, true, false, false); !errors.Is(err, want) {
125 t.Fatalf("approveChecked error = %v, want %v", err, want)
126 }
127 if got := c.approval.peek(id); got.reply != reply {
128 t.Fatal("failed answer persistence removed the pending approval")
129 }
130 select {
131 case <-reply:
132 t.Fatal("tool resumed before PromptAnswered was durable")
133 default:
134 }
135
136 c.sink = event.Discard
137 if err := c.approveChecked(id, true, false, false); err != nil {
138 t.Fatalf("retry approveChecked: %v", err)
139 }
140 select {
141 case resolved := <-reply:
142 if !resolved.allow {
143 t.Fatal("retry did not preserve approval outcome")
144 }
145 default:
146 t.Fatal("durable retry did not release the tool")
147 }
148 if err := c.approveChecked(id, true, false, false); err != nil {
149 t.Fatalf("duplicate approval was not idempotent: %v", err)
150 }
151 }
152
152 lines GO