返回 DeepSeek-Reasonix
sink_test.go
根目录 / internal / notify / sink_test.go
1 package notify
2
3 import (
4 "errors"
5 "testing"
6
7 "reasonix/internal/config"
8 "reasonix/internal/event"
9 )
10
11 var errTestFailure = errors.New("failed")
12
13 type recordSink struct {
14 events []event.Kind
15 recovery []event.ProtocolRecoveryAudit
16 }
17
18 func (s *recordSink) Emit(e event.Event) {
19 s.events = append(s.events, e.Kind)
20 }
21
22 func (s *recordSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) {
23 s.recovery = append(s.recovery, a)
24 }
25
26 type recordSender struct {
27 messages []Message
28 }
29
30 func TestSinkForwardsProtocolRecoveryWithoutNotification(t *testing.T) {
31 inner := &recordSink{}
32 sender := &recordSender{}
33 sink := NewSink(inner, sender, config.NotificationsConfig{Enabled: true, TurnDone: true})
34
35 event.RecordProtocolRecovery(sink, event.ProtocolRecoveryAudit{Kind: event.ProtocolRecoveryMissingReasoningFallback})
36
37 if len(inner.recovery) != 1 || inner.recovery[0].Kind != event.ProtocolRecoveryMissingReasoningFallback {
38 t.Fatalf("forwarded protocol recovery = %+v", inner.recovery)
39 }
40 if len(sender.messages) != 0 {
41 t.Fatalf("protocol recovery sent user notification: %+v", sender.messages)
42 }
43 }
44
45 func (s *recordSender) Send(m Message) error {
46 s.messages = append(s.messages, m)
47 return nil
48 }
49
50 func TestSinkForwardsEventsAndSendsConfiguredNotifications(t *testing.T) {
51 inner := &recordSink{}
52 sender := &recordSender{}
53 sink := NewSink(inner, sender, config.NotificationsConfig{
54 Enabled: true,
55 TurnDone: true,
56 ApprovalRequest: true,
57 AskRequest: true,
58 })
59
60 sink.Emit(event.Event{Kind: event.ApprovalRequest})
61 sink.Emit(event.Event{Kind: event.AskRequest})
62 sink.Emit(event.Event{Kind: event.TurnDone})
63
64 if len(inner.events) != 3 {
65 t.Fatalf("forwarded events = %d, want 3", len(inner.events))
66 }
67 if len(sender.messages) != 3 {
68 t.Fatalf("notifications = %d, want 3", len(sender.messages))
69 }
70 if sender.messages[0].Body != "Approval needed" {
71 t.Errorf("approval notification body = %q", sender.messages[0].Body)
72 }
73 if sender.messages[1].Body != "Question needs your answer" {
74 t.Errorf("ask notification body = %q", sender.messages[1].Body)
75 }
76 if sender.messages[2].Body != "Turn finished" {
77 t.Errorf("turn notification body = %q", sender.messages[2].Body)
78 }
79 }
80
81 func TestSinkSkipsNotificationsWhenDisabled(t *testing.T) {
82 inner := &recordSink{}
83 sender := &recordSender{}
84 sink := NewSink(inner, sender, config.NotificationsConfig{
85 Enabled: false,
86 TurnDone: true,
87 ApprovalRequest: true,
88 AskRequest: true,
89 })
90
91 sink.Emit(event.Event{Kind: event.TurnDone})
92
93 if len(inner.events) != 1 {
94 t.Fatalf("forwarded events = %d, want 1", len(inner.events))
95 }
96 if len(sender.messages) != 0 {
97 t.Fatalf("notifications = %d, want 0", len(sender.messages))
98 }
99 }
100
101 func TestSendEventUsesSameNotificationRules(t *testing.T) {
102 sender := &recordSender{}
103
104 SendEvent(sender, config.NotificationsConfig{Enabled: true, TurnDone: true}, event.Event{Kind: event.TurnDone})
105
106 if len(sender.messages) != 1 {
107 t.Fatalf("notifications = %d, want 1", len(sender.messages))
108 }
109 if sender.messages[0].Body != "Turn finished" {
110 t.Errorf("notification body = %q", sender.messages[0].Body)
111 }
112 }
113
114 func TestTurnDoneWithErrorSendsFailureNotification(t *testing.T) {
115 sender := &recordSender{}
116
117 SendEvent(sender, config.NotificationsConfig{Enabled: true, TurnDone: true}, event.Event{Kind: event.TurnDone, Err: errTestFailure})
118
119 if len(sender.messages) != 1 {
120 t.Fatalf("notifications = %d, want 1", len(sender.messages))
121 }
122 if sender.messages[0].Body != "Turn failed" {
123 t.Errorf("notification body = %q", sender.messages[0].Body)
124 }
125 }
126
127 func TestSinkHonorsPerEventConfig(t *testing.T) {
128 sender := &recordSender{}
129 sink := NewSink(&recordSink{}, sender, config.NotificationsConfig{
130 Enabled: true,
131 TurnDone: false,
132 ApprovalRequest: true,
133 AskRequest: false,
134 })
135
136 sink.Emit(event.Event{Kind: event.TurnDone})
137 sink.Emit(event.Event{Kind: event.ApprovalRequest})
138 sink.Emit(event.Event{Kind: event.AskRequest})
139
140 if len(sender.messages) != 1 {
141 t.Fatalf("notifications = %d, want 1", len(sender.messages))
142 }
143 if sender.messages[0].Body != "Approval needed" {
144 t.Errorf("notification body = %q", sender.messages[0].Body)
145 }
146 }
147
147 lines GO