refactor(runner): 收敛上游失败决策与轮转策略

将同平台重试、跨平台动作、健康副作用和冷却排队统一到单一失败决策,避免旧降级策略与 failover 重复执行。\n\n增加事务级单源保护、候选实时刷新、冷却错误契约、管理接口严格校验及兼容策略只读展示。\n\n验证:真实 Gateway HTTP/PostgreSQL 接受测试 10 项通过;go test ./...、pnpm openapi、pnpm lint、pnpm test、pnpm build 均通过。
This commit is contained in:
2026-07-27 23:50:17 +08:00
parent 046c16fc69
commit 31565af07a
19 changed files with 1988 additions and 307 deletions
@@ -0,0 +1,39 @@
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)
}
}