15 "github.com/stretchr/testify/require"
18 func TestOwnerTalksToTheAgentThroughTelegram(t *testing.T) {
19 server := newModelFake(t)
20 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
21 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Hello from the agent.")}, 100, 0, 5))
24 telegram := newTelegramFake(t)
25 bot, _ := telegramStartTestBot(t, telegram)
26 socket := daemonStartTestWithBot(t, server, bot)
27 terminal := terminalStartTestClient(t, socket)
28 terminal.WaitFor(t, "[connected to OneChat]")
30 telegram.Add(TelegramUpdate{
32 Message: telegramOwnerMessage("hi from the phone"),
35 terminal.WaitFor(t, "[telegram] hi from the phone\n[working]\nHello from the agent.\n[done]")
36 telegramWaitForSent(t, telegram, "Hello from the agent.")
37 require.True(t, slices.ContainsFunc(telegram.Calls(), func(call TelegramFakeCall) bool {
38 return call.method == "sendChatAction" && call.params["action"] == "typing"
41 terminal.Type("hi from the desk")
42 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."))
45 func TestBotIgnoresEveryoneButTheOwner(t *testing.T) {
46 telegram := newTelegramFake(t)
47 bot, logs := telegramStartTestBot(t, telegram)
48 socket := daemonStartTestWithBot(t, newModelFake(t), bot)
49 terminal := terminalStartTestClient(t, socket)
50 terminal.WaitFor(t, "[connected to OneChat]")
52 group := telegramOwnerMessage("secret in a group")
53 group.Chat = TelegramChat{
58 stranger := telegramOwnerMessage("secret of a stranger")
62 unsigned := telegramOwnerMessage("secret with no sender")
65 for id, message := range []*TelegramMessage{group, stranger, unsigned, nil} {
66 telegram.Add(TelegramUpdate{
67 UpdateID: int64(10 + id),
72 require.Eventually(t, func() bool {
73 offset, err := telegramLoadOffset(bot.offsetPath)
74 return err == nil && offset == 14
75 }, 10*time.Second, time.Millisecond)
77 require.Empty(t, telegram.Sent())
78 require.NotContains(t, terminal.Output(), "secret")
79 require.NotContains(t, logs.String(), "secret")
80 require.Contains(t, logs.String(), "<6>message ignored")
83 func TestBotAnswersThatOnlyTextIsSupported(t *testing.T) {
84 telegram := newTelegramFake(t)
85 bot, _ := telegramStartTestBot(t, telegram)
86 daemonStartTestWithBot(t, newModelFake(t), bot)
88 telegram.Add(TelegramUpdate{
90 Message: telegramOwnerMessage(""),
93 telegramWaitForSent(t, telegram, telegramTextOnly)
96 func TestStopFromTelegramInterruptsTheTurn(t *testing.T) {
97 server := newModelFake(t)
98 arrived := make(chan struct{})
99 release := make(chan struct{})
104 server.reply = func(_ http.ResponseWriter, _ ModelFakeCall) {
109 telegram := newTelegramFake(t)
110 bot, _ := telegramStartTestBot(t, telegram)
111 daemonStartTestWithBot(t, server, bot)
113 telegram.Add(TelegramUpdate{
115 Message: telegramOwnerMessage("a long task"),
119 telegram.Add(TelegramUpdate{
121 Message: telegramOwnerMessage(stopCommand),
124 telegramWaitForSent(t, telegram, "Error: interrupted")
126 telegram.Add(TelegramUpdate{
128 Message: telegramOwnerMessage(stopCommand),
131 telegramWaitForSent(t, telegram, "Error: interrupted", "No turn runs.")
133 telegram.Add(TelegramUpdate{
135 Message: telegramOwnerMessage(stopCommand + " BlueFox"),
138 telegramWaitForSent(t, telegram, "No turn runs.", "No subagent BlueFox runs.")
141 func TestBotShowsOnlyTheStartAndTheEndOfASubagent(t *testing.T) {
142 telegram := newTelegramFake(t)
143 server := newModelFake(t)
144 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
145 last := subagentTestLastItem(call)
147 case subagentTestCall(call):
148 // The report comes after the reply of the turn of the
150 require.Eventually(t, func() bool {
151 return slices.Contains(telegram.Sent(), "Started.")
152 }, 10*time.Second, time.Millisecond)
153 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("There are 3 files.")}, 100, 0, 5))
154 case last["type"] == "function_call_output":
155 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Started.")}, 100, 0, 5))
156 case strings.HasPrefix(subagentTestText(last), "["):
157 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("It counted 3 files.")}, 100, 0, 5))
159 modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "spawn", `{"tasks":["Count the files."],"effort":null}`)}, 100, 0, 5))
163 bot, _ := telegramStartTestBot(t, telegram)
164 daemonStartTestWithBot(t, server, bot)
165 telegram.Add(TelegramUpdate{
167 Message: telegramOwnerMessage("Count the files in a subagent."),
170 require.Eventually(t, func() bool {
171 return slices.Contains(telegram.Sent(), "It counted 3 files.")
172 }, 10*time.Second, time.Millisecond)
174 sent := telegram.Sent()
175 name := strings.TrimSuffix(strings.TrimPrefix(sent[0], "["), "] started")
176 require.True(t, subagentNameValid(name), sent[0])
177 require.Equal(t, []string{
178 "[" + name + "] started",
180 "[" + name + "] finished",
181 "It counted 3 files.",
185 func TestBotSkipsTheWorkOfWhatsAppAndTheSchedules(t *testing.T) {
187 ready: make(chan struct{}, 1),
192 Source: whatsappSource,
193 Text: "[WhatsApp] Ana Souza",
198 Source: scheduleSource,
199 Text: "[Schedule 1] Sunday 2026-11-08 20:05 -03:00, once",
202 require.Empty(t, bot.queue)
205 func TestBotStartsAtTheStoredOffset(t *testing.T) {
206 server := newModelFake(t)
207 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
208 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
211 telegram := newTelegramFake(t)
212 bot, _ := telegramStartTestBot(t, telegram)
213 require.NoError(t, telegramSaveOffset(bot.offsetPath, 10))
215 telegram.Add(TelegramUpdate{
217 Message: telegramOwnerMessage("handled before the restart"),
220 telegram.Add(TelegramUpdate{
222 Message: telegramOwnerMessage("new after the restart"),
225 socket := daemonStartTestWithBot(t, server, bot)
226 terminal := terminalStartTestClient(t, socket)
227 terminal.WaitFor(t, "[connected to OneChat]")
228 telegramWaitForSent(t, telegram, "ok")
230 require.InDelta(t, float64(10), telegram.Calls()[0].params["offset"], 0)
231 require.NotContains(t, terminal.Output(), "handled before the restart")
233 require.Eventually(t, func() bool {
234 offset, err := telegramLoadOffset(bot.offsetPath)
235 return err == nil && offset == 11
236 }, 10*time.Second, time.Millisecond)
239 func TestBotTriesAgainAfterFailures(t *testing.T) {
240 server := newModelFake(t)
241 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
242 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
245 telegram := newTelegramFake(t)
246 failures := map[string]int{
248 "sendRichMessage": 1,
251 telegram.fail = func(call TelegramFakeCall) (int, string) {
252 if failures[call.method] == 0 {
256 failures[call.method]--
257 if call.method == "sendRichMessage" {
258 return http.StatusTooManyRequests, "Too Many Requests: retry after 1"
261 return http.StatusBadGateway, "Bad Gateway"
264 bot, logs := telegramStartTestBot(t, telegram)
265 daemonStartTestWithBot(t, server, bot)
267 telegram.Add(TelegramUpdate{
269 Message: telegramOwnerMessage("hello"),
272 telegramWaitForSent(t, telegram, "ok")
273 require.Contains(t, logs.String(), "<4>receive failed retry=1s: 502")
274 require.Contains(t, logs.String(), "<6>receive back after=")
275 require.Contains(t, logs.String(), "<4>send failed retry=1s: 429")
277 // The bot logs the end of the outage after the send returns, so the
278 // line can come after the fake server has the message.
279 require.Eventually(t, func() bool {
280 return strings.Contains(logs.String(), "<6>send back after=")
281 }, 10*time.Second, time.Millisecond)
283 require.NotContains(t, logs.String(), telegramTestToken)
286 func TestDeliverTriesAgainAfterATimeout(t *testing.T) {
287 synctest.Test(t, func(t *testing.T) {
288 telegram := newTelegramLocalFake(t)
290 telegram.stall = func(call TelegramFakeCall) bool {
291 if call.method != "sendMessage" || stalls == 0 {
300 bot, logs := telegramStartTestBot(t, telegram)
301 bot.api.http.Transport = telegram
302 bot.deliver(t.Context(), TelegramPart{
306 require.Equal(t, []string{"hello"}, telegram.Sent())
307 require.Len(t, telegram.Calls(), 2)
308 require.Contains(t, logs.String(), "<4>send failed retry=1s: no answer in 30s\n")
309 require.Contains(t, logs.String(), "<6>send back after=1s\n")
310 require.Equal(t, 2, strings.Count(logs.String(), "\n"))
311 require.NotContains(t, logs.String(), telegramTestToken)
315 func TestRepliesGoAsRichMessages(t *testing.T) {
316 reply := "**Done.** The table:\n\n| Name | Size |\n|:-----|-----:|\n| `a.go` | 10 |\n\n- one\n- two\n\n```go\nx := 1\n```"
317 server := newModelFake(t)
318 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
319 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(reply)}, 100, 0, 5))
322 telegram := newTelegramFake(t)
323 bot, _ := telegramStartTestBot(t, telegram)
324 socket := daemonStartTestWithBot(t, server, bot)
325 terminal := terminalStartTestClient(t, socket)
326 terminal.WaitFor(t, "[connected to OneChat]")
328 terminal.Type("make **it** so")
329 telegramWaitForSent(t, telegram, "[terminal] make **it** so", reply)
331 telegram.Add(TelegramUpdate{
333 Message: telegramOwnerMessage(""),
336 telegramWaitForSent(t, telegram, telegramTextOnly)
338 var sends []TelegramFakeCall
340 for _, call := range telegram.Calls() {
341 if call.method == "sendMessage" || call.method == "sendRichMessage" {
342 sends = append(sends, call)
346 require.Equal(t, []TelegramFakeCall{
348 method: "sendMessage",
349 params: map[string]any{
350 "chat_id": float64(telegramTestOwner),
351 "text": "[terminal] make **it** so",
356 method: "sendRichMessage",
357 params: map[string]any{
358 "chat_id": float64(telegramTestOwner),
359 "rich_message": map[string]any{
366 method: "sendMessage",
367 params: map[string]any{
368 "chat_id": float64(telegramTestOwner),
369 "text": telegramTextOnly,
376 func TestRichMessageFailures(t *testing.T) {
377 testCases := []struct {
387 name: "bad request sends the reply once with entities",
388 status: http.StatusBadRequest,
390 methods: []string{"sendRichMessage", "sendMessage"},
391 sent: []string{"a secret word"},
393 logText: "<4>reply sent with simple formatting",
396 name: "unknown method sends the reply once with entities",
397 status: http.StatusNotFound,
399 methods: []string{"sendRichMessage", "sendMessage"},
400 sent: []string{"a secret word"},
402 logText: "<4>reply sent with simple formatting",
405 name: "rate limit tries the rich message again",
406 status: http.StatusTooManyRequests,
407 methods: []string{"sendRichMessage", "sendRichMessage"},
408 sent: []string{"a **secret** word"},
410 logText: "<4>send failed retry=1s: 429",
413 name: "server error tries the rich message again",
414 status: http.StatusInternalServerError,
415 methods: []string{"sendRichMessage", "sendRichMessage"},
416 sent: []string{"a **secret** word"},
418 logText: "<4>send failed retry=1s: 500",
421 name: "other refusal drops the reply",
422 status: http.StatusForbidden,
424 methods: []string{"sendRichMessage"},
426 logText: "<3>reply lost: 403",
430 for _, tc := range testCases {
431 t.Run(tc.name, func(t *testing.T) {
432 telegram := newTelegramFake(t)
434 telegram.fail = func(call TelegramFakeCall) (int, string) {
435 if call.method != "sendRichMessage" || failed && !tc.always {
441 return tc.status, "refused for bot" + telegramTestToken
444 bot, logs := telegramStartTestBot(t, telegram)
445 bot.post(t.Context(), Event{
447 Text: "a **secret** word",
452 for _, call := range telegram.Calls() {
453 methods = append(methods, call.method)
456 require.Equal(t, tc.methods, methods)
457 require.Equal(t, tc.sent, telegram.Sent())
458 require.Equal(t, tc.lines, strings.Count(logs.String(), "\n"))
459 require.Contains(t, logs.String(), tc.logText)
462 require.Contains(t, logs.String(), "<6>send back after=")
465 require.NotContains(t, logs.String(), telegramTestToken)
466 require.NotContains(t, logs.String(), "secret word")
468 calls := telegram.Calls()
469 if calls[len(calls)-1].method == "sendMessage" {
470 require.NotNil(t, calls[len(calls)-1].params["entities"])
476 func TestLongReplyGoesAsRichMessagesSplitBetweenBlocks(t *testing.T) {
477 paragraphs := strings.Repeat("A paragraph of the reply.\n\n", telegramRichBytes/len("A paragraph of the reply.\n\n"))
478 table := "| a | b |\n|---|---|\n| 1 | 2 |\n"
479 reply := paragraphs + table
481 telegram := newTelegramFake(t)
482 bot, _ := telegramStartTestBot(t, telegram)
483 bot.post(t.Context(), Event{
488 require.Equal(t, []string{paragraphs, table}, telegram.Sent())
491 func TestReplyTagsGoAsText(t *testing.T) {
492 testCases := []struct {
498 name: "rich message escapes the tag outside code",
499 sent: []string{"a <details> & tag and `<b>`"},
502 name: "entity fallback shows the tag as written",
503 status: http.StatusBadRequest,
504 sent: []string{"a <details> & tag and <b>"},
508 for _, tc := range testCases {
509 t.Run(tc.name, func(t *testing.T) {
510 telegram := newTelegramFake(t)
511 telegram.fail = func(call TelegramFakeCall) (int, string) {
512 if call.method != "sendRichMessage" {
516 return tc.status, "Bad Request"
519 bot, _ := telegramStartTestBot(t, telegram)
520 bot.post(t.Context(), Event{
522 Text: "a <details> & tag and `<b>`",
525 require.Equal(t, tc.sent, telegram.Sent())
530 func TestLongReplySplitCountsTheEscapedTags(t *testing.T) {
531 paragraph := "A <b> tag.\n\n"
532 count := telegramRichBytes / len(paragraph)
533 reply := strings.Repeat(paragraph, count)
534 require.LessOrEqual(t, len(reply), telegramRichBytes)
536 telegram := newTelegramFake(t)
537 bot, _ := telegramStartTestBot(t, telegram)
538 bot.post(t.Context(), Event{
543 sent := telegram.Sent()
544 require.Len(t, sent, 2)
545 require.Equal(t, telegramEscapeTags(reply), strings.Join(sent, ""))
547 for _, markdown := range sent {
548 require.LessOrEqual(t, len(markdown), telegramRichBytes)
552 func TestRefusedRichPartGoesWithTheTextAsWritten(t *testing.T) {
553 paragraph := "A <b> & c.\n\n"
554 reply := strings.Repeat(paragraph, telegramRichBytes/len(paragraph))
556 telegram := newTelegramFake(t)
558 telegram.fail = func(call TelegramFakeCall) (int, string) {
559 if call.method != "sendRichMessage" || refused {
565 return http.StatusBadRequest, "Bad Request"
568 bot, _ := telegramStartTestBot(t, telegram)
569 bot.post(t.Context(), Event{
574 var fallback, rich []string
576 for _, call := range telegram.Calls() {
579 case call.method == "sendMessage":
580 fallback = append(fallback, call.params["text"].(string))
581 case call.method == "sendRichMessage":
582 rich = append(rich, call.params["rich_message"].(map[string]any)["markdown"].(string))
586 require.Greater(t, len(fallback), 1)
587 require.Len(t, rich, 1)
588 require.True(t, strings.HasPrefix(reply, strings.Join(fallback, "")), "fallback text is not the reply as written")
590 for _, text := range fallback {
591 require.LessOrEqual(t, telegramUTF16Units(text), telegramMessageUnits)
594 require.Equal(t, reply, strings.Join(fallback, "")+telegramUnescapeTags(rich[0]), "reply is not sent once and whole")
597 func TestRefusedEntitiesGoAgainAsPlainText(t *testing.T) {
598 telegram := newTelegramFake(t)
599 telegram.fail = func(call TelegramFakeCall) (int, string) {
600 if call.params["entities"] == nil {
604 return http.StatusBadRequest, "Bad Request: can't parse entities of bot" + telegramTestToken
607 bot, logs := telegramStartTestBot(t, telegram)
608 text, entities := telegramMarkdownEntities("a **secret** word")
609 bot.deliver(t.Context(), TelegramPart{
614 calls := telegram.Calls()
615 require.Len(t, calls, 2)
616 require.NotNil(t, calls[0].params["entities"])
617 require.Nil(t, calls[1].params["entities"])
618 require.Equal(t, []string{"a secret word"}, telegram.Sent())
619 require.Equal(t, 1, strings.Count(logs.String(), "\n"))
620 require.Contains(t, logs.String(), "<4>reply sent as plain text")
621 require.NotContains(t, logs.String(), telegramTestToken)
622 require.NotContains(t, logs.String(), "secret word")
625 func TestBotLogsALostPlainReplyAsAnError(t *testing.T) {
626 telegram := newTelegramFake(t)
627 telegram.fail = func(call TelegramFakeCall) (int, string) {
628 if call.method == "sendMessage" {
629 return http.StatusForbidden, "Forbidden"
635 bot, logs := telegramStartTestBot(t, telegram)
636 bot.deliver(t.Context(), TelegramPart{text: "secret reply"})
637 require.Equal(t, "<3>reply lost: 403 Forbidden\n", logs.String())
638 require.Empty(t, telegram.Sent())
641 func TestBotLogsNoTokenWhenTheNetworkFails(t *testing.T) {
642 telegram := newTelegramFake(t)
645 bot, logs := telegramStartTestBot(t, telegram)
646 daemonStartTestWithBot(t, newModelFake(t), bot)
648 require.Eventually(t, func() bool {
649 return strings.Contains(logs.String(), "<4>receive failed retry=1s: no answer: ")
650 }, 10*time.Second, time.Millisecond)
652 require.NotContains(t, logs.String(), telegramTestToken)
655 func TestBotLogsAnOutageOnlyAtItsStartAndEnd(t *testing.T) {
656 synctest.Test(t, func(t *testing.T) {
657 telegram := newTelegramLocalFake(t)
659 telegram.fail = func(call TelegramFakeCall) (int, string) {
660 if call.method != "getUpdates" || failures == 0 {
666 return http.StatusBadGateway, "Bad Gateway"
669 bot, logs := telegramStartTestBot(t, telegram)
670 bot.api.http.Transport = telegram
671 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
672 loop, _ := newTestIdleTurnLoop(t, whatsapp)
674 telegram.Add(TelegramUpdate{
676 Message: telegramOwnerMessage("a secret word"),
679 ctx, cancel := context.WithCancel(t.Context())
682 done := make(chan error, 1)
684 done <- bot.poll(ctx, loop, whatsapp)
687 time.Sleep(31 * time.Second)
689 require.Equal(t, 1, loop.memory.Len())
691 message, err := loop.memory.Message(0)
692 require.NoError(t, err)
693 require.Equal(t, "a secret word", message.Text)
695 lines := strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n")
696 require.Equal(t, []string{
697 "<4>receive failed retry=1s: 502 Bad Gateway",
698 "<6>receive back after=31s",
701 require.NotContains(t, logs.String(), "secret word")
702 require.NotContains(t, logs.String(), strconv.FormatInt(telegramTestOwner, 10))
704 require.NoError(t, <-done)
708 func TestSplitMessage(t *testing.T) {
709 testCases := []struct {
716 name: "short text stays whole",
719 parts: []string{"one two"},
722 name: "cut after the last line break",
723 text: "first line\nsecond line",
725 parts: []string{"first line\n", "second line"},
728 name: "cut after the last space without a line break",
729 text: "aaaa bbbb cccc",
731 parts: []string{"aaaa bbbb ", "cccc"},
734 name: "hard cut of one long word",
737 parts: []string{"abcd", "efgh", "ij"},
740 name: "a character outside the BMP counts two units and is not cut",
741 text: "ab\U0001F600cd",
743 parts: []string{"ab", "\U0001F600c", "d"},
747 for _, tc := range testCases {
748 t.Run(tc.name, func(t *testing.T) {
749 require.Equal(t, tc.parts, telegramSplitMessage(tc.text, tc.limit))
753 long := strings.Repeat("word ", 2000)
754 parts := telegramSplitMessage(long, telegramMessageUnits)
755 require.Len(t, parts, 3)
756 require.Equal(t, long, strings.Join(parts, ""))
759 func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) {
760 parts := telegramSplitEntities("aabbbbcc", []TelegramEntity{
773 require.Equal(t, []TelegramPart{
776 entities: []TelegramEntity{
786 entities: []TelegramEntity{
802 func TestSplitRich(t *testing.T) {
803 testCases := []struct {
810 name: "short reply stays whole",
811 markdown: "| a | b |",
813 parts: []string{"| a | b |"},
816 name: "cut after a blank line keeps a table whole",
817 markdown: "Intro.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n",
819 parts: []string{"Intro.\n\n", "| a | b |\n|---|---|\n| 1 | 2 |\n"},
822 name: "blank line in a code block is not a cut",
823 markdown: "Text.\n\n```go\nx\n\ny\n```\n\nEnd.\n",
825 parts: []string{"Text.\n\n", "```go\nx\n\ny\n```\n\n", "End.\n"},
828 name: "paragraph with no blank line is cut after a line",
829 markdown: "aaa\nbbb\nccc\n",
831 parts: []string{"aaa\nbbb\n", "ccc\n"},
834 name: "long code block is closed and opened again",
835 markdown: "```sh\none\ntwo\nthree\n```\n",
837 parts: []string{"```sh\none\n```\n", "```sh\ntwo\n```\n", "```sh\nthree\n```\n"},
840 name: "long line is cut at the limit and not inside a character",
841 markdown: "ab\u00e9cd",
843 parts: []string{"ab", "\u00e9c", "d"},
846 name: "long line is not cut inside an entity",
847 markdown: "abc&def",
849 parts: []string{"abc", "&d", "ef"},
853 for _, tc := range testCases {
854 t.Run(tc.name, func(t *testing.T) {
855 require.Equal(t, tc.parts, telegramSplitRich(tc.markdown, tc.limit))
859 long := strings.Repeat("A paragraph of the reply.\n\n", 2000)
860 parts := telegramSplitRich(long, telegramRichBytes)
861 require.Len(t, parts, 2)
862 require.Len(t, parts[0], telegramRichBytes-telegramRichBytes%len("A paragraph of the reply.\n\n"))
863 require.Equal(t, long, strings.Join(parts, ""))
866 func TestLoadBotConfig(t *testing.T) {
867 testCases := []struct {
874 name: "owner file is read",
875 content: `{"token": "` + telegramTestToken + `", "owner": 4242}`,
879 name: "file that others can read is refused",
880 content: `{"token": "` + telegramTestToken + `", "owner": 4242}`,
882 errorText: "mode must be 0600",
885 name: "token with a path character is refused",
886 content: `{"token": "123:abc/../x", "owner": 4242}`,
888 errorText: "form of a bot token",
891 name: "missing owner is refused",
892 content: `{"token": "` + telegramTestToken + `"}`,
894 errorText: "numeric Telegram user ID",
897 name: "unknown field is refused",
898 content: `{"token": "` + telegramTestToken + `", "owner": 4242, "chat": 1}`,
900 errorText: "not valid JSON",
904 for _, tc := range testCases {
905 t.Run(tc.name, func(t *testing.T) {
907 path := filepath.Join(dir, telegramConfigFileName)
908 require.NoError(t, os.WriteFile(path, []byte(tc.content), tc.mode))
909 require.NoError(t, os.Chmod(path, tc.mode))
911 config, found, err := loadTelegramConfig(dir)
912 if tc.errorText != "" {
913 require.ErrorContains(t, err, tc.errorText)
914 require.NotContains(t, err.Error(), telegramTestToken)
919 require.NoError(t, err)
920 require.True(t, found)
921 require.Equal(t, TelegramConfig{
922 Token: telegramTestToken,
923 Owner: telegramTestOwner,
928 _, found, err := loadTelegramConfig(t.TempDir())
929 require.NoError(t, err)
930 require.False(t, found)
933 // telegramStartTestBot returns a bot of telegramTestOwner on server, with its
934 // offset file in a temporary directory and the buffer of its log.
935 func telegramStartTestBot(t *testing.T, server *TelegramFake) (*TelegramBot, *SyncBuffer) {
938 logs := &SyncBuffer{}
939 bot := newTelegramBot(newTelegramAPI(server.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), daemonTestLogger(logs))
944 // telegramWaitForSent waits until the bot sent the texts in order, and returns
946 func telegramWaitForSent(t *testing.T, server *TelegramFake, texts ...string) []string {
949 require.Eventually(t, func() bool {
950 sent := server.Sent()
951 return len(sent) >= len(texts) && slices.Equal(sent[len(sent)-len(texts):], texts)
952 }, 10*time.Second, time.Millisecond, "sent so far: %q", server.Sent())