Files
easyai-ai-gateway/apps/api/internal/acceptanceemulator/server_test.go
T
wangbo 29a9b8c89f fix(acceptance): 使用可完整解码的 WebP 样本
替换截断的 WebP 验收夹具,新增全量图片像素解码回归,并保留 6K 外部水化与三参考图归一化测试,避免协议模拟样本缺陷被误判为 Worker 运行故障。\n\n验证:Go 全量测试、go vet、gofmt、git diff --check 通过。
2026-07-31 21:11:50 +08:00

258 lines
8.2 KiB
Go

package acceptanceemulator
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
func TestGeminiAndVolcesProtocolEmulation(t *testing.T) {
var mu sync.Mutex
now := time.Unix(1_800_000_000, 0)
server := New(Config{
Now: func() time.Time {
mu.Lock()
defer mu.Unlock()
return now
},
Wait: func(context.Context, time.Duration) error { return nil },
})
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
input := paddedPNG(256 << 10)
geminiBody, _ := json.Marshal(map[string]any{
"contents": []any{map[string]any{"parts": []any{
map[string]any{"text": "edit image"},
map[string]any{"inlineData": map[string]any{
"mimeType": "image/png", "data": base64.StdEncoding.EncodeToString(input),
}},
}}},
"generationConfig": map[string]any{"responseModalities": []any{"IMAGE"}},
})
response, err := postIdempotent(
httpServer.URL+"/v1beta/models/gemini-test:generateContent",
geminiBody,
"gemini-task",
)
if err != nil {
t.Fatalf("Gemini request: %v", err)
}
if response.StatusCode != http.StatusOK {
payload, _ := io.ReadAll(response.Body)
t.Fatalf("Gemini status=%d body=%s", response.StatusCode, payload)
}
var geminiResult map[string]any
if err := json.NewDecoder(response.Body).Decode(&geminiResult); err != nil {
t.Fatalf("decode Gemini response: %v", err)
}
_ = response.Body.Close()
fixtureResponse, err := http.Get(httpServer.URL + "/fixtures/image-12-oversized.jpg")
if err != nil {
t.Fatalf("get oversized fixture: %v", err)
}
fixturePayload, readErr := io.ReadAll(fixtureResponse.Body)
_ = fixtureResponse.Body.Close()
if readErr != nil {
t.Fatalf("read oversized fixture: %v", readErr)
}
config, format, err := image.DecodeConfig(bytes.NewReader(fixturePayload))
if err != nil || format != "jpeg" || config.Width != 6144 || config.Height != 2160 {
t.Fatalf("oversized fixture format=%s config=%+v err=%v", format, config, err)
}
if _, format, err = image.Decode(bytes.NewReader(fixturePayload)); err != nil || format != "jpeg" {
t.Fatalf("fully decode oversized fixture format=%s err=%v", format, err)
}
content := []any{map[string]any{"type": "text", "text": "video"}}
for index, name := range []string{"image-00.png", "image-04.jpg", "image-08.webp"} {
role := "reference_image"
if index == 0 {
role = "first_frame"
}
content = append(content, map[string]any{
"type": "image_url", "role": role,
"image_url": map[string]any{"url": httpServer.URL + "/fixtures/" + name},
})
}
videoBody, _ := json.Marshal(map[string]any{"model": "seedance-test", "content": content, "seed": 1})
response, err = postIdempotent(
httpServer.URL+"/contents/generations/tasks",
videoBody,
"video-task",
)
if err != nil {
t.Fatalf("submit video: %v", err)
}
if response.StatusCode != http.StatusOK {
t.Fatalf("submit video status=%d", response.StatusCode)
}
var submitted map[string]any
_ = json.NewDecoder(response.Body).Decode(&submitted)
_ = response.Body.Close()
taskID := strings.TrimSpace(stringValue(submitted["id"]))
if taskID == "" {
t.Fatal("video emulator returned no task ID")
}
mu.Lock()
now = now.Add(20 * time.Second)
mu.Unlock()
response, err = http.Get(httpServer.URL + "/contents/generations/tasks/" + taskID)
if err != nil {
t.Fatalf("poll video: %v", err)
}
var polled map[string]any
_ = json.NewDecoder(response.Body).Decode(&polled)
_ = response.Body.Close()
if polled["status"] != "succeeded" {
t.Fatalf("video status=%v", polled["status"])
}
response, err = http.Get(httpServer.URL + "/report")
if err != nil {
t.Fatalf("get report: %v", err)
}
var report Report
_ = json.NewDecoder(response.Body).Decode(&report)
_ = response.Body.Close()
if report.GeminiRequests != 1 || report.VideoSubmissions != 1 || report.VideoInvalid != 0 ||
report.VideoReferenceCounts["3"] != 1 || report.UniqueImageHashes != 3 {
t.Fatalf("unexpected emulator report: %+v", report)
}
}
func TestForcedConversionRejectsImagesOutsideOfficialSeedanceRange(t *testing.T) {
server := New(Config{Wait: func(context.Context, time.Duration) error { return nil }})
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
content := []any{map[string]any{"type": "text", "text": "acceptance-force-conversion"}}
for _, name := range []string{"image-12-oversized.jpg", "image-00.png", "image-04.jpg"} {
content = append(content, map[string]any{
"type": "image_url", "role": "reference_image",
"image_url": map[string]any{"url": httpServer.URL + "/fixtures/" + name},
})
}
body, _ := json.Marshal(map[string]any{"model": "seedance-test", "content": content})
response, err := postIdempotent(httpServer.URL+"/contents/generations/tasks", body, "oversized")
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusBadRequest {
t.Fatalf("status=%d, want %d", response.StatusCode, http.StatusBadRequest)
}
}
func TestImageFixturesFullyDecode(t *testing.T) {
for name, fixture := range buildFixtures() {
if !strings.HasPrefix(fixture.ContentType, "image/") {
continue
}
decoded, format, err := image.Decode(bytes.NewReader(fixture.Payload))
if err != nil {
t.Fatalf("fixture %s does not fully decode: %v", name, err)
}
bounds := decoded.Bounds()
if format == "" || bounds.Dx() <= 0 || bounds.Dy() <= 0 {
t.Fatalf("fixture %s format=%q bounds=%v", name, format, bounds)
}
}
}
func TestPaddedPNGIsValidAndExact(t *testing.T) {
for _, size := range []int{256 << 10, 4 << 20, 8 << 20} {
payload := paddedPNG(size)
if len(payload) != size {
t.Fatalf("PNG bytes=%d, want=%d", len(payload), size)
}
if _, _, err := image.DecodeConfig(bytes.NewReader(payload)); err != nil {
t.Fatalf("decode padded PNG: %v", err)
}
}
}
func TestScaledDelayStaysOnWholeSecondsWithinProfile(t *testing.T) {
delay := scaledDelay(8*time.Second, 15*time.Second, 2<<20)
if delay < 8*time.Second || delay > 15*time.Second || delay%time.Second != 0 {
t.Fatalf("scaled delay=%s", delay)
}
}
func TestVolcesProtocolAcceptsThreeSixAndNineReferenceImages(t *testing.T) {
server := New(Config{Wait: func(context.Context, time.Duration) error { return nil }})
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
dataImage := "data:image/png;base64," + base64.StdEncoding.EncodeToString(paddedPNG(0))
for _, imageCount := range []int{3, 6, 9} {
content := []any{map[string]any{"type": "text", "text": "multi-reference video"}}
for index := 0; index < imageCount; index++ {
role := "reference_image"
if index == 0 {
role = "first_frame"
}
if imageCount > 3 && index == imageCount-1 {
role = "last_frame"
}
imageURL := fmt.Sprintf("%s/fixtures/image-%02d.png", httpServer.URL, index%4)
if index%2 == 0 {
imageURL = dataImage
}
content = append(content, map[string]any{
"type": "image_url", "role": role,
"image_url": map[string]any{"url": imageURL},
})
}
body, _ := json.Marshal(map[string]any{"model": "seedance-test", "content": content, "seed": imageCount})
response, err := postIdempotent(
httpServer.URL+"/contents/generations/tasks",
body,
fmt.Sprintf("video-%d", imageCount),
)
if err != nil {
t.Fatalf("%d-image submit: %v", imageCount, err)
}
_ = response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("%d-image status=%d", imageCount, response.StatusCode)
}
}
response, err := http.Get(httpServer.URL + "/report")
if err != nil {
t.Fatalf("get report: %v", err)
}
defer response.Body.Close()
var report Report
if err := json.NewDecoder(response.Body).Decode(&report); err != nil {
t.Fatalf("decode report: %v", err)
}
for _, imageCount := range []string{"3", "6", "9"} {
if report.VideoReferenceCounts[imageCount] != 1 {
t.Fatalf("reference counts=%v", report.VideoReferenceCounts)
}
}
}
func postIdempotent(url string, body []byte, key string) (*http.Response, error) {
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Idempotency-Key", key)
return http.DefaultClient.Do(request)
}