feat(acceptance): 增加生产同构媒体压力验收模式
引入动态流量门禁、隔离验收身份与协议级 Gemini/Volces 模拟器,覆盖双站点 API、Worker、PostgreSQL、River、账务、回调和媒体物化链路。 新增 P24/P28/P32 容量阶梯、Worker 强杀恢复、真实小流量 canary、CAS 放量和失败保持 validation 的生产编排;Worker 执行槽、连接池、媒体并发和双站点副本数改为环境配置。 验证:Go 全量测试、真实 PostgreSQL 迁移集成测试、迁移安全检查、OpenAPI 生成、ShellCheck、Kustomize、gofmt 和 git diff --check。
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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 := http.Post(httpServer.URL+"/v1beta/models/gemini-test:generateContent", "application/json", bytes.NewReader(geminiBody))
|
||||
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-4k.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("get 4K fixture: %v", err)
|
||||
}
|
||||
config, format, err := image.DecodeConfig(fixtureResponse.Body)
|
||||
_ = fixtureResponse.Body.Close()
|
||||
if err != nil || format != "jpeg" || config.Width != 4096 || config.Height != 2160 {
|
||||
t.Fatalf("4K fixture format=%s config=%+v err=%v", format, config, 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 = http.Post(httpServer.URL+"/contents/generations/tasks", "application/json", bytes.NewReader(videoBody))
|
||||
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 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()
|
||||
|
||||
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"
|
||||
}
|
||||
content = append(content, map[string]any{
|
||||
"type": "image_url", "role": role,
|
||||
"image_url": map[string]any{
|
||||
"url": fmt.Sprintf("%s/fixtures/image-%02d.png", httpServer.URL, index%4),
|
||||
},
|
||||
})
|
||||
}
|
||||
body, _ := json.Marshal(map[string]any{"model": "seedance-test", "content": content, "seed": imageCount})
|
||||
response, err := http.Post(httpServer.URL+"/contents/generations/tasks", "application/json", bytes.NewReader(body))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user