返回 DeepSeek-Reasonix
bot_event_sink_test.go
根目录 / desktop / bot_event_sink_test.go
1 package main
2
3 import (
4 "context"
5 "io"
6 "log/slog"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "reasonix/internal/bot"
13 "reasonix/internal/event"
14 )
15
16 type desktopForwardTestAdapter struct {
17 platform bot.Platform
18 name string
19 messages chan bot.InboundMessage
20 entered chan struct{}
21 release chan struct{}
22 sent chan bot.OutboundMessage
23 once sync.Once
24 }
25
26 func newDesktopForwardTestAdapter() *desktopForwardTestAdapter {
27 return &desktopForwardTestAdapter{
28 platform: bot.PlatformFeishu,
29 name: "forward-test",
30 messages: make(chan bot.InboundMessage),
31 entered: make(chan struct{}),
32 release: make(chan struct{}),
33 sent: make(chan bot.OutboundMessage, 8),
34 }
35 }
36
37 func (a *desktopForwardTestAdapter) Platform() bot.Platform { return a.platform }
38 func (a *desktopForwardTestAdapter) Name() string { return a.name }
39 func (a *desktopForwardTestAdapter) Start(context.Context) error {
40 return nil
41 }
42 func (a *desktopForwardTestAdapter) Stop() error { return nil }
43 func (a *desktopForwardTestAdapter) SendTyping(context.Context, string) error {
44 return nil
45 }
46 func (a *desktopForwardTestAdapter) Messages() <-chan bot.InboundMessage {
47 return a.messages
48 }
49 func (a *desktopForwardTestAdapter) Send(ctx context.Context, msg bot.OutboundMessage) (bot.SendResult, error) {
50 a.once.Do(func() { close(a.entered) })
51 select {
52 case <-a.release:
53 case <-ctx.Done():
54 return bot.SendResult{}, ctx.Err()
55 }
56 a.sent <- msg
57 return bot.SendResult{MessageID: "sent"}, nil
58 }
59
60 func newDesktopForwardTestRuntime(adapter bot.Adapter) *desktopBotRuntime {
61 gw := bot.NewGatewayWithAdapterBindings(bot.GatewayConfig{}, []bot.AdapterBinding{{
62 ID: "feishu-lark",
63 Domain: "lark",
64 Platform: bot.PlatformFeishu,
65 Adapter: adapter,
66 }}, slog.New(slog.NewTextHandler(io.Discard, nil)))
67 return &desktopBotRuntime{gw: gw}
68 }
69
70 func TestBotEventForwarderDoesNotBlockEventEmissionOnSlowSend(t *testing.T) {
71 adapter := newDesktopForwardTestAdapter()
72 forwarder := newBotEventForwarder(newDesktopForwardTestRuntime(adapter), []botForwardTarget{{
73 ConnID: "feishu-lark",
74 Domain: "lark",
75 ChatID: "oc-group-1",
76 ChatType: bot.ChatGroup,
77 }})
78
79 done := make(chan struct{})
80 go func() {
81 forwarder.Emit(event.Event{Kind: event.Text, Text: strings.Repeat("x", 400)})
82 close(done)
83 }()
84
85 select {
86 case <-done:
87 case <-time.After(500 * time.Millisecond):
88 t.Fatal("Emit blocked behind slow bot send")
89 }
90 select {
91 case <-adapter.entered:
92 case <-time.After(500 * time.Millisecond):
93 t.Fatal("adapter send did not start")
94 }
95
96 forwarder.Close()
97 close(adapter.release)
98 select {
99 case <-adapter.sent:
100 case <-time.After(500 * time.Millisecond):
101 t.Fatal("queued bot message was not sent after adapter release")
102 }
103 }
104
105 func TestBotEventForwarderApprovalNoticeDoesNotExposeReplyID(t *testing.T) {
106 adapter := newDesktopForwardTestAdapter()
107 close(adapter.release)
108 forwarder := newBotEventForwarder(newDesktopForwardTestRuntime(adapter), []botForwardTarget{{
109 ConnID: "feishu-lark",
110 Domain: "lark",
111 ChatID: "oc-group-1",
112 ChatType: bot.ChatGroup,
113 }})
114
115 forwarder.Emit(event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{
116 ID: "approval-1",
117 Tool: "shell",
118 Subject: "run command",
119 }})
120 forwarder.Close()
121
122 select {
123 case msg := <-adapter.sent:
124 if strings.Contains(msg.Text, "approval-1") || strings.Contains(msg.Text, "/approve") {
125 t.Fatalf("approval notice exposed unusable reply routing: %q", msg.Text)
126 }
127 if !strings.Contains(msg.Text, "桌面") {
128 t.Fatalf("approval notice = %q, want desktop guidance", msg.Text)
129 }
130 case <-time.After(500 * time.Millisecond):
131 t.Fatal("approval notice was not sent")
132 }
133 }
134
135 func TestBotEventForwarderSuppressesOperatorNoticesWithoutHidingUserWarnings(t *testing.T) {
136 forwarder := &botEventForwarder{
137 runtime: &desktopBotRuntime{},
138 targets: []botForwardTarget{{
139 ConnID: "feishu-lark",
140 Domain: "lark",
141 ChatID: "oc-group-1",
142 ChatType: bot.ChatGroup,
143 }},
144 queue: make(chan string, 2),
145 }
146
147 forwarder.Emit(event.Event{
148 Kind: event.Notice,
149 Level: event.LevelWarn,
150 Audience: event.NoticeAudienceOperator,
151 Code: event.NoticeCodeSessionRecoveryForked,
152 Text: "local session maintenance",
153 })
154 forwarder.Emit(event.Event{
155 Kind: event.Notice,
156 Level: event.LevelWarn,
157 Text: "please retry the user action",
158 })
159
160 if got := len(forwarder.queue); got != 1 {
161 t.Fatalf("queued messages = %d, want only the actionable user warning", got)
162 }
163 if got := <-forwarder.queue; got != "⚠️ please retry the user action" {
164 t.Fatalf("queued message = %q, want the ordinary user warning", got)
165 }
166 }
167
167 lines GO