+package main
+
+import (
+ "bytes"
+ "image"
+ "image/jpeg"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestTelegramSelectsLargestPhotoAtMost1568Pixels(t *testing.T) {
+ server := newModelFake(t)
+ test := newTestTurnLoop(t, server)
+ telegram := newTelegramFake(t)
+ bot, logs := telegramStartTestBot(t, telegram)
+ picture := imageTestPNG(t, 8, 6)
+ telegram.Image("selected", picture)
+
+ message := telegramOwnerMessage("")
+ message.Caption = "Read this label.\n"
+ message.Photo = []TelegramPhoto{
+ {
+ FileID: "small",
+ Width: 100,
+ Height: 200,
+ },
+ {
+ FileID: "too-large",
+ Width: 1600,
+ Height: 1200,
+ },
+ {
+ FileID: "selected",
+ Width: 1568,
+ Height: 1176,
+ },
+ {
+ FileID: "medium",
+ Width: 800,
+ Height: 600,
+ },
+ }
+ require.NoError(t, bot.handle(t.Context(), test.loop, nil, TelegramUpdate{
+ UpdateID: 1,
+ Message: message,
+ }))
+ test.events.WaitFor(t, "idle", 1)
+
+ files, err := os.ReadDir(test.agent.Images().dir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+
+ logged, err := test.memory.Message(0)
+ require.NoError(t, err)
+ require.Equal(t, imageText([]string{files[0].Name()}, message.Caption), logged.Text)
+ require.True(t, strings.HasSuffix(logged.Text, "\n"))
+ require.Contains(t, string(server.Call(0).raw), "input_image")
+ require.NotContains(t, logs.String(), "base64")
+ require.NotContains(t, logs.String(), "Read this label")
+
+ for _, call := range telegram.Calls() {
+ if call.method == "getFile" {
+ require.Equal(t, "selected", call.params["file_id"])
+ }
+ }
+}
+
+func TestTelegramImageDocumentsUseContentFormatAndRejectOtherMedia(t *testing.T) {
+ telegram := newTelegramFake(t)
+ png := imageTestPNG(t, 2, 2)
+
+ var jpegBytes bytes.Buffer
+
+ require.NoError(t, jpeg.Encode(&jpegBytes, image.NewRGBA(image.Rect(0, 0, 2, 2)), nil))
+
+ // A made-up one-pixel lossless WebP image.
+ webp := []byte{'R', 'I', 'F', 'F', 22, 0, 0, 0, 'W', 'E', 'B', 'P', 'V', 'P', '8', 'L', 10, 0, 0, 0, 0x2f, 0, 0, 0, 0, 0x07, 0x10, 0x11, 0x11, 0x88}
+
+ telegram.Image("png", png)
+ telegram.Image("jpeg", jpegBytes.Bytes())
+ telegram.Image("webp", webp)
+ telegram.Image("invalid", []byte("not an image"))
+ telegram.Image("large", bytes.Repeat([]byte{0}, imageMaxBytes+1))
+
+ api := newTelegramAPI(telegram.URL, telegramTestToken)
+
+ store := &ImageStore{dir: filepath.Join(t.TempDir(), imageDirName)}
+ testCases := []struct {
+ name string
+ document TelegramDocument
+ suffix string
+ failure string
+ }{
+ {
+ name: "accepts PNG documents",
+ document: TelegramDocument{
+ FileID: "png",
+ MimeType: "image/png",
+ },
+ suffix: ".png]",
+ },
+ {
+ name: "accepts JPEG documents",
+ document: TelegramDocument{
+ FileID: "jpeg",
+ MimeType: "image/jpeg",
+ },
+ suffix: ".jpg]",
+ },
+ {
+ name: "accepts WebP documents",
+ document: TelegramDocument{
+ FileID: "webp",
+ MimeType: "image/webp",
+ },
+ suffix: ".webp]",
+ },
+ {
+ name: "other documents keep text answer",
+ document: TelegramDocument{
+ FileID: "pdf",
+ MimeType: "application/pdf",
+ },
+ failure: "only text messages are supported",
+ },
+ {
+ name: "other images refused",
+ document: TelegramDocument{
+ FileID: "gif",
+ MimeType: "image/gif",
+ },
+ failure: "JPEG, PNG, or WebP",
+ },
+ {
+ name: "invalid image refused",
+ document: TelegramDocument{
+ FileID: "invalid",
+ MimeType: "image/png",
+ },
+ failure: "JPEG, PNG, or WebP",
+ },
+ {
+ name: "large download refused",
+ document: TelegramDocument{
+ FileID: "large",
+ MimeType: "image/png",
+ },
+ failure: "exceeds 20 MB",
+ },
+ {
+ name: "large metadata refused",
+ document: TelegramDocument{
+ FileID: "not-downloaded",
+ MimeType: "image/png",
+ FileSize: imageMaxBytes + 1,
+ },
+ failure: "exceeds 20 MB",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ text, err := telegramImageText(t.Context(), api, store, &TelegramMessage{Document: &tc.document})
+ if tc.failure != "" {
+ require.ErrorContains(t, err, tc.failure)
+
+ return
+ }
+
+ require.NoError(t, err)
+ require.True(t, strings.HasSuffix(text, tc.suffix))
+ })
+ }
+}
+
+func TestTelegramRetriesImageDownloadBeforeAdvancingOffset(t *testing.T) {
+ telegram := newTelegramFake(t)
+ telegram.Image("photo", imageTestPNG(t, 2, 2))
+
+ tries := 0
+ telegram.fail = func(call TelegramFakeCall) (int, string) {
+ if call.method == "getFile" {
+ tries++
+ if tries == 1 {
+ return http.StatusServiceUnavailable, "try again"
+ }
+ }
+
+ return 0, ""
+ }
+ bot, logs := telegramStartTestBot(t, telegram)
+ bot.minWait = time.Millisecond
+ bot.maxWait = 5 * time.Millisecond
+ server := newModelFake(t)
+ socket := daemonStartTestWithBot(t, server, bot)
+ terminal := terminalStartTestClient(t, socket)
+ terminal.WaitFor(t, "[connected to OneChat]")
+
+ message := telegramOwnerMessage("")
+ message.Photo = []TelegramPhoto{{
+ FileID: "photo",
+ Width: 2,
+ Height: 2,
+ }}
+ telegram.Add(TelegramUpdate{
+ UpdateID: 7,
+ Message: message,
+ })
+ terminal.WaitFor(t, "[done]")
+ require.Eventually(t, func() bool {
+ offset, err := telegramLoadOffset(bot.offsetPath)
+ return err == nil && offset == 8
+ }, 10*time.Second, time.Millisecond)
+ require.Contains(t, logs.String(), "receive failed")
+ require.Contains(t, logs.String(), "receive back")
+ require.NotContains(t, logs.String(), telegramTestToken)
+ require.NotContains(t, logs.String(), "base64")
+}
+
+func TestTelegramRefusesPhotoWhenEverySizeIsTooLarge(t *testing.T) {
+ telegram := newTelegramFake(t)
+ _, err := telegramImageText(t.Context(), newTelegramAPI(telegram.URL, telegramTestToken), &ImageStore{dir: t.TempDir()}, &TelegramMessage{Photo: []TelegramPhoto{{
+ FileID: "too-big",
+ Width: 1600,
+ Height: 1000,
+ }}})
+ require.ErrorContains(t, err, "1568 pixels")
+ require.Empty(t, telegram.Calls())
+}
+
+func TestTelegramStorageFailureDoesNotAcceptTheImage(t *testing.T) {
+ telegram := newTelegramFake(t)
+ telegram.Image("photo", imageTestPNG(t, 2, 2))
+
+ test := newTestTurnLoop(t, newModelFake(t))
+ require.NoError(t, os.WriteFile(test.agent.Images().dir, []byte("blocked"), 0o600))
+
+ bot, _ := telegramStartTestBot(t, telegram)
+ message := telegramOwnerMessage("")
+ message.Photo = []TelegramPhoto{{
+ FileID: "photo",
+ Width: 2,
+ Height: 2,
+ }}
+ err := bot.handle(t.Context(), test.loop, nil, TelegramUpdate{
+ UpdateID: 1,
+ Message: message,
+ })
+ require.ErrorContains(t, err, "create image directory")
+ require.Equal(t, 0, test.memory.Len())
+
+ _, found := bot.next()
+ require.False(t, found)
+}