fix(acceptance): 物化验收任务的最终媒体
仅对已认证 Acceptance Run 强制下载并持久化 URL 型结果,确保集群外压测端能验证最终视频;普通生产任务继续遵循现有文件存储策略。压测器仅对 Gateway 自有地址做双入口重写,避免向第三方媒体域名泄露验收凭据。\n\n同时修复本地集群重复使用同一快照文件时的幂等复制失败。验证:Go 全量测试、go vet、gofmt、bash -n、ShellCheck 和 git diff --check 通过。
This commit is contained in:
@@ -760,7 +760,7 @@ func pollVideoTask(ctx context.Context, client *http.Client, opts options, taskI
|
||||
if mediaURL == "" {
|
||||
return fmt.Errorf("video task %s succeeded without a media URL", taskID)
|
||||
}
|
||||
return validateVideoAsset(ctx, client, mediaURL)
|
||||
return validateVideoAsset(ctx, client, opts, mediaURL, index)
|
||||
case "failed", "cancelled", "canceled":
|
||||
return fmt.Errorf("video task %s finished with status %s", taskID, status)
|
||||
}
|
||||
@@ -780,15 +780,34 @@ func pollVideoTask(ctx context.Context, client *http.Client, opts options, taskI
|
||||
func findMediaURL(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range typed {
|
||||
preferredKeys := []string{"video_url", "url", "data", "content", "output", "result", "upload"}
|
||||
visited := make(map[string]struct{}, len(typed))
|
||||
for _, key := range preferredKeys {
|
||||
item, ok := typed[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
visited[key] = struct{}{}
|
||||
normalized := strings.ToLower(strings.TrimSpace(key))
|
||||
if (normalized == "url" || normalized == "video_url") && strings.HasPrefix(strings.TrimSpace(fmt.Sprint(item)), "http") {
|
||||
if (normalized == "url" || normalized == "video_url") && acceptanceMediaURL(strings.TrimSpace(fmt.Sprint(item))) {
|
||||
return strings.TrimSpace(fmt.Sprint(item))
|
||||
}
|
||||
if mediaURL := findMediaURL(item); mediaURL != "" {
|
||||
return mediaURL
|
||||
}
|
||||
}
|
||||
keys := make([]string, 0, len(typed)-len(visited))
|
||||
for key := range typed {
|
||||
if _, ok := visited[key]; !ok {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
if mediaURL := findMediaURL(typed[key]); mediaURL != "" {
|
||||
return mediaURL
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
if mediaURL := findMediaURL(item); mediaURL != "" {
|
||||
@@ -799,11 +818,22 @@ func findMediaURL(value any) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateVideoAsset(ctx context.Context, client *http.Client, mediaURL string) error {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, mediaURL, nil)
|
||||
func acceptanceMediaURL(value string) bool {
|
||||
return strings.HasPrefix(value, "/") || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://")
|
||||
}
|
||||
|
||||
func validateVideoAsset(ctx context.Context, client *http.Client, opts options, mediaURL string, index int) error {
|
||||
requestURL, gatewayRequest, err := acceptanceMediaRequestURL(opts, mediaURL, index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve final video: %w", err)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if gatewayRequest {
|
||||
opts.setHeaders(request, index, false)
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download final video: %w", err)
|
||||
@@ -824,6 +854,46 @@ func validateVideoAsset(ctx context.Context, client *http.Client, mediaURL strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func acceptanceMediaRequestURL(opts options, mediaURL string, index int) (string, bool, error) {
|
||||
mediaURL = strings.TrimSpace(mediaURL)
|
||||
if mediaURL == "" {
|
||||
return "", false, errors.New("empty media URL")
|
||||
}
|
||||
if strings.HasPrefix(mediaURL, "/") {
|
||||
if len(opts.gateways) == 0 {
|
||||
return "", false, errors.New("no gateway URL configured")
|
||||
}
|
||||
base, err := url.Parse(opts.gateways[index%len(opts.gateways)])
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
reference, err := url.Parse(mediaURL)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return base.ResolveReference(reference).String(), true, nil
|
||||
}
|
||||
parsed, err := url.Parse(mediaURL)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if opts.gatewayTLSName == "" || !strings.EqualFold(parsed.Hostname(), opts.gatewayTLSName) {
|
||||
return parsed.String(), false, nil
|
||||
}
|
||||
if len(opts.gateways) == 0 {
|
||||
return "", false, errors.New("no gateway URL configured")
|
||||
}
|
||||
base, err := url.Parse(opts.gateways[index%len(opts.gateways)])
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
base.Path = parsed.Path
|
||||
base.RawPath = parsed.RawPath
|
||||
base.RawQuery = parsed.RawQuery
|
||||
base.Fragment = parsed.Fragment
|
||||
return base.String(), true, nil
|
||||
}
|
||||
|
||||
func (o options) setHeaders(request *http.Request, requestIndex int, realUpstream bool) {
|
||||
if o.gatewayTLSName != "" {
|
||||
request.Host = o.gatewayTLSName
|
||||
|
||||
@@ -158,12 +158,15 @@ func TestGeminiLoadIsSplitAcrossTwoGatewayAPIs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateVideoAssetDownloadsFinalMedia(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
if request.Header.Get("Authorization") != "" {
|
||||
t.Fatal("acceptance credentials leaked to external media host")
|
||||
}
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
_, _ = w.Write([]byte{0, 0, 0, 16, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'})
|
||||
}))
|
||||
defer server.Close()
|
||||
if err := validateVideoAsset(t.Context(), server.Client(), server.URL+"/result.mp4"); err != nil {
|
||||
if err := validateVideoAsset(t.Context(), server.Client(), options{}, server.URL+"/result.mp4", 0); err != nil {
|
||||
t.Fatalf("validate video asset: %v", err)
|
||||
}
|
||||
if got := findMediaURL(map[string]any{"content": map[string]any{"video_url": server.URL}}); got != server.URL {
|
||||
@@ -171,6 +174,34 @@ func TestValidateVideoAssetDownloadsFinalMedia(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateVideoAssetUsesGatewayForMaterializedPath(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
if request.Host != "gateway.easyai.local" || request.Header.Get("Authorization") != "Bearer key-1" {
|
||||
t.Fatalf("host=%q authorization=%q", request.Host, request.Header.Get("Authorization"))
|
||||
}
|
||||
if request.URL.Path != "/static/generated/result.mp4" {
|
||||
t.Fatalf("path=%q", request.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
_, _ = w.Write([]byte{0, 0, 0, 16, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'})
|
||||
}))
|
||||
defer server.Close()
|
||||
opts := options{
|
||||
gateways: []string{server.URL}, apiKeys: []string{"key-1"},
|
||||
runID: "run-1", runToken: "token-1", gatewayTLSName: "gateway.easyai.local",
|
||||
}
|
||||
if err := validateVideoAsset(t.Context(), server.Client(), opts, "/static/generated/result.mp4", 0); err != nil {
|
||||
t.Fatalf("validate materialized video: %v", err)
|
||||
}
|
||||
got := findMediaURL(map[string]any{
|
||||
"raw": map[string]any{"video_url": "http://internal.invalid/video.mp4"},
|
||||
"data": []any{map[string]any{"video_url": "/static/generated/result.mp4"}},
|
||||
})
|
||||
if got != "/static/generated/result.mp4" {
|
||||
t.Fatalf("preferred media URL=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptanceReportErrorRedactsSecretsAndURLs(t *testing.T) {
|
||||
got := redactError(
|
||||
`token-1 failed at https://example.invalid/video.mp4?token=signed`,
|
||||
|
||||
Reference in New Issue
Block a user