返回 DeepSeek-Reasonix
replay_notify_test.go
根目录 / internal / control / replay_notify_test.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "sync"
7 "testing"
8
9 "reasonix/internal/config"
10 "reasonix/internal/event"
11 "reasonix/internal/notify"
12 )
13
14 type countingSender struct {
15 mu sync.Mutex
16 sent []notify.Message
17 }
18
19 func (s *countingSender) Send(m notify.Message) error {
20 s.mu.Lock()
21 defer s.mu.Unlock()
22 s.sent = append(s.sent, m)
23 return nil
24 }
25
26 func (s *countingSender) count() int {
27 s.mu.Lock()
28 defer s.mu.Unlock()
29 return len(s.sent)
30 }
31
32 func notifyingSink(sender notify.Sender, reqs chan<- event.Event) event.Sink {
33 return notify.NewSink(event.FuncSink(func(e event.Event) {
34 if e.Kind == event.ApprovalRequest || e.Kind == event.AskRequest {
35 reqs <- e
36 }
37 }), sender, config.NotificationsConfig{Enabled: true, ApprovalRequest: true, AskRequest: true})
38 }
39
40 func approveAsync(c *Controller) <-chan struct{} {
41 done := make(chan struct{})
42 go func() {
43 defer close(done)
44 _, _, _ = gateApprover{c}.Approve(context.Background(), "bash", "go test ./...", json.RawMessage(`{"command":"go test ./..."}`))
45 }()
46 return done
47 }
48
49 // The desktop replays a blocked prompt whenever it reconciles a tab, through
50 // the same notification-wrapped sink as the original request. A notification
51 // announces a request; a replay only rebuilds its card (#9156).
52 func TestReplayedPromptDoesNotNotifyAgain(t *testing.T) {
53 sender := &countingSender{}
54 reqs := make(chan event.Event, 8)
55 c := newOwnedTestController(t, Options{Sink: notifyingSink(sender, reqs)})
56
57 done := approveAsync(c)
58 first := <-reqs
59 for range 3 {
60 c.ReplayPendingPrompts()
61 if replayed := <-reqs; !replayed.Replayed {
62 t.Fatal("a replayed approval is not marked as a replay")
63 }
64 }
65 if got := sender.count(); got != 1 {
66 t.Fatalf("notifications for one approval replayed 3 times = %d, want 1", got)
67 }
68 c.Approve(first.Approval.ID, true, false, false)
69 <-done
70
71 askDone := make(chan struct{})
72 go func() {
73 defer close(askDone)
74 _, _ = c.Ask(context.Background(), []event.AskQuestion{{ID: "q1", Prompt: "Which?", Options: []event.AskOption{{Label: "A"}}}})
75 }()
76 ask := <-reqs
77 c.ReplayPendingPrompts()
78 <-reqs
79 if got := sender.count(); got != 2 {
80 t.Fatalf("notifications after a new ask and one replay = %d, want 2", got)
81 }
82 c.AnswerQuestion(ask.Ask.ID, []event.AskAnswer{{QuestionID: "q1", Selected: []string{"A"}}})
83 <-askDone
84 }
85
86 // The TUI keeps one notification sink across /reload and model switches while
87 // each rebuilt controller numbers its prompts from "1" again. Two requests are
88 // two requests even when they share an id.
89 func TestRebuiltControllersSharingASinkEachNotify(t *testing.T) {
90 sender := &countingSender{}
91 reqs := make(chan event.Event, 8)
92 sink := notifyingSink(sender, reqs)
93 for i := range 2 {
94 c := newOwnedTestController(t, Options{Sink: sink})
95 done := approveAsync(c)
96 req := <-reqs
97 if req.Approval.ID != "1" {
98 t.Fatalf("controller %d first approval id = %q, want 1", i, req.Approval.ID)
99 }
100 c.Approve(req.Approval.ID, true, false, false)
101 <-done
102 }
103 if got := sender.count(); got != 2 {
104 t.Fatalf("notifications for two controllers' approval 1 = %d, want 2", got)
105 }
106 }
107
107 lines GO