Vectrum
Sign In
137 lines · 4.1 KB · Go
package tools

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"image"
	"image/color"
	"image/png"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"codey/internal/llm"
)

func writePNG(t *testing.T, dir, name string, w, h int) string {
	t.Helper()
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for x := 0; x < w; x++ {
		for y := 0; y < h; y++ {
			img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 128, A: 255})
		}
	}
	var buf bytes.Buffer
	if err := png.Encode(&buf, img); err != nil {
		t.Fatal(err)
	}
	p := filepath.Join(dir, name)
	if err := os.WriteFile(p, buf.Bytes(), 0o644); err != nil {
		t.Fatal(err)
	}
	return p
}

func readBlocks(t *testing.T, tool Tool, path string) (Output, error) {
	t.Helper()
	bt, ok := tool.(BlockTool)
	if !ok {
		t.Fatalf("read_file does not implement BlockTool; an image would go to the model as bytes-as-text")
	}
	raw, _ := json.Marshal(ReadArgs{Path: path})
	return bt.ExecuteBlocks(context.Background(), raw)
}

// A screenshot the agent took is only useful if the model can see it. A PNG
// comes back as an image block with a caption, not as a page of binary.
func TestReadFileReturnsAnImageForAPNG(t *testing.T) {
	dir := t.TempDir()
	writePNG(t, dir, "shot.png", 12, 7)
	track := &fakeTracker{}

	out, err := readBlocks(t, NewRead(dir, track), "shot.png")
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if len(out.Blocks) != 2 || out.Blocks[0].Type != llm.BlockText || out.Blocks[1].Type != llm.BlockImage {
		t.Fatalf("blocks = %+v, want a caption then an image", out.Blocks)
	}
	img := out.Blocks[1]
	if img.Source == nil || img.Source.MediaType != "image/png" || img.Source.Type != "base64" {
		t.Errorf("image source = %+v, want base64 image/png", img.Source)
	}
	decoded, err := base64.StdEncoding.DecodeString(img.Source.Data)
	if err != nil {
		t.Fatalf("image data is not base64: %v", err)
	}
	if cfg, err := png.DecodeConfig(bytes.NewReader(decoded)); err != nil || cfg.Width != 12 || cfg.Height != 7 {
		t.Errorf("decoded image is not the file that was written: %v %+v", err, cfg)
	}
	for _, want := range []string{"shot.png", "PNG", "12×7"} {
		if !strings.Contains(out.Text, want) {
			t.Errorf("caption %q does not say %q", out.Text, want)
		}
	}
	if out.Text != out.Blocks[0].Text {
		t.Errorf("Text %q should be the caption block %q", out.Text, out.Blocks[0].Text)
	}
	if track.n != 1 {
		t.Errorf("the read was not tracked")
	}
}

func TestReadFileImageMediaTypeFollowsTheExtension(t *testing.T) {
	for ext, want := range map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".PNG": "image/png"} {
		if got, ok := imageMediaType("x" + ext); !ok || got != want {
			t.Errorf("%s: got %q %v, want %q", ext, got, ok, want)
		}
	}
	for _, ext := range []string{".go", ".svg", ".txt", ""} {
		if _, ok := imageMediaType("x" + ext); ok {
			t.Errorf("%q should not be treated as an image", ext)
		}
	}
}

// Text files keep their line-numbered form: the block path is for pictures.
func TestReadFileTextGoesThroughUnchanged(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "a.go", "package a\n")
	out, err := readBlocks(t, NewRead(dir, nil), "a.go")
	if err != nil {
		t.Fatal(err)
	}
	if len(out.Blocks) != 0 || !strings.Contains(out.Text, "1\tpackage a") {
		t.Errorf("text read changed shape: %+v", out)
	}
}

func TestReadFileRefusesAnOversizedImage(t *testing.T) {
	dir := t.TempDir()
	p := filepath.Join(dir, "huge.png")
	if err := os.WriteFile(p, make([]byte, maxImageBytes+1), 0o644); err != nil {
		t.Fatal(err)
	}
	_, err := readBlocks(t, NewRead(dir, nil), "huge.png")
	if err == nil || !strings.Contains(err.Error(), "too large") {
		t.Errorf("err = %v, want a size refusal that says what to do instead", err)
	}
}

// The registry hands a BlockTool's blocks through and logs that it did.
func TestRegistryExecutePassesBlocksThrough(t *testing.T) {
	dir := t.TempDir()
	writePNG(t, dir, "s.png", 2, 2)
	r := NewRegistry(nil).Register(NewRead(dir, nil))
	raw, _ := json.Marshal(ReadArgs{Path: "s.png"})
	out, err := r.Execute(context.Background(), "read_file", raw)
	if err != nil {
		t.Fatal(err)
	}
	if len(out.Blocks) != 2 {
		t.Errorf("registry dropped the blocks: %+v", out)
	}
}