// vision_test.go covers openai.Provider.ExtractVision (the shared // flyto.VisionProvider capability over an OpenAI-compatible /v1/chat/ // completions endpoint with an image_url content block). Uses httptest so no // live VLM is touched. Verifies: the happy path returns message.content; the // request actually carries the model + an image_url data URI; the error // envelope, empty content (with reasoning surfaced), missing model, and empty // image all fail loudly. // // vision_test.go 覆盖 openai.Provider.ExtractVision (经 OpenAI 兼容 // /v1/chat/completions + image_url content 块实现的共享 flyto.VisionProvider). // 用 httptest 不碰真 VLM. 验证: happy path 返 message.content; 请求确实带 model // + image_url data URI; error 外壳 / 空 content (露 reasoning) / 缺 model / 空图 // 都 fail-loud. package openai import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/flyto" ) func TestExtractVision_HappyPath(t *testing.T) { var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/v1/chat/completions") { t.Errorf("path = %q, want .../v1/chat/completions", r.URL.Path) } if auth := r.Header.Get("Authorization"); auth != "Bearer sk-test" { t.Errorf("auth = %q, want Bearer sk-test", auth) } b, _ := io.ReadAll(r.Body) _ = json.Unmarshal(b, &gotBody) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"{\"summary\":\"ok\"}","reasoning_content":"thinking..."},"finish_reason":"stop"}]}`)) })) defer srv.Close() p := New(Config{APIKey: "sk-test", BaseURL: srv.URL, HTTPClient: srv.Client()}) resp, err := p.ExtractVision(context.Background(), &flyto.VisionRequest{ Prompt: "extract", Image: []byte{0x89, 0x50, 0x4e, 0x47}, MediaType: "image/png", Model: "gemma4-moe-26b-a4b-q6", }) if err != nil { t.Fatalf("ExtractVision: %v", err) } if resp.Content != `{"summary":"ok"}` { t.Errorf("content = %q, want the JSON answer (not reasoning_content)", resp.Content) } // request must carry the model + an image_url data URI. if gotBody["model"] != "gemma4-moe-26b-a4b-q6" { t.Errorf("request model = %v, want gemma4-moe-26b-a4b-q6", gotBody["model"]) } raw, _ := json.Marshal(gotBody) if !strings.Contains(string(raw), "data:image/png;base64,") || !strings.Contains(string(raw), "image_url") { t.Errorf("request missing image_url data URI: %s", raw) } } func TestExtractVision_ErrorEnvelope(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"error":{"message":"model does not support images","type":"invalid_request_error"}}`)) })) defer srv.Close() p := New(Config{APIKey: "sk-test", BaseURL: srv.URL, HTTPClient: srv.Client()}) _, err := p.ExtractVision(context.Background(), &flyto.VisionRequest{Image: []byte{1}, Model: "m"}) if err == nil || !strings.Contains(err.Error(), "does not support images") { t.Errorf("err = %v, want error envelope surfaced", err) } } func TestExtractVision_EmptyContentSurfacesReasoning(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"","reasoning_content":"I was still thinking when cut off"},"finish_reason":"length"}]}`)) })) defer srv.Close() p := New(Config{APIKey: "sk-test", BaseURL: srv.URL, HTTPClient: srv.Client()}) _, err := p.ExtractVision(context.Background(), &flyto.VisionRequest{Image: []byte{1}, Model: "m"}) if err == nil || !strings.Contains(err.Error(), "empty content") || !strings.Contains(err.Error(), "still thinking") { t.Errorf("err = %v, want empty-content error surfacing the reasoning snippet", err) } } func TestExtractVision_Guards(t *testing.T) { p := New(Config{APIKey: "sk-test", BaseURL: "http://unused"}) if _, err := p.ExtractVision(context.Background(), &flyto.VisionRequest{Image: []byte{1}}); err == nil { t.Error("missing Model should error (openai has no endpoint-locked default)") } if _, err := p.ExtractVision(context.Background(), &flyto.VisionRequest{Model: "m"}); err == nil { t.Error("empty image bytes should error") } }