Files
weichenandCursor cfde38de31 Send current-turn images and keep vision fallbacks capable
Why: Uploaded images never reached the model, so a text-only chat model could be asked to read a picture it cannot see.

Evidence: go test ./internal/... passed locally before this commit. Image-link rejection is covered by vision package tests.

Impact: Current-turn attachments and fetched image links become image input. History keeps a text placeholder. Image turns swap to the configured vision model when the chat model is not in the capable list, and fallbacks are filtered the same way.

Refs: Agent-System-Dev feature/four-model-vision
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 17:51:17 +00:00

121 lines
4.0 KiB
Go

package vision
import (
"context"
"encoding/base64"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
func TestCheckIP(t *testing.T) {
blocked := []string{"127.0.0.1", "10.1.2.3", "192.168.1.1", "172.16.0.1", "169.254.169.254", "100.64.0.1", "::1", "fc00::1", "0.0.0.0"}
for _, s := range blocked {
if err := checkIP(net.ParseIP(s)); err == nil {
t.Errorf("%s should be blocked", s)
}
}
if err := checkIP(net.ParseIP("8.8.8.8")); err != nil {
t.Fatal(err)
}
if err := checkIP(net.ParseIP("1.1.1.1")); err != nil {
t.Fatal(err)
}
}
func TestValidateImageURLRejectsUnsafeTargets(t *testing.T) {
ctx := context.Background()
for _, raw := range []string{
"http://127.0.0.1/a.png",
"http://169.254.169.254/latest/a.png",
"http://10.0.0.8/a.jpg",
"http://user:pass@8.8.8.8/a.png",
"file:///tmp/a.png",
"http://localhost/a.png",
} {
if err := validateImageURL(ctx, raw); err == nil {
t.Errorf("accepted %s", raw)
}
}
if err := validateImageURL(ctx, "https://8.8.8.8/a.png"); err != nil {
t.Fatal(err)
}
}
func TestFetchImageRejectsLoopbackServer(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write(onePixelPNG)
}))
defer srv.Close()
_, err := FetchImage(context.Background(), srv.URL+"/pixel.png")
if err == nil || !strings.Contains(err.Error(), "允许范围") {
t.Fatalf("err = %v", err)
}
}
func TestImageLinksAndAttachNote(t *testing.T) {
text := "看这张 http://127.0.0.1/a.png。还有 https://cdn.example.com/b.JPG?x=1 和重复 http://127.0.0.1/a.png"
links := ImageLinks(text)
if len(links) != 2 || links[0] != "http://127.0.0.1/a.png" || !strings.Contains(links[1], "b.JPG") {
t.Fatalf("%#v", links)
}
msgs := []agentllm.Message{{Role: agentllm.RoleUser, Content: "请看 http://127.0.0.1/secret.png"}}
notes := AttachLinkedImages(context.Background(), msgs)
if len(notes) == 0 || len(msgs[0].Images) != 0 {
t.Fatalf("notes=%v images=%d", notes, len(msgs[0].Images))
}
if !strings.Contains(msgs[0].Content, "地址不在允许范围") {
t.Fatalf("content missing reason: %s", msgs[0].Content)
}
}
func TestPrepareImageAndBudget(t *testing.T) {
block, warn, ok := PrepareImage("pixel.png", "image/png", onePixelPNG)
if !ok || !strings.Contains(warn, "按图像发送") || block.Type != core.BlockImageRef || block.Ref == "" {
t.Fatalf("ok=%v warn=%q block=%+v", ok, warn, block)
}
if _, warn, ok := PrepareImage("icon.svg", "image/svg+xml", []byte("<svg/>")); ok || !strings.Contains(warn, "不作为图像发送") {
t.Fatalf("svg ok=%v warn=%q", ok, warn)
}
big := make([]byte, MaxImageBytes+1)
if _, warn, ok := PrepareImage("big.png", "image/png", big); ok || !strings.Contains(warn, "超过") {
t.Fatalf("big ok=%v warn=%q", ok, warn)
}
if _, warn, ok := PrepareImage("bad.png", "image/png", []byte("not a png")); ok || !strings.Contains(warn, "不符") {
t.Fatalf("bad ok=%v warn=%q", ok, warn)
}
img := agentllm.Image{MIME: "image/png", Data: block.Ref}
kept, notes := Admit(nil, []agentllm.Image{img})
if len(kept) != 1 || len(notes) != 0 {
t.Fatalf("kept=%d notes=%v", len(kept), notes)
}
many := make([]agentllm.Image, MaxImages+1)
for i := range many {
many[i] = img
}
kept, notes = Admit(nil, many)
if len(kept) != MaxImages || len(notes) == 0 {
t.Fatalf("kept=%d notes=%v", len(kept), notes)
}
raw, err := base64.StdEncoding.DecodeString(block.Ref)
if err != nil || len(raw) != len(onePixelPNG) {
t.Fatal(err)
}
}
// 1x1 PNG。
var onePixelPNG = []byte{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x50,
0x0f, 0x00, 0x04, 0x85, 0x01, 0x80, 0x84, 0xa9, 0x8c, 0x21, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
}