]> git.giacomolli.uk - onechat/blob - telegram_image_test.go
Send terminal images by local path
[onechat] / telegram_image_test.go
1 package main
2
3 import (
4         "bytes"
5         "image"
6         "image/jpeg"
7         "net/http"
8         "os"
9         "path/filepath"
10         "strings"
11         "testing"
12         "time"
13
14         "github.com/stretchr/testify/require"
15 )
16
17 func TestTelegramSelectsLargestPhotoAtMost1568Pixels(t *testing.T) {
18         server := newModelFake(t)
19         test := newTestTurnLoop(t, server)
20         telegram := newTelegramFake(t)
21         bot, logs := telegramStartTestBot(t, telegram)
22         picture := imageTestPNG(t, 8, 6)
23         telegram.Image("selected", picture)
24
25         message := telegramOwnerMessage("")
26         message.Caption = "Read this label.\n"
27         message.Photo = []TelegramPhoto{
28                 {
29                         FileID: "small",
30                         Width:  100,
31                         Height: 200,
32                 },
33                 {
34                         FileID: "too-large",
35                         Width:  1600,
36                         Height: 1200,
37                 },
38                 {
39                         FileID: "selected",
40                         Width:  1568,
41                         Height: 1176,
42                 },
43                 {
44                         FileID: "medium",
45                         Width:  800,
46                         Height: 600,
47                 },
48         }
49         require.NoError(t, bot.handle(t.Context(), test.loop, nil, TelegramUpdate{
50                 UpdateID: 1,
51                 Message:  message,
52         }))
53         test.events.WaitFor(t, "idle", 1)
54
55         files, err := os.ReadDir(test.agent.Images().dir)
56         require.NoError(t, err)
57         require.Len(t, files, 1)
58
59         logged, err := test.memory.Message(0)
60         require.NoError(t, err)
61         require.Equal(t, imageText([]string{files[0].Name()}, message.Caption), logged.Text)
62         require.True(t, strings.HasSuffix(logged.Text, "\n"))
63         require.Contains(t, string(server.Call(0).raw), "input_image")
64         require.NotContains(t, logs.String(), "base64")
65         require.NotContains(t, logs.String(), "Read this label")
66
67         for _, call := range telegram.Calls() {
68                 if call.method == "getFile" {
69                         require.Equal(t, "selected", call.params["file_id"])
70                 }
71         }
72 }
73
74 func TestTelegramImageDocumentsUseContentFormatAndRejectOtherMedia(t *testing.T) {
75         telegram := newTelegramFake(t)
76         png := imageTestPNG(t, 2, 2)
77
78         var jpegBytes bytes.Buffer
79
80         require.NoError(t, jpeg.Encode(&jpegBytes, image.NewRGBA(image.Rect(0, 0, 2, 2)), nil))
81
82         // A made-up one-pixel lossless WebP image.
83         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}
84
85         telegram.Image("png", png)
86         telegram.Image("jpeg", jpegBytes.Bytes())
87         telegram.Image("webp", webp)
88         telegram.Image("invalid", []byte("not an image"))
89         telegram.Image("large", bytes.Repeat([]byte{0}, imageMaxBytes+1))
90
91         api := newTelegramAPI(telegram.URL, telegramTestToken)
92
93         store := &ImageStore{dir: filepath.Join(t.TempDir(), imageDirName)}
94         testCases := []struct {
95                 name     string
96                 document TelegramDocument
97                 suffix   string
98                 failure  string
99         }{
100                 {
101                         name: "accepts PNG documents",
102                         document: TelegramDocument{
103                                 FileID:   "png",
104                                 MimeType: "image/png",
105                         },
106                         suffix: ".png]",
107                 },
108                 {
109                         name: "accepts JPEG documents",
110                         document: TelegramDocument{
111                                 FileID:   "jpeg",
112                                 MimeType: "image/jpeg",
113                         },
114                         suffix: ".jpg]",
115                 },
116                 {
117                         name: "accepts WebP documents",
118                         document: TelegramDocument{
119                                 FileID:   "webp",
120                                 MimeType: "image/webp",
121                         },
122                         suffix: ".webp]",
123                 },
124                 {
125                         name: "other documents keep text answer",
126                         document: TelegramDocument{
127                                 FileID:   "pdf",
128                                 MimeType: "application/pdf",
129                         },
130                         failure: "only text messages are supported",
131                 },
132                 {
133                         name: "other images refused",
134                         document: TelegramDocument{
135                                 FileID:   "gif",
136                                 MimeType: "image/gif",
137                         },
138                         failure: "JPEG, PNG, or WebP",
139                 },
140                 {
141                         name: "invalid image refused",
142                         document: TelegramDocument{
143                                 FileID:   "invalid",
144                                 MimeType: "image/png",
145                         },
146                         failure: "JPEG, PNG, or WebP",
147                 },
148                 {
149                         name: "large download refused",
150                         document: TelegramDocument{
151                                 FileID:   "large",
152                                 MimeType: "image/png",
153                         },
154                         failure: "exceeds 20 MB",
155                 },
156                 {
157                         name: "large metadata refused",
158                         document: TelegramDocument{
159                                 FileID:   "not-downloaded",
160                                 MimeType: "image/png",
161                                 FileSize: imageMaxBytes + 1,
162                         },
163                         failure: "exceeds 20 MB",
164                 },
165         }
166         for _, tc := range testCases {
167                 t.Run(tc.name, func(t *testing.T) {
168                         text, err := telegramImageText(t.Context(), api, store, &TelegramMessage{Document: &tc.document})
169                         if tc.failure != "" {
170                                 require.ErrorContains(t, err, tc.failure)
171
172                                 return
173                         }
174
175                         require.NoError(t, err)
176                         require.True(t, strings.HasSuffix(text, tc.suffix))
177                 })
178         }
179 }
180
181 func TestTelegramRetriesImageDownloadBeforeAdvancingOffset(t *testing.T) {
182         telegram := newTelegramFake(t)
183         telegram.Image("photo", imageTestPNG(t, 2, 2))
184
185         tries := 0
186         telegram.fail = func(call TelegramFakeCall) (int, string) {
187                 if call.method == "getFile" {
188                         tries++
189                         if tries == 1 {
190                                 return http.StatusServiceUnavailable, "try again"
191                         }
192                 }
193
194                 return 0, ""
195         }
196         bot, logs := telegramStartTestBot(t, telegram)
197         bot.minWait = time.Millisecond
198         bot.maxWait = 5 * time.Millisecond
199         server := newModelFake(t)
200         socket := daemonStartTestWithBot(t, server, bot)
201         terminal := terminalStartTestClient(t, socket)
202         terminal.WaitFor(t, "[connected to OneChat]")
203
204         message := telegramOwnerMessage("")
205         message.Photo = []TelegramPhoto{{
206                 FileID: "photo",
207                 Width:  2,
208                 Height: 2,
209         }}
210         telegram.Add(TelegramUpdate{
211                 UpdateID: 7,
212                 Message:  message,
213         })
214         terminal.WaitFor(t, "[done]")
215         require.Eventually(t, func() bool {
216                 offset, err := telegramLoadOffset(bot.offsetPath)
217                 return err == nil && offset == 8
218         }, 10*time.Second, time.Millisecond)
219         require.Contains(t, logs.String(), "receive failed")
220         require.Contains(t, logs.String(), "receive back")
221         require.NotContains(t, logs.String(), telegramTestToken)
222         require.NotContains(t, logs.String(), "base64")
223 }
224
225 func TestTelegramRefusesPhotoWhenEverySizeIsTooLarge(t *testing.T) {
226         telegram := newTelegramFake(t)
227         _, err := telegramImageText(t.Context(), newTelegramAPI(telegram.URL, telegramTestToken), &ImageStore{dir: t.TempDir()}, &TelegramMessage{Photo: []TelegramPhoto{{
228                 FileID: "too-big",
229                 Width:  1600,
230                 Height: 1000,
231         }}})
232         require.ErrorContains(t, err, "1568 pixels")
233         require.Empty(t, telegram.Calls())
234 }
235
236 func TestTelegramStorageFailureDoesNotAcceptTheImage(t *testing.T) {
237         telegram := newTelegramFake(t)
238         telegram.Image("photo", imageTestPNG(t, 2, 2))
239
240         test := newTestTurnLoop(t, newModelFake(t))
241         require.NoError(t, os.WriteFile(test.agent.Images().dir, []byte("blocked"), 0o600))
242
243         bot, _ := telegramStartTestBot(t, telegram)
244         message := telegramOwnerMessage("")
245         message.Photo = []TelegramPhoto{{
246                 FileID: "photo",
247                 Width:  2,
248                 Height: 2,
249         }}
250         err := bot.handle(t.Context(), test.loop, nil, TelegramUpdate{
251                 UpdateID: 1,
252                 Message:  message,
253         })
254         require.ErrorContains(t, err, "create image directory")
255         require.Equal(t, 0, test.memory.Len())
256
257         _, found := bot.next()
258         require.False(t, found)
259 }