package runner import ( "bytes" "context" "encoding/base64" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/easyai/easyai-ai-gateway/apps/api/internal/clients" "github.com/easyai/easyai-ai-gateway/apps/api/internal/config" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) func TestGeneratedAssetDecisionSkipsURLResultAndStripsInlinePayload(t *testing.T) { item := map[string]any{ "b64_json": base64.StdEncoding.EncodeToString([]byte("inline image")), "url": "https://cdn.example.com/generated.png", } decision, err := generatedAssetDecisionForItem("images.generations", item, defaultGeneratedAssetUploadPolicy()) if err != nil { t.Fatalf("unexpected error: %v", err) } if decision.Inline != nil { t.Fatalf("URL media should not be uploaded by the default policy") } if !containsString(decision.StripKeys, "b64_json") { t.Fatalf("inline payload should be stripped when URL is already available: %+v", decision.StripKeys) } } func TestGeneratedAssetDecisionUploadsInlineImageBase64(t *testing.T) { item := map[string]any{ "b64_json": base64.StdEncoding.EncodeToString([]byte("inline image")), "mime_type": "image/jpeg", } decision, err := generatedAssetDecisionForItem("images.generations", item, defaultGeneratedAssetUploadPolicy()) if err != nil { t.Fatalf("unexpected error: %v", err) } if decision.Inline == nil { t.Fatalf("expected inline image to be uploaded") } if decision.Inline.Kind != "image" || decision.Inline.ContentType != "image/jpeg" { t.Fatalf("unexpected inline image metadata: %+v", decision.Inline) } if !containsString(decision.StripKeys, "b64_json") { t.Fatalf("uploaded inline payload should be stripped: %+v", decision.StripKeys) } } func TestMediaResultMaterializationConcurrencyIsBounded(t *testing.T) { service := New(config.Config{MediaMaterializationConcurrency: 1}, nil, nil) result := map[string]any{ "data": []any{map[string]any{ "b64_json": base64.StdEncoding.EncodeToString([]byte("inline image")), "mime_type": "image/png", }}, } release, err := service.acquireMediaResultSlot(context.Background(), result) if err != nil { t.Fatalf("acquire first media result slot: %v", err) } waitCtx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond) defer cancel() if _, err := service.acquireMediaResultSlot(waitCtx, result); err == nil { t.Fatal("second media result materialization bypassed configured concurrency") } release() release, err = service.acquireMediaResultSlot(context.Background(), result) if err != nil { t.Fatalf("acquire released media result slot: %v", err) } release() } func TestMediaTaskPreacquiresMaterializationSlotForInlineResultTypes(t *testing.T) { t.Parallel() for _, modelType := range []string{ "image_generate", "image_edit", "audio_generate", "text_to_speech", "voice_clone", "omni", } { if !mediaTaskNeedsPreUpstreamMaterializationSlot(modelType) { t.Fatalf("%s should preacquire the media materialization slot", modelType) } } for _, modelType := range []string{ "text_generate", "text_embedding", "text_rerank", "video_generate", "image_to_video", } { if mediaTaskNeedsPreUpstreamMaterializationSlot(modelType) { t.Fatalf("%s should not hold a media materialization slot across the upstream call", modelType) } } } func TestGeneratedAssetDecisionUploadsVectorDocumentWithoutChangingType(t *testing.T) { item := map[string]any{ "type": "file", "b64_json": base64.StdEncoding.EncodeToString([]byte("%PDF-1.7\n")), "mime_type": "application/pdf", } decision, err := generatedAssetDecisionForItem("images.vectorize", item, defaultGeneratedAssetUploadPolicy()) if err != nil { t.Fatal(err) } if decision.Inline == nil || decision.Inline.Kind != "file" || decision.Inline.ContentType != "application/pdf" { t.Fatalf("unexpected vector document decision: %+v", decision) } contentType := resolvedGeneratedAssetContentType(decision.Inline.ContentType, decision.Inline.Kind, decision.Inline.Bytes) if contentType != "application/pdf" || fileExtensionForContentType(contentType, "file") != ".pdf" { t.Fatalf("vector document type changed: contentType=%s", contentType) } } func TestGeneratedAssetDecisionUploadsInlineVideoBuffer(t *testing.T) { item := map[string]any{ "type": "video", "video_buffer": []any{float64(0), float64(1), float64(2), float64(3)}, } decision, err := generatedAssetDecisionForItem("videos.generations", item, defaultGeneratedAssetUploadPolicy()) if err != nil { t.Fatalf("unexpected error: %v", err) } if decision.Inline == nil { t.Fatalf("expected inline video buffer to be uploaded") } if decision.Inline.Kind != "video" || decision.Inline.ContentType != "video/mp4" { t.Fatalf("unexpected inline video metadata: %+v", decision.Inline) } if !containsString(decision.StripKeys, "video_buffer") { t.Fatalf("uploaded video buffer should be stripped: %+v", decision.StripKeys) } } func TestGeneratedAssetDecisionUploadsDataURL(t *testing.T) { item := map[string]any{ "url": "data:image/webp;base64," + base64.StdEncoding.EncodeToString([]byte("inline webp")), } decision, err := generatedAssetDecisionForItem("images.generations", item, defaultGeneratedAssetUploadPolicy()) if err != nil { t.Fatalf("unexpected error: %v", err) } if decision.Inline == nil { t.Fatalf("expected data URL to be uploaded") } if decision.Inline.SourceKey != "url" || decision.Inline.ContentType != "image/webp" { t.Fatalf("unexpected data URL metadata: %+v", decision.Inline) } if !containsString(decision.StripKeys, "url") { t.Fatalf("uploaded data URL field should be stripped: %+v", decision.StripKeys) } } func TestGeneratedAssetDecisionUploadsAudioContentDataURL(t *testing.T) { item := map[string]any{ "type": "audio", "content": "data:audio/mpeg;base64," + base64.StdEncoding.EncodeToString([]byte("inline audio")), "mime_type": "audio/mpeg", } decision, err := generatedAssetDecisionForItem("speech.generations", item, defaultGeneratedAssetUploadPolicy()) if err != nil { t.Fatalf("unexpected error: %v", err) } if decision.Inline == nil { t.Fatalf("expected inline audio content to be uploaded") } if decision.Inline.Kind != "audio" || decision.Inline.ContentType != "audio/mpeg" || decision.Inline.SourceKey != "content" { t.Fatalf("unexpected inline audio metadata: %+v", decision.Inline) } if !containsString(decision.StripKeys, "content") { t.Fatalf("uploaded audio content should be stripped: %+v", decision.StripKeys) } } func TestGeneratedAssetDecisionDoesNotUploadPlainTextContent(t *testing.T) { item := map[string]any{ "type": "text", "content": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("plain text ", 20))), } decision, err := generatedAssetDecisionForItem("speech.generations", item, defaultGeneratedAssetUploadPolicy()) if err != nil { t.Fatalf("unexpected error: %v", err) } if decision.Inline != nil || len(decision.StripKeys) > 0 { t.Fatalf("plain text content should not be treated as generated media: %+v", decision) } } func TestGeneratedAssetDecisionUploadsURLWhenPolicyUploadAll(t *testing.T) { item := map[string]any{ "type": "video", "video_url": "https://cdn.example.com/generated.mp4", } decision, err := generatedAssetDecisionForItem("videos.generations", item, generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: true}) if err != nil { t.Fatalf("unexpected error: %v", err) } if decision.URL == nil { t.Fatalf("expected URL media to be uploaded") } if decision.URL.Kind != "video" || decision.URL.SourceKey != "video_url" { t.Fatalf("unexpected URL media metadata: %+v", decision.URL) } if !containsString(decision.StripKeys, "video_url") { t.Fatalf("uploaded URL field should be stripped: %+v", decision.StripKeys) } } func TestGeneratedAssetUploadPolicyFromName(t *testing.T) { tests := []struct { name string policyName string want generatedAssetUploadPolicy }{ { name: "default", policyName: store.FileStorageResultUploadPolicyDefault, want: generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: false, PreserveInlineMedia: false}, }, { name: "upload all", policyName: store.FileStorageResultUploadPolicyUploadAll, want: generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: true, PreserveInlineMedia: false}, }, { name: "legacy upload none becomes default", policyName: store.FileStorageResultUploadPolicyUploadNone, want: generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: false, PreserveInlineMedia: false}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := generatedAssetUploadPolicyFromName(tt.policyName) if got != tt.want { t.Fatalf("unexpected policy: got %+v, want %+v", got, tt.want) } }) } } func TestAcceptanceRunForcesURLMaterializationOnlyForAcceptanceTask(t *testing.T) { configured := generatedAssetUploadPolicy{UploadInlineMedia: true} if got := generatedAssetUploadPolicyForAcceptanceRun(configured, ""); got.UploadURLMedia { t.Fatal("ordinary task unexpectedly enabled URL materialization") } got := generatedAssetUploadPolicyForAcceptanceRun(configured, "acceptance-run-id") if !got.UploadInlineMedia || !got.UploadURLMedia { t.Fatalf("acceptance policy=%+v", got) } } func TestAcceptanceGeneratedMediaAllowsOnlyExactEmulatorOrigin(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 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() service := &Service{} asset := &generatedURLAsset{URL: server.URL + "/media/result.mp4", Kind: "video"} payload, contentType, err := service.readGeneratedURLAsset(t.Context(), asset, server.URL+"/v1") if err != nil { t.Fatalf("read exact acceptance origin: %v", err) } if len(payload) != 12 || contentType != "video/mp4" { t.Fatalf("payload=%d contentType=%q", len(payload), contentType) } for _, allowed := range []string{ strings.Replace(server.URL, "http://", "https://", 1), server.URL + "1", strings.Replace(server.URL, "http://", "http://user@", 1), } { if sameHTTPOrigin(asset.URL, allowed) { t.Fatalf("unexpected origin match: %q", allowed) } } } func TestFinalizeGeneratedAssetsUploadsNestedInlineBinaryUnderDefaultPolicy(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() service := &Service{} channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-result")} payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 160)...) encoded := base64.StdEncoding.EncodeToString(payload) result := map[string]any{ "id": "image-edit", "provider_result": map[string]any{ "binary_data_base64": encoded, "mime_type": "image/png", }, } finalized, err := service.finalizeGeneratedAssets( context.Background(), "task-nested-binary", "images.edits", result, defaultGeneratedAssetUploadPolicy(), channels, true, 0, ) if err != nil { t.Fatalf("finalize generated assets: %v", err) } if TaskResultHasInlineBinary(finalized) { t.Fatal("finalized result still contains inline binary") } nested := finalized["provider_result"].(map[string]any) reference, ok := nested["binary_data_base64"].(map[string]any) if !ok { t.Fatalf("nested binary should be replaced by an asset reference: %+v", nested) } if urlValue := stringFromAny(reference["url"]); !strings.HasPrefix(urlValue, "https://cdn.example.com/media/image_result/") { t.Fatalf("unexpected object storage URL: %s", urlValue) } } func TestResolvedGeneratedAssetContentTypePrefersDetectedMedia(t *testing.T) { pngPayload := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0} contentType := resolvedGeneratedAssetContentType("image/jpeg", "image", pngPayload) if contentType != "image/png" { t.Fatalf("expected detected PNG content type, got %s", contentType) } if extension := fileExtensionForContentType(contentType, "image"); extension != ".png" { t.Fatalf("expected PNG extension, got %s", extension) } } func TestResolvedGeneratedAssetContentTypeKeepsDeclaredMediaWhenDetectionIsGeneric(t *testing.T) { contentType := resolvedGeneratedAssetContentType("image/webp", "image", []byte("not enough media bytes")) if contentType != "image/webp" { t.Fatalf("expected declared webp content type, got %s", contentType) } } func TestGeneratedAssetFileNameIsUniqueAndTyped(t *testing.T) { first := generatedAssetFileName("663e19cd4fa9d8078385c7c9", 0, "image/png", "image") second := generatedAssetFileName("663e19cd4fa9d8078385c7c9", 0, "image/png", "image") if first == second { t.Fatalf("expected generated file names to be unique, both were %s", first) } if !strings.HasPrefix(first, "gateway-result-663e19cd4fa9d8078385c7c9-01-") || !strings.HasSuffix(first, ".png") { t.Fatalf("unexpected generated file name: %s", first) } } func TestUploadGeneratedAssetFailsWithoutObjectStorage(t *testing.T) { service := &Service{} payload := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0} asset := &generatedInlineAsset{ Bytes: payload, ContentType: "image/jpeg", Kind: "image", SourceKey: "b64_json", } _, _, _, _, err := service.uploadGeneratedAsset(context.Background(), "task-123", asset, 0, nil) if clients.ErrorCode(err) != "storage_write_failed" { t.Fatalf("unexpected error: %v", err) } } func TestUploadGeneratedAudioFailsWithoutObjectStorage(t *testing.T) { service := &Service{} asset := &generatedInlineAsset{ Bytes: []byte("inline audio payload"), ContentType: "audio/mpeg", Kind: "audio", SourceKey: "content", } _, _, _, _, err := service.uploadGeneratedAsset(context.Background(), "task-tts", asset, 0, nil) if clients.ErrorCode(err) != "storage_write_failed" { t.Fatalf("unexpected error: %v", err) } } func TestUploadGeneratedRawMediaValueReplacesGeminiInlineDataWithAssetRef(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() service := &Service{} channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-result")} payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 160)...) raw := map[string]any{ "candidates": []any{ map[string]any{ "content": map[string]any{ "parts": []any{ map[string]any{ "inlineData": map[string]any{ "mimeType": "image/png", "data": base64.StdEncoding.EncodeToString(payload), }, }, }, }, }, }, } index := 0 uploaded, changed, err := service.uploadGeneratedRawMediaValue(context.Background(), "task-raw", "chat.completions", raw, "", nil, defaultGeneratedAssetUploadPolicy(), channels, &index) if err != nil { t.Fatalf("upload raw media: %v", err) } if !changed { t.Fatal("expected raw inlineData to be replaced") } uploadedRaw := uploaded.(map[string]any) candidates := uploadedRaw["candidates"].([]any) candidate := candidates[0].(map[string]any) content := candidate["content"].(map[string]any) parts := content["parts"].([]any) part := parts[0].(map[string]any) inlineData := part["inlineData"].(map[string]any) data, ok := inlineData["data"].(map[string]any) if !ok { t.Fatalf("inlineData.data should be an asset reference, got %+v", inlineData["data"]) } ref, _ := data["assetRef"].(map[string]any) if ref["sha256"] == "" || ref["contentType"] != "image/png" || ref["size"] != len(payload) { t.Fatalf("unexpected asset ref: %+v", ref) } if urlValue := stringFromAny(data["url"]); !strings.HasPrefix(urlValue, "https://cdn.example.com/media/image_result/") || !strings.HasSuffix(urlValue, ".png") { t.Fatalf("unexpected raw media URL: %s", urlValue) } if inlineData["data"] == base64.StdEncoding.EncodeToString(payload) { t.Fatal("raw inlineData still contains base64 payload") } } func TestUploadGeneratedRawMediaValueReplacesBufferAndBytesWithAssetRefs(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() service := &Service{} channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-buffer-result")} raw := map[string]any{ "buffer": map[string]any{ "type": "Buffer", "mimeType": "image/png", "data": []any{float64(0x89), float64('P'), float64('N'), float64('G')}, }, "audio_bytes": []any{float64('I'), float64('D'), float64('3')}, "direct": []byte("direct bytes"), } index := 0 uploaded, changed, err := service.uploadGeneratedRawMediaValue(t.Context(), "task-buffer", "images.generations", raw, "", nil, defaultGeneratedAssetUploadPolicy(), channels, &index) if err != nil { t.Fatal(err) } if !changed || index != 3 { t.Fatalf("changed=%v uploads=%d", changed, index) } next := uploaded.(map[string]any) for _, key := range []string{"buffer", "audio_bytes", "direct"} { item, ok := next[key].(map[string]any) if !ok || item["assetRef"] == nil || item["upload"] == nil { t.Fatalf("%s was not objectified: %#v", key, next[key]) } } if TaskResultHasInlineBinary(next) { t.Fatalf("objectified result still contains inline binary: %#v", next) } } func TestUploadFileFailsWithoutObjectStorageAndCreatesNoLocalFile(t *testing.T) { storageDir := t.TempDir() service := &Service{cfg: config.Config{ LocalUploadedStorageDir: storageDir, ServerMainBaseURL: "http://127.0.0.1:1", ServerMainInternalToken: "change-me", }} payload := []byte("%PDF-1.4") _, err := service.UploadFile(context.Background(), FileUploadPayload{ Bytes: payload, ContentType: "application/pdf", FileName: "用户文件.png", Source: "playground", }) if clients.ErrorCode(err) != "storage_write_failed" { t.Fatalf("unexpected error: %v", err) } entries, err := os.ReadDir(storageDir) if err != nil { t.Fatalf("failed to read uploaded static dir: %v", err) } if len(entries) != 0 { t.Fatalf("unexpected local files: %+v", entries) } } func TestRedactGeneratedResultRawDataAudioPayload(t *testing.T) { audioHex := strings.Repeat("49443304", 40) result := map[string]any{ "raw_data": map[string]any{ "base_resp": map[string]any{"status_code": float64(0)}, "data": map[string]any{"audio": audioHex}, }, } if !redactGeneratedResultRawData(result) { t.Fatalf("expected raw audio payload to be redacted") } rawData, _ := result["raw_data"].(map[string]any) data, _ := rawData["data"].(map[string]any) audio, _ := data["audio"].(map[string]any) if audio["redacted"] != true || audio["encoding"] != "hex" || audio["contentType"] != "audio/mpeg" { t.Fatalf("unexpected redacted audio payload: %+v", audio) } if _, ok := audio["sha256"].(string); !ok { t.Fatalf("expected redacted audio payload to include sha256: %+v", audio) } }