将同平台重试、跨平台动作、健康副作用和冷却排队统一到单一失败决策,避免旧降级策略与 failover 重复执行。\n\n增加事务级单源保护、候选实时刷新、冷却错误契约、管理接口严格校验及兼容策略只读展示。\n\n验证:真实 Gateway HTTP/PostgreSQL 接受测试 10 项通过;go test ./...、pnpm openapi、pnpm lint、pnpm test、pnpm build 均通过。
40 lines
1.2 KiB
Go
40 lines
1.2 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
func TestModelCooldownErrorPublicContract(t *testing.T) {
|
|
recoveryAt := time.Now().UTC().Add(17 * time.Second).Truncate(time.Second)
|
|
err := &store.ModelCandidateUnavailableError{
|
|
Code: "model_cooling_down",
|
|
Message: "请求的模型暂时不可用,请稍后重试",
|
|
RetryAfter: 17 * time.Second,
|
|
RecoveryAt: recoveryAt,
|
|
Details: map[string]any{
|
|
"retryAfterSeconds": 17,
|
|
"recoveryAt": recoveryAt.Format(time.RFC3339),
|
|
},
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
applyRunErrorHeaders(recorder, err)
|
|
|
|
if got := statusFromRunError(err); got != 429 {
|
|
t.Fatalf("status = %d, want 429", got)
|
|
}
|
|
if got := runErrorCode(err); got != "model_cooling_down" {
|
|
t.Fatalf("code = %q, want model_cooling_down", got)
|
|
}
|
|
if got := recorder.Header().Get("Retry-After"); got != "17" {
|
|
t.Fatalf("Retry-After = %q, want 17", got)
|
|
}
|
|
details := runErrorDetails(err)
|
|
if len(details) != 2 || details["retryAfterSeconds"] != 17 || details["recoveryAt"] != recoveryAt.Format(time.RFC3339) {
|
|
t.Fatalf("unexpected public details: %#v", details)
|
|
}
|
|
}
|