统一媒体异步提交和轮询响应,扩展 /ai/result 到当前用户的 Gateway 媒体任务,并保持既有 Gateway 字段兼容。\n\n启用转存但无可用渠道时回退到 24 小时本地静态资源;关闭转存时保留图片、音频等上游 Base64 字段。同步更新文件上传兼容结构、OpenAPI、管理端说明和回归测试。\n\n验证:cd apps/api && env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1;gofmt -l 无输出;pnpm nx run web:typecheck。
62 lines
2.2 KiB
Go
62 lines
2.2 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
func TestOptionalTaskIdempotencyKeyRejectsMultipleValues(t *testing.T) {
|
|
t.Parallel()
|
|
request := httptest.NewRequest("POST", "/", nil)
|
|
if _, present, err := optionalTaskIdempotencyKey(request); err != nil || present {
|
|
t.Fatalf("missing key present=%v err=%v", present, err)
|
|
}
|
|
request.Header.Add("Idempotency-Key", "one")
|
|
request.Header.Add("Idempotency-Key", "two")
|
|
if _, _, err := optionalTaskIdempotencyKey(request); err == nil {
|
|
t.Fatal("multiple keys must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestTaskIdempotencyRequestHashIsCanonical(t *testing.T) {
|
|
t.Parallel()
|
|
first := map[string]any{"model": "m", "n": float64(1), "nested": map[string]any{"b": true, "a": "x"}}
|
|
second := map[string]any{"nested": map[string]any{"a": "x", "b": true}, "n": float64(1), "model": "m"}
|
|
if taskIdempotencyRequestHash("images.generations", false, false, first) != taskIdempotencyRequestHash("images.generations", false, false, second) {
|
|
t.Fatal("equivalent JSON objects must have the same request hash")
|
|
}
|
|
if taskIdempotencyRequestHash("images.generations", true, false, first) == taskIdempotencyRequestHash("images.generations", false, false, first) {
|
|
t.Fatal("async response semantics must be part of the request hash")
|
|
}
|
|
}
|
|
|
|
func TestWriteIdempotentAsyncTaskReplayKeepsEasyAICompatibilityFields(t *testing.T) {
|
|
t.Parallel()
|
|
task := store.GatewayTask{
|
|
ID: "async-replay-1",
|
|
Kind: "images.generations",
|
|
Status: "queued",
|
|
AsyncMode: true,
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
|
|
writeIdempotentTaskReplay(recorder, task, true)
|
|
|
|
if recorder.Code != http.StatusAccepted ||
|
|
recorder.Header().Get("Idempotent-Replayed") != "true" ||
|
|
recorder.Header().Get("X-Gateway-Task-Id") != task.ID {
|
|
t.Fatalf("unexpected replay status/headers: code=%d headers=%v", recorder.Code, recorder.Header())
|
|
}
|
|
var got map[string]any
|
|
if err := json.NewDecoder(recorder.Body).Decode(&got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["status"] != "submitted" || got["task_id"] != task.ID || got["taskId"] != task.ID {
|
|
t.Fatalf("unexpected replay body: %+v", got)
|
|
}
|
|
}
|