Remove content-shape and message-type attachment filtering, recover forwarded unknown attachments, and preserve original media across all agent backends.
187 lines
6.6 KiB
Go
187 lines
6.6 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGeminiChannelUsesAPIWithoutLocalCLI(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
t.Setenv("PATH", t.TempDir())
|
|
t.Setenv("GEMINI_API_KEY", "test-key")
|
|
t.Setenv("GEMINI_API_BASE_URL", "https://gemini-proxy.example/v1beta")
|
|
|
|
fwd, err := forwarderForChannel("gemini", "", connectAgentOptions{Model: "gemini-test"})
|
|
if err != nil {
|
|
t.Fatalf("forwarderForChannel(gemini): %v", err)
|
|
}
|
|
gf, ok := fwd.(*geminiAPIForwarder)
|
|
if !ok {
|
|
t.Fatalf("gemini forwarder = %T, want *geminiAPIForwarder", fwd)
|
|
}
|
|
if gf.model != "gemini-test" {
|
|
t.Fatalf("model = %q, want gemini-test", gf.model)
|
|
}
|
|
if gf.baseURL != "https://gemini-proxy.example/v1beta" {
|
|
t.Fatalf("baseURL = %q", gf.baseURL)
|
|
}
|
|
}
|
|
|
|
func TestGeminiAPIForwarderSendsSmallAttachmentInline(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
path := filepath.Join(t.TempDir(), "voice.mp3")
|
|
wantBytes := []byte("real-audio-bytes")
|
|
if err := os.WriteFile(path, wantBytes, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got geminiPart
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req geminiGenerateContentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode request: %v", err)
|
|
}
|
|
if len(req.Contents) == 1 && len(req.Contents[0].Parts) == 2 {
|
|
got = req.Contents[0].Parts[1]
|
|
}
|
|
_, _ = w.Write([]byte(`{"candidates":[{"content":{"parts":[{"text":"ok"}]}}]}`))
|
|
}))
|
|
defer ts.Close()
|
|
f := &geminiAPIForwarder{model: "gemini-test", apiKey: "key", baseURL: ts.URL, httpClient: ts.Client()}
|
|
_, err := f.forwardWithAttachments(context.Background(), "conv", "转写", []connectMediaAttachment{{LocalPath: path, FileName: "voice.mp3", MediaType: "audio"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.InlineData == nil || got.InlineData.MIMEType != "audio/mpeg" {
|
|
t.Fatalf("inlineData = %#v", got.InlineData)
|
|
}
|
|
decoded, err := base64.StdEncoding.DecodeString(got.InlineData.Data)
|
|
if err != nil || string(decoded) != string(wantBytes) {
|
|
t.Fatalf("inline bytes = %q, err=%v", decoded, err)
|
|
}
|
|
}
|
|
|
|
func TestGeminiAPIForwarderUploadsLargeAttachment(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
path := filepath.Join(t.TempDir(), "video.mp4")
|
|
if err := os.WriteFile(path, make([]byte, geminiInlineRawLimit+1), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var started, uploaded, generated bool
|
|
var ts *httptest.Server
|
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/upload/v1beta/files":
|
|
started = r.Header.Get("X-Goog-Upload-Command") == "start"
|
|
w.Header().Set("X-Goog-Upload-URL", ts.URL+"/upload-session")
|
|
case "/upload-session":
|
|
uploaded = r.Header.Get("X-Goog-Upload-Command") == "upload, finalize"
|
|
_, _ = w.Write([]byte(`{"file":{"name":"files/1","uri":"https://files.example/1","mimeType":"video/mp4","state":"ACTIVE"}}`))
|
|
case "/v1beta/models/gemini-test:generateContent":
|
|
var req geminiGenerateContentRequest
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
generated = len(req.Contents) == 1 && len(req.Contents[0].Parts) == 2 && req.Contents[0].Parts[1].FileData != nil && req.Contents[0].Parts[1].FileData.FileURI == "https://files.example/1"
|
|
_, _ = w.Write([]byte(`{"candidates":[{"content":{"parts":[{"text":"ok"}]}}]}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
f := &geminiAPIForwarder{model: "gemini-test", apiKey: "key", baseURL: ts.URL + "/v1beta", httpClient: ts.Client()}
|
|
_, err := f.forwardWithAttachments(context.Background(), "conv", "分析", []connectMediaAttachment{{LocalPath: path, FileName: "video.mp4", MediaType: "video"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !started || !uploaded || !generated {
|
|
t.Fatalf("started=%v uploaded=%v generated=%v", started, uploaded, generated)
|
|
}
|
|
}
|
|
|
|
func TestGeminiAPIForwarderForward(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
var gotPath, gotKey, gotText string
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotKey = r.Header.Get("x-goog-api-key")
|
|
var req geminiGenerateContentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode request: %v", err)
|
|
}
|
|
if len(req.Contents) > 0 && len(req.Contents[0].Parts) > 0 {
|
|
gotText = req.Contents[0].Parts[0].Text
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"candidates":[{"content":{"parts":[{"text":"gemini-api-ok"}]}}]}`))
|
|
}))
|
|
defer ts.Close()
|
|
t.Setenv("GEMINI_API_KEY", "test-key")
|
|
t.Setenv("GEMINI_API_BASE_URL", ts.URL)
|
|
|
|
fwd, err := forwarderForChannel("gemini", "", connectAgentOptions{Model: "models/gemini-test"})
|
|
if err != nil {
|
|
t.Fatalf("forwarderForChannel(gemini): %v", err)
|
|
}
|
|
reply, err := fwd.forward(context.Background(), "conv-1", "你好")
|
|
if err != nil {
|
|
t.Fatalf("forward: %v", err)
|
|
}
|
|
if reply != "gemini-api-ok" {
|
|
t.Fatalf("reply = %q, want gemini-api-ok", reply)
|
|
}
|
|
if gotPath != "/models/gemini-test:generateContent" {
|
|
t.Fatalf("path = %q", gotPath)
|
|
}
|
|
if gotKey != "test-key" {
|
|
t.Fatalf("x-goog-api-key = %q", gotKey)
|
|
}
|
|
if gotText != "你好" {
|
|
t.Fatalf("prompt text = %q", gotText)
|
|
}
|
|
}
|
|
|
|
func TestGeminiChannelRequiresAPIKey(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
t.Setenv("GEMINI_API_KEY", "")
|
|
t.Setenv("GOOGLE_API_KEY", "")
|
|
|
|
_, err := forwarderForChannel("gemini", "", connectAgentOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "GEMINI_API_KEY") {
|
|
t.Fatalf("err = %v, want GEMINI_API_KEY validation", err)
|
|
}
|
|
}
|
|
|
|
func TestGeminiCLIStatusUsesAPIKey(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
t.Setenv("GEMINI_API_KEY", "")
|
|
t.Setenv("GOOGLE_API_KEY", "")
|
|
if got := connectCliStatus("gemini")["installed"]; got != false {
|
|
t.Fatalf("installed without key = %v, want false", got)
|
|
}
|
|
t.Setenv("GOOGLE_API_KEY", "google-key")
|
|
if got := connectCliStatus("gemini")["installed"]; got != true {
|
|
t.Fatalf("installed with key = %v, want true", got)
|
|
}
|
|
if got := connectCliStatus("gemini")["autoInstall"]; got != false {
|
|
t.Fatalf("autoInstall = %v, want false", got)
|
|
}
|
|
}
|