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)
}
}