package main
import (
- "log"
+ "context"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
+ "testing/synctest"
"time"
"github.com/stretchr/testify/require"
require.Empty(t, telegram.Sent())
require.NotContains(t, terminal.Output(), "secret")
require.NotContains(t, logs.String(), "secret")
- require.Contains(t, logs.String(), "message ignored")
+ require.Contains(t, logs.String(), "<6>message ignored")
}
func TestBotAnswersThatOnlyTextIsSupported(t *testing.T) {
last := subagentTestLastItem(call)
switch {
case subagentTestCall(call):
- // The report comes after the reply of the turn of the spawn.
+ // The report comes after the reply of the turn of the
+ // spawn.
require.Eventually(t, func() bool {
return slices.Contains(telegram.Sent(), "Started.")
}, 10*time.Second, time.Millisecond)
})
telegramWaitForSent(t, telegram, "ok")
- require.Contains(t, logs.String(), "502")
- require.Contains(t, logs.String(), "429")
+ require.Contains(t, logs.String(), "<4>receive failed retry=1s: 502")
+ require.Contains(t, logs.String(), "<6>receive back after=")
+ require.Contains(t, logs.String(), "<4>send failed retry=1s: 429")
+
+ // The bot logs the end of the outage after the send returns, so the
+ // line can come after the fake server has the message.
+ require.Eventually(t, func() bool {
+ return strings.Contains(logs.String(), "<6>send back after=")
+ }, 10*time.Second, time.Millisecond)
+
require.NotContains(t, logs.String(), telegramTestToken)
}
func TestDeliverTriesAgainAfterATimeout(t *testing.T) {
- telegram := newTelegramFake(t)
- stalls := 1
- telegram.stall = func(call TelegramFakeCall) bool {
- if call.method != "sendMessage" || stalls == 0 {
- return false
- }
+ synctest.Test(t, func(t *testing.T) {
+ telegram := newTelegramLocalFake(t)
+ stalls := 1
+ telegram.stall = func(call TelegramFakeCall) bool {
+ if call.method != "sendMessage" || stalls == 0 {
+ return false
+ }
- stalls--
+ stalls--
- return true
- }
+ return true
+ }
- bot, logs := telegramStartTestBot(t, telegram)
- bot.api.sendTimeout = 50 * time.Millisecond
- bot.deliver(t.Context(), TelegramPart{
- text: "hello",
- })
+ bot, logs := telegramStartTestBot(t, telegram)
+ bot.api.http.Transport = telegram
+ bot.deliver(t.Context(), TelegramPart{
+ text: "hello",
+ })
- require.Equal(t, []string{"hello"}, telegram.Sent())
- require.Len(t, telegram.Calls(), 2)
- require.Contains(t, logs.String(), "send failed: no answer in 50ms (retry in 1ms)\n")
- require.Contains(t, logs.String(), "send back after ")
- require.Equal(t, 2, strings.Count(logs.String(), "\n"))
- require.NotContains(t, logs.String(), telegramTestToken)
+ require.Equal(t, []string{"hello"}, telegram.Sent())
+ require.Len(t, telegram.Calls(), 2)
+ require.Contains(t, logs.String(), "<4>send failed retry=1s: no answer in 30s\n")
+ require.Contains(t, logs.String(), "<6>send back after=1s\n")
+ require.Equal(t, 2, strings.Count(logs.String(), "\n"))
+ require.NotContains(t, logs.String(), telegramTestToken)
+ })
}
func TestRepliesGoAsRichMessages(t *testing.T) {
telegramWaitForSent(t, telegram, telegramTextOnly)
var sends []TelegramFakeCall
+
for _, call := range telegram.Calls() {
if call.method == "sendMessage" || call.method == "sendRichMessage" {
sends = append(sends, call)
methods: []string{"sendRichMessage", "sendMessage"},
sent: []string{"a secret word"},
lines: 1,
- logText: "reply sent with simple formatting",
+ logText: "<4>reply sent with simple formatting",
},
{
name: "unknown method sends the reply once with entities",
methods: []string{"sendRichMessage", "sendMessage"},
sent: []string{"a secret word"},
lines: 1,
- logText: "reply sent with simple formatting",
+ logText: "<4>reply sent with simple formatting",
},
{
name: "rate limit tries the rich message again",
methods: []string{"sendRichMessage", "sendRichMessage"},
sent: []string{"a **secret** word"},
lines: 2,
- logText: "send failed: 429",
+ logText: "<4>send failed retry=1s: 429",
},
{
name: "server error tries the rich message again",
methods: []string{"sendRichMessage", "sendRichMessage"},
sent: []string{"a **secret** word"},
lines: 2,
- logText: "send failed: 500",
+ logText: "<4>send failed retry=1s: 500",
},
{
name: "other refusal drops the reply",
always: true,
methods: []string{"sendRichMessage"},
lines: 1,
- logText: "reply lost: 403",
+ logText: "<3>reply lost: 403",
},
}
})
var methods []string
+
for _, call := range telegram.Calls() {
methods = append(methods, call.method)
}
require.Equal(t, tc.sent, telegram.Sent())
require.Equal(t, tc.lines, strings.Count(logs.String(), "\n"))
require.Contains(t, logs.String(), tc.logText)
+
+ if tc.lines == 2 {
+ require.Contains(t, logs.String(), "<6>send back after=")
+ }
+
require.NotContains(t, logs.String(), telegramTestToken)
require.NotContains(t, logs.String(), "secret word")
})
var fallback, rich []string
+
for _, call := range telegram.Calls() {
switch {
case !call.ok:
require.Nil(t, calls[1].params["entities"])
require.Equal(t, []string{"a secret word"}, telegram.Sent())
require.Equal(t, 1, strings.Count(logs.String(), "\n"))
- require.Contains(t, logs.String(), "reply sent as plain text")
+ require.Contains(t, logs.String(), "<4>reply sent as plain text")
require.NotContains(t, logs.String(), telegramTestToken)
require.NotContains(t, logs.String(), "secret word")
}
+func TestBotLogsALostPlainReplyAsAnError(t *testing.T) {
+ telegram := newTelegramFake(t)
+ telegram.fail = func(call TelegramFakeCall) (int, string) {
+ if call.method == "sendMessage" {
+ return http.StatusForbidden, "Forbidden"
+ }
+
+ return 0, ""
+ }
+
+ bot, logs := telegramStartTestBot(t, telegram)
+ bot.deliver(t.Context(), TelegramPart{text: "secret reply"})
+ require.Equal(t, "<3>reply lost: 403 Forbidden\n", logs.String())
+ require.Empty(t, telegram.Sent())
+}
+
func TestBotLogsNoTokenWhenTheNetworkFails(t *testing.T) {
telegram := newTelegramFake(t)
telegram.Close()
daemonStartTestWithBot(t, newModelFake(t), bot)
require.Eventually(t, func() bool {
- return strings.Contains(logs.String(), "receive failed: no answer: ")
+ return strings.Contains(logs.String(), "<4>receive failed retry=1s: no answer: ")
}, 10*time.Second, time.Millisecond)
require.NotContains(t, logs.String(), telegramTestToken)
}
func TestBotLogsAnOutageOnlyAtItsStartAndEnd(t *testing.T) {
- server := newModelFake(t)
- server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
- modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
- }
+ synctest.Test(t, func(t *testing.T) {
+ telegram := newTelegramLocalFake(t)
+ failures := 5
+ telegram.fail = func(call TelegramFakeCall) (int, string) {
+ if call.method != "getUpdates" || failures == 0 {
+ return 0, ""
+ }
- telegram := newTelegramFake(t)
- failures := 5
- telegram.fail = func(call TelegramFakeCall) (int, string) {
- if call.method != "getUpdates" || failures == 0 {
- return 0, ""
- }
+ failures--
- failures--
+ return http.StatusBadGateway, "Bad Gateway"
+ }
- return http.StatusBadGateway, "Bad Gateway"
- }
+ bot, logs := telegramStartTestBot(t, telegram)
+ bot.api.http.Transport = telegram
+ whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
+ loop, _ := newTestIdleTurnLoop(t, whatsapp)
- bot, logs := telegramStartTestBot(t, telegram)
- daemonStartTestWithBot(t, server, bot)
+ telegram.Add(TelegramUpdate{
+ UpdateID: 1,
+ Message: telegramOwnerMessage("a secret word"),
+ })
- telegram.Add(TelegramUpdate{
- UpdateID: 1,
- Message: telegramOwnerMessage("a secret word"),
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ done := make(chan error, 1)
+ go func() {
+ done <- bot.poll(ctx, loop, whatsapp)
+ }()
+
+ time.Sleep(31 * time.Second)
+ synctest.Wait()
+ require.Equal(t, 1, loop.memory.Len())
+
+ message, err := loop.memory.Message(0)
+ require.NoError(t, err)
+ require.Equal(t, "a secret word", message.Text)
+
+ lines := strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n")
+ require.Equal(t, []string{
+ "<4>receive failed retry=1s: 502 Bad Gateway",
+ "<6>receive back after=31s",
+ }, lines)
+ require.NotContains(t, logs.String(), "secret word")
+ require.NotContains(t, logs.String(), strconv.FormatInt(telegramTestOwner, 10))
+ cancel()
+ require.NoError(t, <-done)
})
-
- telegramWaitForSent(t, telegram, "ok")
- lines := strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n")
- require.Len(t, lines, 2)
- require.Equal(t, "receive failed: 502 Bad Gateway (retry in 1ms)", lines[0])
- require.Regexp(t, `^receive back after [0-9]+m?s$`, lines[1])
- require.NotContains(t, logs.String(), "secret word")
- require.NotContains(t, logs.String(), strconv.FormatInt(telegramTestOwner, 10))
}
func TestSplitMessage(t *testing.T) {
}
// telegramStartTestBot returns a bot of telegramTestOwner on server, with its
-// offset file in a temporary directory and short waits, and the buffer of its
-// log.
+// offset file in a temporary directory and the buffer of its log.
func telegramStartTestBot(t *testing.T, server *TelegramFake) (*TelegramBot, *SyncBuffer) {
t.Helper()
logs := &SyncBuffer{}
- bot := newTelegramBot(newTelegramAPI(server.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), log.New(logs, "", 0))
- bot.minWait = time.Millisecond
- bot.maxWait = 10 * time.Millisecond
+ bot := newTelegramBot(newTelegramAPI(server.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), daemonTestLogger(logs))
return bot, logs
}