// Tests for the prompt image-reference syntax (US-010, #126): @image: and // the Markdown ![alt]() form. They write a tiny real PNG to a temp file so // loadImageContent exercises the real read + base64 + mime path, and assert that // a missing file is an error (not a silently dropped image). package ui import ( "encoding/base64" "os" "path/filepath" "strings" "testing" "github.com/smallnest/pigo/internal/agentcore" ) // pngBytes is a minimal 1x1 PNG (valid signature + IHDR) so mime detection and // the image/ prefix check pass without pulling in an image library. var pngBytes = []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, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, } func writeTempPNG(t *testing.T) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "pixel.png") if err := os.WriteFile(path, pngBytes, 0o644); err != nil { t.Fatalf("write temp png: %v", err) } return path } // TestBuildUserContentNoImage: a plain prompt yields a single text block. func TestBuildUserContentNoImage(t *testing.T) { content, err := BuildUserContent("just some text") if err != nil { t.Fatalf("BuildUserContent: %v", err) } if len(content) != 1 { t.Fatalf("content len = %d, want 1", len(content)) } if tc, ok := content[0].(agentcore.TextContent); !ok || tc.Text != "just some text" { t.Errorf("content[0] = %#v, want text \"just some text\"", content[0]) } } // TestBuildUserContentAtImageSyntax: @image: attaches an ImageContent and // keeps the surrounding text. func TestBuildUserContentAtImageSyntax(t *testing.T) { path := writeTempPNG(t) content, err := BuildUserContent("look at @image:" + path + " please") if err != nil { t.Fatalf("BuildUserContent: %v", err) } assertTextThenImage(t, content, "look at", "image/png") } // TestBuildUserContentMarkdownSyntax: ![alt](path) attaches an ImageContent. func TestBuildUserContentMarkdownSyntax(t *testing.T) { path := writeTempPNG(t) content, err := BuildUserContent("before ![a pixel](" + path + ") after") if err != nil { t.Fatalf("BuildUserContent: %v", err) } // text "before", image, text "after" if len(content) != 3 { t.Fatalf("content len = %d, want 3", len(content)) } if _, ok := content[1].(agentcore.ImageContent); !ok { t.Errorf("content[1] = %T, want ImageContent", content[1]) } } // TestBuildUserContentMissingFile: a missing referenced file is an error. func TestBuildUserContentMissingFile(t *testing.T) { _, err := BuildUserContent("@image:/no/such/file.png") if err == nil { t.Fatal("BuildUserContent accepted a missing image file, want error") } } // TestLoadImageContentBase64: the encoded data must match the file bytes. func TestLoadImageContentBase64(t *testing.T) { path := writeTempPNG(t) img, err := loadImageContent(path) if err != nil { t.Fatalf("loadImageContent: %v", err) } if img.MimeType != "image/png" { t.Errorf("mime = %q, want image/png", img.MimeType) } if want := base64.StdEncoding.EncodeToString(pngBytes); img.Data != want { t.Errorf("data mismatch") } } func assertTextThenImage(t *testing.T, content agentcore.ContentList, wantText, wantMime string) { t.Helper() var sawText, sawImage bool for _, c := range content { switch b := c.(type) { case agentcore.TextContent: if strings.Contains(b.Text, wantText) { sawText = true } case agentcore.ImageContent: if b.MimeType == wantMime { sawImage = true } } } if !sawText { t.Errorf("no text block containing %q in %#v", wantText, content) } if !sawImage { t.Errorf("no image block with mime %q in %#v", wantMime, content) } }