13 "github.com/stretchr/testify/require"
16 func TestOwnerTalksToTheAgentThroughTelegram(t *testing.T) {
17 server := newFakeResponses(t)
18 server.reply = func(w http.ResponseWriter, call fakeCall) {
19 writeEvents(w, completedEvent([]any{messageItem("Hello from the agent.")}, 100, 0, 5))
22 telegram := newFakeBot(t)
23 bot, _ := startTestBot(t, telegram)
24 socket := startTestDaemonWithBot(t, server, bot)
25 terminal := startTestClient(t, socket)
26 terminal.WaitFor(t, "[connected to OneChat]")
28 telegram.Add(botUpdate{
30 Message: ownerMessage("hi from the phone"),
33 terminal.WaitFor(t, "[telegram] hi from the phone\n[working]\nHello from the agent.\n[done]")
34 waitForSent(t, telegram, "Hello from the agent.")
35 require.True(t, slices.ContainsFunc(telegram.Calls(), func(call fakeBotCall) bool {
36 return call.method == "sendChatAction" && call.params["action"] == "typing"
39 terminal.Type("hi from the desk")
40 require.Equal(t, []string{"Hello from the agent.", "[terminal] hi from the desk", "Hello from the agent."}, waitForSent(t, telegram, "Hello from the agent.", "[terminal] hi from the desk", "Hello from the agent."))
43 func TestBotIgnoresEveryoneButTheOwner(t *testing.T) {
44 telegram := newFakeBot(t)
45 bot, logs := startTestBot(t, telegram)
46 socket := startTestDaemonWithBot(t, newFakeResponses(t), bot)
47 terminal := startTestClient(t, socket)
48 terminal.WaitFor(t, "[connected to OneChat]")
50 group := ownerMessage("secret in a group")
56 stranger := ownerMessage("secret of a stranger")
60 unsigned := ownerMessage("secret with no sender")
63 for id, message := range []*botMessage{group, stranger, unsigned, nil} {
64 telegram.Add(botUpdate{
65 UpdateID: int64(10 + id),
70 require.Eventually(t, func() bool {
71 offset, err := loadOffset(bot.offsetPath)
72 return err == nil && offset == 14
73 }, 10*time.Second, time.Millisecond)
75 require.Empty(t, telegram.Sent())
76 require.NotContains(t, terminal.Output(), "secret")
77 require.NotContains(t, logs.String(), "secret")
78 require.Contains(t, logs.String(), "update ignored")
81 func TestBotAnswersThatOnlyTextIsSupported(t *testing.T) {
82 telegram := newFakeBot(t)
83 bot, _ := startTestBot(t, telegram)
84 startTestDaemonWithBot(t, newFakeResponses(t), bot)
86 telegram.Add(botUpdate{
88 Message: ownerMessage(""),
91 waitForSent(t, telegram, textOnly)
94 func TestStopFromTelegramInterruptsTheTurn(t *testing.T) {
95 server := newFakeResponses(t)
96 arrived := make(chan struct{})
97 release := make(chan struct{})
102 server.reply = func(w http.ResponseWriter, call fakeCall) {
107 telegram := newFakeBot(t)
108 bot, _ := startTestBot(t, telegram)
109 startTestDaemonWithBot(t, server, bot)
111 telegram.Add(botUpdate{
113 Message: ownerMessage("a long task"),
117 telegram.Add(botUpdate{
119 Message: ownerMessage(stopCommand),
122 waitForSent(t, telegram, "Error: interrupted")
124 telegram.Add(botUpdate{
126 Message: ownerMessage(stopCommand),
129 waitForSent(t, telegram, "Error: interrupted", "No turn runs.")
132 func TestBotStartsAtTheStoredOffset(t *testing.T) {
133 server := newFakeResponses(t)
134 server.reply = func(w http.ResponseWriter, call fakeCall) {
135 writeEvents(w, completedEvent([]any{messageItem("ok")}, 100, 0, 5))
138 telegram := newFakeBot(t)
139 bot, _ := startTestBot(t, telegram)
140 require.NoError(t, saveOffset(bot.offsetPath, 10))
142 telegram.Add(botUpdate{
144 Message: ownerMessage("handled before the restart"),
147 telegram.Add(botUpdate{
149 Message: ownerMessage("new after the restart"),
152 socket := startTestDaemonWithBot(t, server, bot)
153 terminal := startTestClient(t, socket)
154 terminal.WaitFor(t, "[connected to OneChat]")
155 waitForSent(t, telegram, "ok")
157 require.Equal(t, float64(10), telegram.Calls()[0].params["offset"])
158 require.NotContains(t, terminal.Output(), "handled before the restart")
160 require.Eventually(t, func() bool {
161 offset, err := loadOffset(bot.offsetPath)
162 return err == nil && offset == 11
163 }, 10*time.Second, time.Millisecond)
166 func TestBotTriesAgainAfterFailures(t *testing.T) {
167 server := newFakeResponses(t)
168 server.reply = func(w http.ResponseWriter, call fakeCall) {
169 writeEvents(w, completedEvent([]any{messageItem("ok")}, 100, 0, 5))
172 telegram := newFakeBot(t)
173 failures := map[string]int{
178 telegram.fail = func(call fakeBotCall) (int, string) {
179 if failures[call.method] == 0 {
183 failures[call.method]--
184 if call.method == "sendMessage" {
185 return http.StatusTooManyRequests, "Too Many Requests: retry after 1"
188 return http.StatusBadGateway, "Bad Gateway"
191 bot, logs := startTestBot(t, telegram)
192 startTestDaemonWithBot(t, server, bot)
194 telegram.Add(botUpdate{
196 Message: ownerMessage("hello"),
199 waitForSent(t, telegram, "ok")
200 require.Contains(t, logs.String(), "answered 502")
201 require.Contains(t, logs.String(), "answered 429")
202 require.NotContains(t, logs.String(), testBotToken)
205 func TestDeliverTriesAgainAfterATimeout(t *testing.T) {
206 telegram := newFakeBot(t)
208 telegram.stall = func(call fakeBotCall) bool {
209 if call.method != "sendMessage" || stalls == 0 {
218 bot, logs := startTestBot(t, telegram)
219 bot.api.sendTimeout = 50 * time.Millisecond
220 bot.deliver(t.Context(), "hello")
222 require.Equal(t, []string{"hello"}, telegram.Sent())
223 require.Len(t, telegram.Calls(), 2)
224 require.Contains(t, logs.String(), "no valid answer")
225 require.NotContains(t, logs.String(), testBotToken)
228 func TestBotLogsNoTokenWhenTheNetworkFails(t *testing.T) {
229 telegram := newFakeBot(t)
232 bot, logs := startTestBot(t, telegram)
233 startTestDaemonWithBot(t, newFakeResponses(t), bot)
235 require.Eventually(t, func() bool {
236 return strings.Count(logs.String(), "no valid answer") >= 2
237 }, 10*time.Second, time.Millisecond)
238 require.NotContains(t, logs.String(), testBotToken)
241 func TestSplitMessage(t *testing.T) {
242 testCases := []struct {
249 name: "short text stays whole",
252 parts: []string{"one two"},
255 name: "cut after the last line break",
256 text: "first line\nsecond line",
258 parts: []string{"first line\n", "second line"},
261 name: "cut after the last space without a line break",
262 text: "aaaa bbbb cccc",
264 parts: []string{"aaaa bbbb ", "cccc"},
267 name: "hard cut of one long word",
270 parts: []string{"abcd", "efgh", "ij"},
273 name: "a character outside the BMP counts two units and is not cut",
274 text: "ab\U0001F600cd",
276 parts: []string{"ab", "\U0001F600c", "d"},
280 for _, tc := range testCases {
281 t.Run(tc.name, func(t *testing.T) {
282 require.Equal(t, tc.parts, splitMessage(tc.text, tc.limit))
286 long := strings.Repeat("word ", 2000)
287 parts := splitMessage(long, messageUnits)
288 require.Len(t, parts, 3)
289 require.Equal(t, long, strings.Join(parts, ""))
292 func TestLoadBotConfig(t *testing.T) {
293 testCases := []struct {
300 name: "owner file is read",
301 content: `{"token": "` + testBotToken + `", "owner": 4242}`,
305 name: "file that others can read is refused",
306 content: `{"token": "` + testBotToken + `", "owner": 4242}`,
308 errorText: "mode must be 0600",
311 name: "token with a path character is refused",
312 content: `{"token": "123:abc/../x", "owner": 4242}`,
314 errorText: "form of a bot token",
317 name: "missing owner is refused",
318 content: `{"token": "` + testBotToken + `"}`,
320 errorText: "numeric Telegram user ID",
323 name: "unknown field is refused",
324 content: `{"token": "` + testBotToken + `", "owner": 4242, "chat": 1}`,
326 errorText: "not valid JSON",
330 for _, tc := range testCases {
331 t.Run(tc.name, func(t *testing.T) {
333 path := filepath.Join(dir, botConfigFileName)
334 require.NoError(t, os.WriteFile(path, []byte(tc.content), tc.mode))
335 require.NoError(t, os.Chmod(path, tc.mode))
337 config, found, err := loadBotConfig(dir)
338 if tc.errorText != "" {
339 require.ErrorContains(t, err, tc.errorText)
340 require.NotContains(t, err.Error(), testBotToken)
345 require.NoError(t, err)
346 require.True(t, found)
347 require.Equal(t, botConfig{
354 _, found, err := loadBotConfig(t.TempDir())
355 require.NoError(t, err)
356 require.False(t, found)
359 // startTestBot returns a bot of testOwner on server, with its offset file in a
360 // temporary directory and short waits, and the buffer of its log.
361 func startTestBot(t *testing.T, server *fakeBot) (*telegramBot, *syncBuffer) {
364 logs := &syncBuffer{}
365 bot := newTelegramBot(newBotAPI(server.URL, testBotToken), testOwner, filepath.Join(t.TempDir(), offsetFileName), log.New(logs, "", 0))
366 bot.minWait = time.Millisecond
367 bot.maxWait = 10 * time.Millisecond
372 // waitForSent waits until the bot sent the texts in order, and returns them.
373 func waitForSent(t *testing.T, server *fakeBot, texts ...string) []string {
376 require.Eventually(t, func() bool {
377 sent := server.Sent()
378 return len(sent) >= len(texts) && slices.Equal(sent[len(sent)-len(texts):], texts)
379 }, 10*time.Second, time.Millisecond, "sent so far: %q", server.Sent())