Files
mamamiyear 87874e64cf feat: prefer base64 inline image for summary, fall back to S3
Send the screenshot to the multimodal model inline as a base64 data URL
so summarisation no longer requires S3 to be configured or publicly
reachable. Only when the PNG exceeds the provider MaxInlineBytes limit
do we upload to S3, verify the public URL is reachable, and delete the
temporary object once done. Oversized images without S3 now surface a
clear hint instead of a hard S3 requirement.
2026-07-07 13:39:23 +08:00

141 lines
3.8 KiB
Go

package application
import (
"context"
"strings"
"testing"
)
type fakeOSSProvider struct {
key string
data []byte
contentType string
url string
deletedKey string
}
func (f *fakeOSSProvider) Upload(_ context.Context, key string, data []byte, contentType string) (string, error) {
f.key = key
f.data = append([]byte(nil), data...)
f.contentType = contentType
if f.url == "" {
return "https://cdn.example.com/" + key, nil
}
return f.url, nil
}
func (f *fakeOSSProvider) Name() string { return "fake" }
func (f *fakeOSSProvider) Delete(_ context.Context, key string) error {
f.deletedKey = key
return nil
}
type fakeSummarizer struct {
prompt string
url string
text string
}
func (f *fakeSummarizer) SummarizeImage(_ context.Context, prompt string, imageURL string) (string, error) {
f.prompt = prompt
f.url = imageURL
if f.text == "" {
return "summary text", nil
}
return f.text, nil
}
func TestCaptureSummaryServiceUploadsSummarizesAndCopies(t *testing.T) {
ctx := context.Background()
provider := &fakeOSSProvider{}
summarizer := &fakeSummarizer{}
clip := &fakeClipboard{}
svc := &CaptureSummaryService{
Provider: provider,
Summarizer: summarizer,
Clipboard: clip,
Prompt: "describe screenshot",
PathPrefix: "snapgo/",
}
uploaded, err := svc.UploadImage(ctx, []byte("png"))
if err != nil {
t.Fatalf("upload image: %v", err)
}
if provider.contentType != "image/png" {
t.Fatalf("expected image/png content type, got %q", provider.contentType)
}
if string(provider.data) != "png" {
t.Fatalf("expected uploaded png bytes, got %q", string(provider.data))
}
summary, err := svc.Summarize(ctx, uploaded.URL)
if err != nil {
t.Fatalf("summarize: %v", err)
}
if summarizer.prompt != "describe screenshot" {
t.Fatalf("expected prompt forwarded, got %q", summarizer.prompt)
}
if summarizer.url != uploaded.URL {
t.Fatalf("expected uploaded url forwarded, got %q", summarizer.url)
}
if summary != "summary text" {
t.Fatalf("expected summary text, got %q", summary)
}
if err := svc.CopySummary(ctx, summary); err != nil {
t.Fatalf("copy summary: %v", err)
}
if clip.text != "summary text" {
t.Fatalf("expected summary copied, got %q", clip.text)
}
}
func TestCaptureSummaryServiceInlineDataURL(t *testing.T) {
svc := &CaptureSummaryService{MaxInlineBytes: 10}
if !svc.CanInline([]byte("small")) {
t.Fatalf("expected 5-byte payload to be inlinable under 10-byte cap")
}
if svc.CanInline([]byte("this is definitely too big")) {
t.Fatalf("expected oversized payload to be rejected for inlining")
}
url := svc.InlineDataURL([]byte("png"))
if !strings.HasPrefix(url, "data:image/png;base64,") {
t.Fatalf("expected base64 data URL prefix, got %q", url)
}
if strings.Contains(url, "https://") {
t.Fatalf("inline URL must not be a remote URL, got %q", url)
}
}
func TestCaptureSummaryServiceDeleteUploadedUsesDeleter(t *testing.T) {
provider := &fakeOSSProvider{}
svc := &CaptureSummaryService{Provider: provider}
if err := svc.DeleteUploaded(context.Background(), "snapgo/a.png"); err != nil {
t.Fatalf("delete uploaded: %v", err)
}
if provider.deletedKey != "snapgo/a.png" {
t.Fatalf("expected object deleted, got %q", provider.deletedKey)
}
}
func TestCaptureSummaryServiceDeleteUploadedNoDeleterIsNoop(t *testing.T) {
// A provider without Delete must not error when cleanup is requested.
svc := &CaptureSummaryService{Provider: nonDeletingProvider{}}
if err := svc.DeleteUploaded(context.Background(), "snapgo/a.png"); err != nil {
t.Fatalf("expected no-op delete, got %v", err)
}
}
type nonDeletingProvider struct{}
func (nonDeletingProvider) Upload(_ context.Context, _ string, _ []byte, _ string) (string, error) {
return "https://cdn.example.com/x.png", nil
}
func (nonDeletingProvider) Name() string { return "no-delete" }