+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestTerminalImageLineSendsAnAbsolutePathAndCaption(t *testing.T) {
+ client, daemon := net.Pipe()
+ defer func() {
+ require.NoError(t, client.Close())
+ require.NoError(t, daemon.Close())
+ }()
+
+ done := make(chan error, 1)
+ go func() {
+ done <- terminalSendLines(t.Context(), client, strings.NewReader("/image 'image sample.png' Read the label.\n"))
+ }()
+
+ var request SocketRequest
+
+ require.NoError(t, json.NewDecoder(daemon).Decode(&request))
+ require.NoError(t, <-done)
+
+ expected, err := filepath.Abs("image sample.png")
+ require.NoError(t, err)
+ require.Equal(t, SocketRequest{
+ Type: "image",
+ Path: expected,
+ Text: "Read the label.",
+ }, request)
+}
+
+func TestTerminalImagePathsAndCaptionsHaveNoShellExpansion(t *testing.T) {
+ testCases := []struct {
+ name string
+ line string
+ path string
+ caption string
+ command bool
+ }{
+ {
+ name: "accepts paths without captions",
+ line: "/image sample.png",
+ path: "sample.png",
+ command: true,
+ },
+ {
+ name: "keeps spaces in double quoted paths",
+ line: `/image "a b.png" caption`,
+ path: "a b.png",
+ caption: "caption",
+ command: true,
+ },
+ {
+ name: "caption stays literal",
+ line: "/image sample.png $(not-a-command) ",
+ path: "sample.png",
+ caption: "$(not-a-command) ",
+ command: true,
+ },
+ {
+ name: "missing path refused",
+ line: "/image",
+ command: true,
+ },
+ {
+ name: "unclosed quote refused",
+ line: `/image "sample.png`,
+ command: true,
+ },
+ {
+ name: "joined caption refused",
+ line: `/image "sample.png"caption`,
+ command: true,
+ },
+ {
+ name: "other command stays text",
+ line: "/images sample.png",
+ command: false,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ request, command := terminalImageRequest(tc.line)
+ require.Equal(t, tc.command, command)
+ require.Equal(t, tc.path, request.Path)
+ require.Equal(t, tc.caption, request.Text)
+ })
+ }
+}
+
+func TestTerminalImageIsStoredBeforeItReachesTheModel(t *testing.T) {
+ server := newModelFake(t)
+ socket := daemonStartTest(t, server)
+ terminal := terminalStartTestClient(t, socket)
+ terminal.WaitFor(t, "[connected to OneChat]")
+
+ path := filepath.Join(t.TempDir(), "sample.png")
+ data := imageTestPNG(t, 2, 2)
+ require.NoError(t, os.WriteFile(path, data, 0o600))
+ terminal.Type("/image " + path + " What is this?")
+ terminal.WaitFor(t, "[done]")
+ require.Equal(t, 1, server.Calls())
+ require.Contains(t, string(server.Call(0).raw), "input_image")
+ require.Contains(t, string(server.Call(0).raw), "What is this?")
+ require.NotContains(t, string(server.Call(0).raw), path)
+
+ files, err := os.ReadDir(filepath.Join(filepath.Dir(socket), imageDirName))
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+
+ stored, err := os.ReadFile(filepath.Join(filepath.Dir(socket), imageDirName, files[0].Name()))
+ require.NoError(t, err)
+ require.Equal(t, data, stored)
+ require.NotContains(t, terminal.Output(), "base64")
+}
+
+func TestTerminalRefusesLargeAndUnsupportedImagesWithoutAModelCall(t *testing.T) {
+ server := newModelFake(t)
+ test := newTestTurnLoop(t, server)
+ path := filepath.Join(t.TempDir(), "sample.png")
+ require.NoError(t, os.WriteFile(path, bytes.Repeat([]byte{0}, imageMaxBytes+1), 0o600))
+
+ request := SocketRequest{
+ Type: "image",
+ Path: path,
+ }
+ require.ErrorContains(t, daemonRunRequest(test.loop, nil, request, func(string) {
+ }), "exceeds 20 MB")
+ require.NoError(t, os.WriteFile(path, []byte("not an image"), 0o600))
+ require.ErrorContains(t, daemonRunRequest(test.loop, nil, request, func(string) {
+ }), "JPEG, PNG, or WebP")
+ require.Equal(t, 0, test.memory.Len())
+ require.Equal(t, 0, server.Calls())
+
+ _, err := os.Stat(test.agent.Images().dir)
+ require.ErrorIs(t, err, os.ErrNotExist)
+}
+
+func TestImageCommandErrorDoesNotEndTheTerminalClient(t *testing.T) {
+ socket := daemonStartTest(t, newModelFake(t))
+ terminal := terminalStartTestClient(t, socket)
+ terminal.WaitFor(t, "[connected to OneChat]")
+ terminal.Type("/image")
+ terminal.WaitFor(t, "use /image PATH [caption]")
+ terminal.Type("Continue.")
+ terminal.WaitFor(t, "[done]")
+ require.NoError(t, terminal.Close())
+}