13 "github.com/stretchr/testify/require"
16 func TestOwnerTalksToTheAgentThroughTelegram(t *testing.T) {
17 server := newModelFake(t)
18 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
19 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Hello from the agent.")}, 100, 0, 5))
22 telegram := newTelegramFake(t)
23 bot, _ := telegramStartTestBot(t, telegram)
24 socket := daemonStartTestWithBot(t, server, bot)
25 terminal := terminalStartTestClient(t, socket)
26 terminal.WaitFor(t, "[connected to OneChat]")
28 telegram.Add(TelegramUpdate{
30 Message: telegramOwnerMessage("hi from the phone"),
33 terminal.WaitFor(t, "[telegram] hi from the phone\n[working]\nHello from the agent.\n[done]")
34 telegramWaitForSent(t, telegram, "Hello from the agent.")
35 require.True(t, slices.ContainsFunc(telegram.Calls(), func(call TelegramFakeCall) 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."}, telegramWaitForSent(t, telegram, "Hello from the agent.", "[terminal] hi from the desk", "Hello from the agent."))
43 func TestBotIgnoresEveryoneButTheOwner(t *testing.T) {
44 telegram := newTelegramFake(t)
45 bot, logs := telegramStartTestBot(t, telegram)
46 socket := daemonStartTestWithBot(t, newModelFake(t), bot)
47 terminal := terminalStartTestClient(t, socket)
48 terminal.WaitFor(t, "[connected to OneChat]")
50 group := telegramOwnerMessage("secret in a group")
51 group.Chat = TelegramChat{
56 stranger := telegramOwnerMessage("secret of a stranger")
60 unsigned := telegramOwnerMessage("secret with no sender")
63 for id, message := range []*TelegramMessage{group, stranger, unsigned, nil} {
64 telegram.Add(TelegramUpdate{
65 UpdateID: int64(10 + id),
70 require.Eventually(t, func() bool {
71 offset, err := telegramLoadOffset(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(), "<6>message ignored")
81 func TestBotAnswersThatOnlyTextIsSupported(t *testing.T) {
82 telegram := newTelegramFake(t)
83 bot, _ := telegramStartTestBot(t, telegram)
84 daemonStartTestWithBot(t, newModelFake(t), bot)
86 telegram.Add(TelegramUpdate{
88 Message: telegramOwnerMessage(""),
91 telegramWaitForSent(t, telegram, telegramTextOnly)
94 func TestStopFromTelegramInterruptsTheTurn(t *testing.T) {
95 server := newModelFake(t)
96 arrived := make(chan struct{})
97 release := make(chan struct{})
102 server.reply = func(_ http.ResponseWriter, _ ModelFakeCall) {
107 telegram := newTelegramFake(t)
108 bot, _ := telegramStartTestBot(t, telegram)
109 daemonStartTestWithBot(t, server, bot)
111 telegram.Add(TelegramUpdate{
113 Message: telegramOwnerMessage("a long task"),
117 telegram.Add(TelegramUpdate{
119 Message: telegramOwnerMessage(stopCommand),
122 telegramWaitForSent(t, telegram, "Error: interrupted")
124 telegram.Add(TelegramUpdate{
126 Message: telegramOwnerMessage(stopCommand),
129 telegramWaitForSent(t, telegram, "Error: interrupted", "No turn runs.")
131 telegram.Add(TelegramUpdate{
133 Message: telegramOwnerMessage(stopCommand + " BlueFox"),
136 telegramWaitForSent(t, telegram, "No turn runs.", "No subagent BlueFox runs.")
139 func TestBotShowsOnlyTheStartAndTheEndOfASubagent(t *testing.T) {
140 telegram := newTelegramFake(t)
141 server := newModelFake(t)
142 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
143 last := subagentTestLastItem(call)
145 case subagentTestCall(call):
146 // The report comes after the reply of the turn of the spawn.
147 require.Eventually(t, func() bool {
148 return slices.Contains(telegram.Sent(), "Started.")
149 }, 10*time.Second, time.Millisecond)
150 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("There are 3 files.")}, 100, 0, 5))
151 case last["type"] == "function_call_output":
152 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Started.")}, 100, 0, 5))
153 case strings.HasPrefix(subagentTestText(last), "["):
154 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("It counted 3 files.")}, 100, 0, 5))
156 modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "spawn", `{"tasks":["Count the files."],"effort":null}`)}, 100, 0, 5))
160 bot, _ := telegramStartTestBot(t, telegram)
161 daemonStartTestWithBot(t, server, bot)
162 telegram.Add(TelegramUpdate{
164 Message: telegramOwnerMessage("Count the files in a subagent."),
167 require.Eventually(t, func() bool {
168 return slices.Contains(telegram.Sent(), "It counted 3 files.")
169 }, 10*time.Second, time.Millisecond)
171 sent := telegram.Sent()
172 name := strings.TrimSuffix(strings.TrimPrefix(sent[0], "["), "] started")
173 require.True(t, subagentNameValid(name), sent[0])
174 require.Equal(t, []string{
175 "[" + name + "] started",
177 "[" + name + "] finished",
178 "It counted 3 files.",
182 func TestBotSkipsTheWorkOfWhatsAppAndTheSchedules(t *testing.T) {
184 ready: make(chan struct{}, 1),
189 Source: whatsappSource,
190 Text: "[WhatsApp] Ana Souza",
194 Source: scheduleSource,
195 Text: "[Schedule 1] Sunday 2026-11-08 20:05 -03:00, once",
197 require.Empty(t, bot.queue)
200 func TestBotStartsAtTheStoredOffset(t *testing.T) {
201 server := newModelFake(t)
202 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
203 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
206 telegram := newTelegramFake(t)
207 bot, _ := telegramStartTestBot(t, telegram)
208 require.NoError(t, telegramSaveOffset(bot.offsetPath, 10))
210 telegram.Add(TelegramUpdate{
212 Message: telegramOwnerMessage("handled before the restart"),
215 telegram.Add(TelegramUpdate{
217 Message: telegramOwnerMessage("new after the restart"),
220 socket := daemonStartTestWithBot(t, server, bot)
221 terminal := terminalStartTestClient(t, socket)
222 terminal.WaitFor(t, "[connected to OneChat]")
223 telegramWaitForSent(t, telegram, "ok")
225 require.InDelta(t, float64(10), telegram.Calls()[0].params["offset"], 0)
226 require.NotContains(t, terminal.Output(), "handled before the restart")
228 require.Eventually(t, func() bool {
229 offset, err := telegramLoadOffset(bot.offsetPath)
230 return err == nil && offset == 11
231 }, 10*time.Second, time.Millisecond)
234 func TestBotTriesAgainAfterFailures(t *testing.T) {
235 server := newModelFake(t)
236 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
237 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
240 telegram := newTelegramFake(t)
241 failures := map[string]int{
243 "sendRichMessage": 1,
246 telegram.fail = func(call TelegramFakeCall) (int, string) {
247 if failures[call.method] == 0 {
251 failures[call.method]--
252 if call.method == "sendRichMessage" {
253 return http.StatusTooManyRequests, "Too Many Requests: retry after 1"
256 return http.StatusBadGateway, "Bad Gateway"
259 bot, logs := telegramStartTestBot(t, telegram)
260 daemonStartTestWithBot(t, server, bot)
262 telegram.Add(TelegramUpdate{
264 Message: telegramOwnerMessage("hello"),
267 telegramWaitForSent(t, telegram, "ok")
268 require.Contains(t, logs.String(), "<4>receive failed retry=1ms: 502")
269 require.Contains(t, logs.String(), "<6>receive back after=")
270 require.Contains(t, logs.String(), "<4>send failed retry=1s: 429")
272 // The bot logs the end of the outage after the send returns, so the line
273 // can come after the fake server has the message.
274 require.Eventually(t, func() bool {
275 return strings.Contains(logs.String(), "<6>send back after=")
276 }, 10*time.Second, time.Millisecond)
278 require.NotContains(t, logs.String(), telegramTestToken)
281 func TestDeliverTriesAgainAfterATimeout(t *testing.T) {
282 telegram := newTelegramFake(t)
284 telegram.stall = func(call TelegramFakeCall) bool {
285 if call.method != "sendMessage" || stalls == 0 {
294 bot, logs := telegramStartTestBot(t, telegram)
295 bot.api.sendTimeout = 50 * time.Millisecond
296 bot.deliver(t.Context(), TelegramPart{
300 require.Equal(t, []string{"hello"}, telegram.Sent())
301 require.Len(t, telegram.Calls(), 2)
302 require.Contains(t, logs.String(), "<4>send failed retry=1ms: no answer in 50ms\n")
303 require.Contains(t, logs.String(), "<6>send back after=")
304 require.Equal(t, 2, strings.Count(logs.String(), "\n"))
305 require.NotContains(t, logs.String(), telegramTestToken)
308 func TestRepliesGoAsRichMessages(t *testing.T) {
309 reply := "**Done.** The table:\n\n| Name | Size |\n|:-----|-----:|\n| `a.go` | 10 |\n\n- one\n- two\n\n```go\nx := 1\n```"
310 server := newModelFake(t)
311 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
312 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(reply)}, 100, 0, 5))
315 telegram := newTelegramFake(t)
316 bot, _ := telegramStartTestBot(t, telegram)
317 socket := daemonStartTestWithBot(t, server, bot)
318 terminal := terminalStartTestClient(t, socket)
319 terminal.WaitFor(t, "[connected to OneChat]")
321 terminal.Type("make **it** so")
322 telegramWaitForSent(t, telegram, "[terminal] make **it** so", reply)
324 telegram.Add(TelegramUpdate{
326 Message: telegramOwnerMessage(""),
329 telegramWaitForSent(t, telegram, telegramTextOnly)
331 var sends []TelegramFakeCall
333 for _, call := range telegram.Calls() {
334 if call.method == "sendMessage" || call.method == "sendRichMessage" {
335 sends = append(sends, call)
339 require.Equal(t, []TelegramFakeCall{
341 method: "sendMessage",
342 params: map[string]any{
343 "chat_id": float64(telegramTestOwner),
344 "text": "[terminal] make **it** so",
349 method: "sendRichMessage",
350 params: map[string]any{
351 "chat_id": float64(telegramTestOwner),
352 "rich_message": map[string]any{
359 method: "sendMessage",
360 params: map[string]any{
361 "chat_id": float64(telegramTestOwner),
362 "text": telegramTextOnly,
369 func TestRichMessageFailures(t *testing.T) {
370 testCases := []struct {
380 name: "bad request sends the reply once with entities",
381 status: http.StatusBadRequest,
383 methods: []string{"sendRichMessage", "sendMessage"},
384 sent: []string{"a secret word"},
386 logText: "<4>reply sent with simple formatting",
389 name: "unknown method sends the reply once with entities",
390 status: http.StatusNotFound,
392 methods: []string{"sendRichMessage", "sendMessage"},
393 sent: []string{"a secret word"},
395 logText: "<4>reply sent with simple formatting",
398 name: "rate limit tries the rich message again",
399 status: http.StatusTooManyRequests,
400 methods: []string{"sendRichMessage", "sendRichMessage"},
401 sent: []string{"a **secret** word"},
403 logText: "<4>send failed retry=1s: 429",
406 name: "server error tries the rich message again",
407 status: http.StatusInternalServerError,
408 methods: []string{"sendRichMessage", "sendRichMessage"},
409 sent: []string{"a **secret** word"},
411 logText: "<4>send failed retry=1ms: 500",
414 name: "other refusal drops the reply",
415 status: http.StatusForbidden,
417 methods: []string{"sendRichMessage"},
419 logText: "<3>reply lost: 403",
423 for _, tc := range testCases {
424 t.Run(tc.name, func(t *testing.T) {
425 telegram := newTelegramFake(t)
427 telegram.fail = func(call TelegramFakeCall) (int, string) {
428 if call.method != "sendRichMessage" || failed && !tc.always {
434 return tc.status, "refused for bot" + telegramTestToken
437 bot, logs := telegramStartTestBot(t, telegram)
438 bot.post(t.Context(), Event{
440 Text: "a **secret** word",
445 for _, call := range telegram.Calls() {
446 methods = append(methods, call.method)
449 require.Equal(t, tc.methods, methods)
450 require.Equal(t, tc.sent, telegram.Sent())
451 require.Equal(t, tc.lines, strings.Count(logs.String(), "\n"))
452 require.Contains(t, logs.String(), tc.logText)
455 require.Contains(t, logs.String(), "<6>send back after=")
458 require.NotContains(t, logs.String(), telegramTestToken)
459 require.NotContains(t, logs.String(), "secret word")
461 calls := telegram.Calls()
462 if calls[len(calls)-1].method == "sendMessage" {
463 require.NotNil(t, calls[len(calls)-1].params["entities"])
469 func TestLongReplyGoesAsRichMessagesSplitBetweenBlocks(t *testing.T) {
470 paragraphs := strings.Repeat("A paragraph of the reply.\n\n", telegramRichBytes/len("A paragraph of the reply.\n\n"))
471 table := "| a | b |\n|---|---|\n| 1 | 2 |\n"
472 reply := paragraphs + table
474 telegram := newTelegramFake(t)
475 bot, _ := telegramStartTestBot(t, telegram)
476 bot.post(t.Context(), Event{
481 require.Equal(t, []string{paragraphs, table}, telegram.Sent())
484 func TestReplyTagsGoAsText(t *testing.T) {
485 testCases := []struct {
491 name: "rich message escapes the tag outside code",
492 sent: []string{"a <details> & tag and `<b>`"},
495 name: "entity fallback shows the tag as written",
496 status: http.StatusBadRequest,
497 sent: []string{"a <details> & tag and <b>"},
501 for _, tc := range testCases {
502 t.Run(tc.name, func(t *testing.T) {
503 telegram := newTelegramFake(t)
504 telegram.fail = func(call TelegramFakeCall) (int, string) {
505 if call.method != "sendRichMessage" {
509 return tc.status, "Bad Request"
512 bot, _ := telegramStartTestBot(t, telegram)
513 bot.post(t.Context(), Event{
515 Text: "a <details> & tag and `<b>`",
518 require.Equal(t, tc.sent, telegram.Sent())
523 func TestLongReplySplitCountsTheEscapedTags(t *testing.T) {
524 paragraph := "A <b> tag.\n\n"
525 count := telegramRichBytes / len(paragraph)
526 reply := strings.Repeat(paragraph, count)
527 require.LessOrEqual(t, len(reply), telegramRichBytes)
529 telegram := newTelegramFake(t)
530 bot, _ := telegramStartTestBot(t, telegram)
531 bot.post(t.Context(), Event{
536 sent := telegram.Sent()
537 require.Len(t, sent, 2)
538 require.Equal(t, telegramEscapeTags(reply), strings.Join(sent, ""))
540 for _, markdown := range sent {
541 require.LessOrEqual(t, len(markdown), telegramRichBytes)
545 func TestRefusedRichPartGoesWithTheTextAsWritten(t *testing.T) {
546 paragraph := "A <b> & c.\n\n"
547 reply := strings.Repeat(paragraph, telegramRichBytes/len(paragraph))
549 telegram := newTelegramFake(t)
551 telegram.fail = func(call TelegramFakeCall) (int, string) {
552 if call.method != "sendRichMessage" || refused {
558 return http.StatusBadRequest, "Bad Request"
561 bot, _ := telegramStartTestBot(t, telegram)
562 bot.post(t.Context(), Event{
567 var fallback, rich []string
569 for _, call := range telegram.Calls() {
572 case call.method == "sendMessage":
573 fallback = append(fallback, call.params["text"].(string))
574 case call.method == "sendRichMessage":
575 rich = append(rich, call.params["rich_message"].(map[string]any)["markdown"].(string))
579 require.Greater(t, len(fallback), 1)
580 require.Len(t, rich, 1)
581 require.True(t, strings.HasPrefix(reply, strings.Join(fallback, "")), "fallback text is not the reply as written")
583 for _, text := range fallback {
584 require.LessOrEqual(t, telegramUTF16Units(text), telegramMessageUnits)
587 require.Equal(t, reply, strings.Join(fallback, "")+telegramUnescapeTags(rich[0]), "reply is not sent once and whole")
590 func TestRefusedEntitiesGoAgainAsPlainText(t *testing.T) {
591 telegram := newTelegramFake(t)
592 telegram.fail = func(call TelegramFakeCall) (int, string) {
593 if call.params["entities"] == nil {
597 return http.StatusBadRequest, "Bad Request: can't parse entities of bot" + telegramTestToken
600 bot, logs := telegramStartTestBot(t, telegram)
601 text, entities := telegramMarkdownEntities("a **secret** word")
602 bot.deliver(t.Context(), TelegramPart{
607 calls := telegram.Calls()
608 require.Len(t, calls, 2)
609 require.NotNil(t, calls[0].params["entities"])
610 require.Nil(t, calls[1].params["entities"])
611 require.Equal(t, []string{"a secret word"}, telegram.Sent())
612 require.Equal(t, 1, strings.Count(logs.String(), "\n"))
613 require.Contains(t, logs.String(), "<4>reply sent as plain text")
614 require.NotContains(t, logs.String(), telegramTestToken)
615 require.NotContains(t, logs.String(), "secret word")
618 func TestBotLogsALostPlainReplyAsAnError(t *testing.T) {
619 telegram := newTelegramFake(t)
620 telegram.fail = func(call TelegramFakeCall) (int, string) {
621 if call.method == "sendMessage" {
622 return http.StatusForbidden, "Forbidden"
628 bot, logs := telegramStartTestBot(t, telegram)
629 bot.deliver(t.Context(), TelegramPart{text: "secret reply"})
630 require.Equal(t, "<3>reply lost: 403 Forbidden\n", logs.String())
631 require.Empty(t, telegram.Sent())
634 func TestBotLogsNoTokenWhenTheNetworkFails(t *testing.T) {
635 telegram := newTelegramFake(t)
638 bot, logs := telegramStartTestBot(t, telegram)
639 daemonStartTestWithBot(t, newModelFake(t), bot)
641 require.Eventually(t, func() bool {
642 return strings.Contains(logs.String(), "<4>receive failed retry=1ms: no answer: ")
643 }, 10*time.Second, time.Millisecond)
644 require.NotContains(t, logs.String(), telegramTestToken)
647 func TestBotLogsAnOutageOnlyAtItsStartAndEnd(t *testing.T) {
648 server := newModelFake(t)
649 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
650 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
653 telegram := newTelegramFake(t)
655 telegram.fail = func(call TelegramFakeCall) (int, string) {
656 if call.method != "getUpdates" || failures == 0 {
662 return http.StatusBadGateway, "Bad Gateway"
665 bot, logs := telegramStartTestBot(t, telegram)
666 daemonStartTestWithBot(t, server, bot)
668 telegram.Add(TelegramUpdate{
670 Message: telegramOwnerMessage("a secret word"),
673 telegramWaitForSent(t, telegram, "ok")
675 lines := strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n")
676 require.Len(t, lines, 2)
677 require.Equal(t, "<4>receive failed retry=1ms: 502 Bad Gateway", lines[0])
678 require.Regexp(t, `^<6>receive back after=[0-9]+m?s$`, lines[1])
679 require.NotContains(t, logs.String(), "secret word")
680 require.NotContains(t, logs.String(), strconv.FormatInt(telegramTestOwner, 10))
683 func TestSplitMessage(t *testing.T) {
684 testCases := []struct {
691 name: "short text stays whole",
694 parts: []string{"one two"},
697 name: "cut after the last line break",
698 text: "first line\nsecond line",
700 parts: []string{"first line\n", "second line"},
703 name: "cut after the last space without a line break",
704 text: "aaaa bbbb cccc",
706 parts: []string{"aaaa bbbb ", "cccc"},
709 name: "hard cut of one long word",
712 parts: []string{"abcd", "efgh", "ij"},
715 name: "a character outside the BMP counts two units and is not cut",
716 text: "ab\U0001F600cd",
718 parts: []string{"ab", "\U0001F600c", "d"},
722 for _, tc := range testCases {
723 t.Run(tc.name, func(t *testing.T) {
724 require.Equal(t, tc.parts, telegramSplitMessage(tc.text, tc.limit))
728 long := strings.Repeat("word ", 2000)
729 parts := telegramSplitMessage(long, telegramMessageUnits)
730 require.Len(t, parts, 3)
731 require.Equal(t, long, strings.Join(parts, ""))
734 func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) {
735 parts := telegramSplitEntities("aabbbbcc", []TelegramEntity{
748 require.Equal(t, []TelegramPart{
751 entities: []TelegramEntity{
761 entities: []TelegramEntity{
777 func TestSplitRich(t *testing.T) {
778 testCases := []struct {
785 name: "short reply stays whole",
786 markdown: "| a | b |",
788 parts: []string{"| a | b |"},
791 name: "cut after a blank line keeps a table whole",
792 markdown: "Intro.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n",
794 parts: []string{"Intro.\n\n", "| a | b |\n|---|---|\n| 1 | 2 |\n"},
797 name: "blank line in a code block is not a cut",
798 markdown: "Text.\n\n```go\nx\n\ny\n```\n\nEnd.\n",
800 parts: []string{"Text.\n\n", "```go\nx\n\ny\n```\n\n", "End.\n"},
803 name: "paragraph with no blank line is cut after a line",
804 markdown: "aaa\nbbb\nccc\n",
806 parts: []string{"aaa\nbbb\n", "ccc\n"},
809 name: "long code block is closed and opened again",
810 markdown: "```sh\none\ntwo\nthree\n```\n",
812 parts: []string{"```sh\none\n```\n", "```sh\ntwo\n```\n", "```sh\nthree\n```\n"},
815 name: "long line is cut at the limit and not inside a character",
816 markdown: "ab\u00e9cd",
818 parts: []string{"ab", "\u00e9c", "d"},
821 name: "long line is not cut inside an entity",
822 markdown: "abc&def",
824 parts: []string{"abc", "&d", "ef"},
828 for _, tc := range testCases {
829 t.Run(tc.name, func(t *testing.T) {
830 require.Equal(t, tc.parts, telegramSplitRich(tc.markdown, tc.limit))
834 long := strings.Repeat("A paragraph of the reply.\n\n", 2000)
835 parts := telegramSplitRich(long, telegramRichBytes)
836 require.Len(t, parts, 2)
837 require.Len(t, parts[0], telegramRichBytes-telegramRichBytes%len("A paragraph of the reply.\n\n"))
838 require.Equal(t, long, strings.Join(parts, ""))
841 func TestLoadBotConfig(t *testing.T) {
842 testCases := []struct {
849 name: "owner file is read",
850 content: `{"token": "` + telegramTestToken + `", "owner": 4242}`,
854 name: "file that others can read is refused",
855 content: `{"token": "` + telegramTestToken + `", "owner": 4242}`,
857 errorText: "mode must be 0600",
860 name: "token with a path character is refused",
861 content: `{"token": "123:abc/../x", "owner": 4242}`,
863 errorText: "form of a bot token",
866 name: "missing owner is refused",
867 content: `{"token": "` + telegramTestToken + `"}`,
869 errorText: "numeric Telegram user ID",
872 name: "unknown field is refused",
873 content: `{"token": "` + telegramTestToken + `", "owner": 4242, "chat": 1}`,
875 errorText: "not valid JSON",
879 for _, tc := range testCases {
880 t.Run(tc.name, func(t *testing.T) {
882 path := filepath.Join(dir, telegramConfigFileName)
883 require.NoError(t, os.WriteFile(path, []byte(tc.content), tc.mode))
884 require.NoError(t, os.Chmod(path, tc.mode))
886 config, found, err := loadTelegramConfig(dir)
887 if tc.errorText != "" {
888 require.ErrorContains(t, err, tc.errorText)
889 require.NotContains(t, err.Error(), telegramTestToken)
894 require.NoError(t, err)
895 require.True(t, found)
896 require.Equal(t, TelegramConfig{
897 Token: telegramTestToken,
898 Owner: telegramTestOwner,
903 _, found, err := loadTelegramConfig(t.TempDir())
904 require.NoError(t, err)
905 require.False(t, found)
908 // telegramStartTestBot returns a bot of telegramTestOwner on server, with its
909 // offset file in a temporary directory and short waits, and the buffer of its
911 func telegramStartTestBot(t *testing.T, server *TelegramFake) (*TelegramBot, *SyncBuffer) {
914 logs := &SyncBuffer{}
915 bot := newTelegramBot(newTelegramAPI(server.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), daemonTestLogger(logs))
916 bot.minWait = time.Millisecond
917 bot.maxWait = 10 * time.Millisecond
922 // telegramWaitForSent waits until the bot sent the texts in order, and returns
924 func telegramWaitForSent(t *testing.T, server *TelegramFake, texts ...string) []string {
927 require.Eventually(t, func() bool {
928 sent := server.Sent()
929 return len(sent) >= len(texts) && slices.Equal(sent[len(sent)-len(texts):], texts)
930 }, 10*time.Second, time.Millisecond, "sent so far: %q", server.Sent())