1package provider
2
3import (
4 "bytes"
5 "image"
6 "image/color"
7 "image/jpeg"
8 "image/png"
9 "testing"
10)
11
12func makeRect(w, h int) image.Image {
13 img := image.NewRGBA(image.Rect(0, 0, w, h))
14 // Fill with a recognisable colour so a buggy resize that goes
15 // transparent or zero-size is obvious.
16 c := color.RGBA{R: 80, G: 200, B: 120, A: 255}
17 for y := 0; y < h; y++ {
18 for x := 0; x < w; x++ {
19 img.Set(x, y, c)
20 }
21 }
22 return img
23}
24
25func encodePNG(t *testing.T, img image.Image) []byte {
26 t.Helper()
27 var buf bytes.Buffer
28 if err := png.Encode(&buf, img); err != nil {
29 t.Fatalf("encode png: %v", err)
30 }
31 return buf.Bytes()
32}
33
34func encodeJPEG(t *testing.T, img image.Image) []byte {
35 t.Helper()
36 var buf bytes.Buffer
37 if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
38 t.Fatalf("encode jpeg: %v", err)
39 }
40 return buf.Bytes()
41}
42
43func decodeConfig(t *testing.T, data []byte) image.Config {
44 t.Helper()
45 cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
46 if err != nil {
47 t.Fatalf("decode config: %v", err)
48 }
49 return cfg
50}
51
52func TestAnthShrinkImage_PassesThroughWhenSmall(t *testing.T) {
53 src := encodePNG(t, makeRect(800, 600))
54 out, mime := anthShrinkImageBytesIfTooBig(src, "image/png")
55 if !bytes.Equal(out, src) {
56 t.Errorf("small image was rewritten; expected pass-through")
57 }
58 if mime != "image/png" {
59 t.Errorf("mime changed unexpectedly: %s", mime)
60 }
61}
62
63func TestAnthShrinkImage_CorrectsMislabeledMimeWhenSmall(t *testing.T) {
64 // A JPEG that fits within the cap but is wrongly declared as PNG.
65 // Anthropic 400s on the mismatch, so the builder must rewrite the
66 // declared media type to match the bytes even without resizing.
67 src := encodeJPEG(t, makeRect(800, 600))
68 out, mime := anthShrinkImageBytesIfTooBig(src, "image/png")
69 if !bytes.Equal(out, src) {
70 t.Errorf("small image bytes were rewritten; expected pass-through")
71 }
72 if mime != "image/jpeg" {
73 t.Errorf("mislabeled mime not corrected: got %s want image/jpeg", mime)
74 }
75}
76
77func TestAnthBuildToolResultContent_RepairsMislabeledImageOnResume(t *testing.T) {
78 // Simulates continuing a session whose transcript already carries a
79 // tool_result image with the wrong declared media type (.png name,
80 // JPEG bytes). The outbound request builder must rewrite the media
81 // type to match the bytes so Anthropic accepts the resumed request.
82 jpegBytes := encodeJPEG(t, makeRect(64, 64))
83 blocks := []Content{
84 TextBlock{Text: "screenshot"},
85 ImageBlock{MimeType: "image/png", Data: jpegBytes},
86 }
87 raw, err := anthBuildToolResultContent(blocks)
88 if err != nil {
89 t.Fatalf("build tool result: %v", err)
90 }
91 if !bytes.Contains(raw, []byte(`"media_type":"image/jpeg"`)) {
92 t.Fatalf("media type not repaired in outbound request: %s", raw)
93 }
94 if bytes.Contains(raw, []byte(`"media_type":"image/png"`)) {
95 t.Fatalf("stale image/png media type still present: %s", raw)
96 }
97}
98
99func TestAnthShrinkImage_DownscalesWhenTooWide(t *testing.T) {
100 src := encodePNG(t, makeRect(4000, 1000))
101 out, mime := anthShrinkImageBytesIfTooBig(src, "image/png")
102 if bytes.Equal(out, src) {
103 t.Fatalf("image was not resized")
104 }
105 if mime != "image/png" {
106 t.Errorf("mime changed: %s", mime)
107 }
108 cfg := decodeConfig(t, out)
109 if cfg.Width != anthMaxImageSide {
110 t.Errorf("width: got %d want %d", cfg.Width, anthMaxImageSide)
111 }
112 // Aspect ratio preserved: 4000:1000 -> 2000:500.
113 if cfg.Height != 500 {
114 t.Errorf("height: got %d want 500", cfg.Height)
115 }
116}
117
118func TestAnthShrinkImage_DownscalesWhenTooTall(t *testing.T) {
119 src := encodePNG(t, makeRect(1500, 6000))
120 out, _ := anthShrinkImageBytesIfTooBig(src, "image/png")
121 cfg := decodeConfig(t, out)
122 if cfg.Height != anthMaxImageSide {
123 t.Errorf("height: got %d want %d", cfg.Height, anthMaxImageSide)
124 }
125 // 1500:6000 -> 500:2000.
126 if cfg.Width != 500 {
127 t.Errorf("width: got %d want 500", cfg.Width)
128 }
129}
130
131func TestAnthShrinkImage_PreservesJPEGFormat(t *testing.T) {
132 src := encodeJPEG(t, makeRect(3000, 2500))
133 out, mime := anthShrinkImageBytesIfTooBig(src, "image/jpeg")
134 if mime != "image/jpeg" {
135 t.Errorf("mime should stay image/jpeg, got %s", mime)
136 }
137 cfg := decodeConfig(t, out)
138 if cfg.Width > anthMaxImageSide || cfg.Height > anthMaxImageSide {
139 t.Errorf("dimensions exceed cap: %dx%d", cfg.Width, cfg.Height)
140 }
141}
142
143func TestAnthShrinkImage_BadDataReturnsOriginal(t *testing.T) {
144 src := []byte("not an image at all")
145 out, mime := anthShrinkImageBytesIfTooBig(src, "image/png")
146 if !bytes.Equal(out, src) {
147 t.Errorf("garbage input was mutated")
148 }
149 if mime != "image/png" {
150 t.Errorf("mime was changed on bad input: %s", mime)
151 }
152}