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,57 @@
package httpapi
import (
"strings"
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestValidateRunnerPolicyInputActionRules(t *testing.T) {
validRule := func() map[string]any {
return map[string]any{
"categories": []any{"rate_limit"},
"action": "cooldown_and_next",
"target": "model",
"cooldownSeconds": float64(60),
}
}
tests := []struct {
name string
mutate func(map[string]any)
wantErr string
}{
{name: "valid"},
{name: "invalid action", mutate: func(rule map[string]any) { rule["action"] = "rotate" }, wantErr: ".action is invalid"},
{name: "invalid target", mutate: func(rule map[string]any) { rule["target"] = "client" }, wantErr: ".target is invalid"},
{name: "empty match", mutate: func(rule map[string]any) { rule["categories"] = []any{} }, wantErr: "at least one match condition"},
{name: "missing cooldown", mutate: func(rule map[string]any) { delete(rule, "cooldownSeconds") }, wantErr: ".cooldownSeconds must be a positive integer"},
{name: "zero cooldown", mutate: func(rule map[string]any) { rule["cooldownSeconds"] = float64(0) }, wantErr: ".cooldownSeconds must be a positive integer"},
{name: "fractional cooldown", mutate: func(rule map[string]any) { rule["cooldownSeconds"] = 1.5 }, wantErr: ".cooldownSeconds must be a positive integer"},
{name: "missing demote steps", mutate: func(rule map[string]any) {
rule["action"] = "demote_and_next"
delete(rule, "cooldownSeconds")
}, wantErr: ".demoteSteps must be a positive integer"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rule := validRule()
if test.mutate != nil {
test.mutate(rule)
}
err := validateRunnerPolicyInput(store.RunnerPolicyInput{
FailoverPolicy: map[string]any{"actionRules": []any{rule}},
})
if test.wantErr == "" {
if err != nil {
t.Fatalf("unexpected validation error: %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("validation error = %v, want substring %q", err, test.wantErr)
}
})
}
}