]> git.giacomolli.uk - onechat/blob - terminal_image_test.go
Send terminal images by local path
[onechat] / terminal_image_test.go
1 package main
2
3 import (
4         "bytes"
5         "encoding/json"
6         "net"
7         "os"
8         "path/filepath"
9         "strings"
10         "testing"
11
12         "github.com/stretchr/testify/require"
13 )
14
15 func TestTerminalImageLineSendsAnAbsolutePathAndCaption(t *testing.T) {
16         client, daemon := net.Pipe()
17         defer func() {
18                 require.NoError(t, client.Close())
19                 require.NoError(t, daemon.Close())
20         }()
21
22         done := make(chan error, 1)
23         go func() {
24                 done <- terminalSendLines(t.Context(), client, strings.NewReader("/image 'image sample.png' Read the label.\n"))
25         }()
26
27         var request SocketRequest
28
29         require.NoError(t, json.NewDecoder(daemon).Decode(&request))
30         require.NoError(t, <-done)
31
32         expected, err := filepath.Abs("image sample.png")
33         require.NoError(t, err)
34         require.Equal(t, SocketRequest{
35                 Type: "image",
36                 Path: expected,
37                 Text: "Read the label.",
38         }, request)
39 }
40
41 func TestTerminalImagePathsAndCaptionsHaveNoShellExpansion(t *testing.T) {
42         testCases := []struct {
43                 name    string
44                 line    string
45                 path    string
46                 caption string
47                 command bool
48         }{
49                 {
50                         name:    "accepts paths without captions",
51                         line:    "/image sample.png",
52                         path:    "sample.png",
53                         command: true,
54                 },
55                 {
56                         name:    "keeps spaces in double quoted paths",
57                         line:    `/image "a b.png" caption`,
58                         path:    "a b.png",
59                         caption: "caption",
60                         command: true,
61                 },
62                 {
63                         name:    "caption stays literal",
64                         line:    "/image sample.png $(not-a-command)  ",
65                         path:    "sample.png",
66                         caption: "$(not-a-command)  ",
67                         command: true,
68                 },
69                 {
70                         name:    "missing path refused",
71                         line:    "/image",
72                         command: true,
73                 },
74                 {
75                         name:    "unclosed quote refused",
76                         line:    `/image "sample.png`,
77                         command: true,
78                 },
79                 {
80                         name:    "joined caption refused",
81                         line:    `/image "sample.png"caption`,
82                         command: true,
83                 },
84                 {
85                         name:    "other command stays text",
86                         line:    "/images sample.png",
87                         command: false,
88                 },
89         }
90         for _, tc := range testCases {
91                 t.Run(tc.name, func(t *testing.T) {
92                         request, command := terminalImageRequest(tc.line)
93                         require.Equal(t, tc.command, command)
94                         require.Equal(t, tc.path, request.Path)
95                         require.Equal(t, tc.caption, request.Text)
96                 })
97         }
98 }
99
100 func TestTerminalImageIsStoredBeforeItReachesTheModel(t *testing.T) {
101         server := newModelFake(t)
102         socket := daemonStartTest(t, server)
103         terminal := terminalStartTestClient(t, socket)
104         terminal.WaitFor(t, "[connected to OneChat]")
105
106         path := filepath.Join(t.TempDir(), "sample.png")
107         data := imageTestPNG(t, 2, 2)
108         require.NoError(t, os.WriteFile(path, data, 0o600))
109         terminal.Type("/image " + path + " What is this?")
110         terminal.WaitFor(t, "[done]")
111         require.Equal(t, 1, server.Calls())
112         require.Contains(t, string(server.Call(0).raw), "input_image")
113         require.Contains(t, string(server.Call(0).raw), "What is this?")
114         require.NotContains(t, string(server.Call(0).raw), path)
115
116         files, err := os.ReadDir(filepath.Join(filepath.Dir(socket), imageDirName))
117         require.NoError(t, err)
118         require.Len(t, files, 1)
119
120         stored, err := os.ReadFile(filepath.Join(filepath.Dir(socket), imageDirName, files[0].Name()))
121         require.NoError(t, err)
122         require.Equal(t, data, stored)
123         require.NotContains(t, terminal.Output(), "base64")
124 }
125
126 func TestTerminalRefusesLargeAndUnsupportedImagesWithoutAModelCall(t *testing.T) {
127         server := newModelFake(t)
128         test := newTestTurnLoop(t, server)
129         path := filepath.Join(t.TempDir(), "sample.png")
130         require.NoError(t, os.WriteFile(path, bytes.Repeat([]byte{0}, imageMaxBytes+1), 0o600))
131
132         request := SocketRequest{
133                 Type: "image",
134                 Path: path,
135         }
136         require.ErrorContains(t, daemonRunRequest(test.loop, nil, request, func(string) {
137         }), "exceeds 20 MB")
138         require.NoError(t, os.WriteFile(path, []byte("not an image"), 0o600))
139         require.ErrorContains(t, daemonRunRequest(test.loop, nil, request, func(string) {
140         }), "JPEG, PNG, or WebP")
141         require.Equal(t, 0, test.memory.Len())
142         require.Equal(t, 0, server.Calls())
143
144         _, err := os.Stat(test.agent.Images().dir)
145         require.ErrorIs(t, err, os.ErrNotExist)
146 }
147
148 func TestImageCommandErrorDoesNotEndTheTerminalClient(t *testing.T) {
149         socket := daemonStartTest(t, newModelFake(t))
150         terminal := terminalStartTestClient(t, socket)
151         terminal.WaitFor(t, "[connected to OneChat]")
152         terminal.Type("/image")
153         terminal.WaitFor(t, "use /image PATH [caption]")
154         terminal.Type("Continue.")
155         terminal.WaitFor(t, "[done]")
156         require.NoError(t, terminal.Close())
157 }