Files
sub2api-cn-relay-manager/internal/access/openai_responses_repair_test.go

252 lines
8.4 KiB
Go

package access
import (
"context"
"errors"
"testing"
"sub2api-cn-relay-manager/internal/host/sub2api"
)
func TestSuspectsOpenAIResponsesCapabilityMismatch(t *testing.T) {
t.Parallel()
tests := []struct {
name string
probe sub2api.ProbeResult
want bool
}{
{
name: "ok result is not suspect",
probe: sub2api.ProbeResult{OK: true, Message: "API returned 403: Forbidden"},
want: false,
},
{
name: "blank message is not suspect",
probe: sub2api.ProbeResult{OK: false},
want: false,
},
{
name: "403 forbidden is suspect",
probe: sub2api.ProbeResult{OK: false, Message: " API returned 403: Forbidden "},
want: true,
},
{
name: "responses advisory chinese is suspect",
probe: sub2api.ProbeResult{OK: false, Message: "账号本身可正常使用,但当前测试接口仅支持 Responses API 路径。请直接通过实际 API 调用验证。"},
want: true,
},
{
name: "responses advisory english is suspect",
probe: sub2api.ProbeResult{OK: false, Message: "Responses API endpoint exists, please directly verify with actual API calls."},
want: true,
},
{
name: "unrelated failure is not suspect",
probe: sub2api.ProbeResult{OK: false, Message: "API returned 401: invalid token"},
want: false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := SuspectsOpenAIResponsesCapabilityMismatch(tc.probe); got != tc.want {
t.Fatalf("SuspectsOpenAIResponsesCapabilityMismatch(%+v) = %v, want %v", tc.probe, got, tc.want)
}
})
}
}
func TestShouldAttemptOpenAIResponsesCapabilityRepair(t *testing.T) {
t.Parallel()
tests := []struct {
name string
suspect bool
completion sub2api.GatewayCompletionResult
want bool
}{
{
name: "suspect 502 temporarily unavailable",
suspect: true,
completion: sub2api.GatewayCompletionResult{StatusCode: 502, BodyPreview: `{"error":{"message":"Upstream service temporarily unavailable"}}`},
want: true,
},
{
name: "suspect 503 no available accounts",
suspect: true,
completion: sub2api.GatewayCompletionResult{StatusCode: 503, BodyPreview: `{"error":{"message":"No available accounts"}}`},
want: true,
},
{
name: "non suspect does not repair",
suspect: false,
completion: sub2api.GatewayCompletionResult{StatusCode: 502, BodyPreview: `{"error":{"message":"Upstream service temporarily unavailable"}}`},
want: false,
},
{
name: "successful completion does not repair",
suspect: true,
completion: sub2api.GatewayCompletionResult{OK: true, StatusCode: 200},
want: false,
},
{
name: "wrong status does not repair",
suspect: true,
completion: sub2api.GatewayCompletionResult{StatusCode: 500, BodyPreview: `{"error":{"message":"Upstream service temporarily unavailable"}}`},
want: false,
},
{
name: "wrong body does not repair",
suspect: true,
completion: sub2api.GatewayCompletionResult{StatusCode: 502, BodyPreview: `{"error":{"message":"bad gateway"}}`},
want: false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := ShouldAttemptOpenAIResponsesCapabilityRepair(tc.suspect, tc.completion); got != tc.want {
t.Fatalf("ShouldAttemptOpenAIResponsesCapabilityRepair(%v, %+v) = %v, want %v", tc.suspect, tc.completion, got, tc.want)
}
})
}
}
func TestNormalizedAccountIDs(t *testing.T) {
t.Parallel()
got := normalizedAccountIDs([]string{" account-1 ", "", "account-2", "account-1", " "})
want := []string{"account-1", "account-2"}
if len(got) != len(want) {
t.Fatalf("normalizedAccountIDs() len = %d, want %d; values = %v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("normalizedAccountIDs()[%d] = %q, want %q; values = %v", i, got[i], want[i], got)
}
}
}
func TestMaybeRepairOpenAIResponsesCapabilitySkipsWithoutAccountIDs(t *testing.T) {
t.Parallel()
service := NewService(&fakeRepairHost{})
original := sub2api.GatewayCompletionResult{
StatusCode: 502,
BodyPreview: `{"error":{"message":"No available accounts"}}`,
}
got, err := service.maybeRepairOpenAIResponsesCapability(context.Background(), ClosureRequest{
AccountIDs: []string{" ", ""},
ResponsesCapabilitySuspect: true,
}, sub2api.GatewayCompletionCheckRequest{APIKey: "user-key", Model: "kimi-k2.6"}, original)
if err != nil {
t.Fatalf("maybeRepairOpenAIResponsesCapability() error = %v", err)
}
if got != original {
t.Fatalf("maybeRepairOpenAIResponsesCapability() = %+v, want original %+v", got, original)
}
}
func TestMaybeRepairOpenAIResponsesCapabilitySwallowsDisableError(t *testing.T) {
t.Parallel()
host := &fakeRepairHost{disableErr: errors.New("update failed")}
service := NewService(host)
original := sub2api.GatewayCompletionResult{
StatusCode: 502,
BodyPreview: `{"error":{"message":"Upstream service temporarily unavailable"}}`,
}
got, err := service.maybeRepairOpenAIResponsesCapability(context.Background(), ClosureRequest{
AccountIDs: []string{"account-1"},
ResponsesCapabilitySuspect: true,
}, sub2api.GatewayCompletionCheckRequest{APIKey: "user-key", Model: "kimi-k2.6"}, original)
if err != nil {
t.Fatalf("maybeRepairOpenAIResponsesCapability() error = %v", err)
}
if got != original {
t.Fatalf("maybeRepairOpenAIResponsesCapability() = %+v, want original %+v", got, original)
}
if host.disableCalls != 1 {
t.Fatalf("disableCalls = %d, want 1", host.disableCalls)
}
if host.completionCalls != 0 {
t.Fatalf("completionCalls = %d, want 0", host.completionCalls)
}
}
func TestMaybeRepairOpenAIResponsesCapabilityClearsTempUnschedulableBeforeRetry(t *testing.T) {
t.Parallel()
host := &fakeRepairHost{}
service := NewService(host)
got, err := service.maybeRepairOpenAIResponsesCapability(context.Background(), ClosureRequest{
AccountIDs: []string{"account-1", "account-1"},
ResponsesCapabilitySuspect: true,
}, sub2api.GatewayCompletionCheckRequest{APIKey: "user-key", Model: "kimi-k2.6"}, sub2api.GatewayCompletionResult{
StatusCode: 503,
BodyPreview: `{"error":{"message":"No available accounts"}}`,
})
if err != nil {
t.Fatalf("maybeRepairOpenAIResponsesCapability() error = %v", err)
}
if !got.OK || got.StatusCode != 200 {
t.Fatalf("maybeRepairOpenAIResponsesCapability() = %+v, want repaired completion success", got)
}
if host.disableCalls != 1 {
t.Fatalf("disableCalls = %d, want 1", host.disableCalls)
}
if host.clearTempUnschedulableCalls != 1 {
t.Fatalf("clearTempUnschedulableCalls = %d, want 1", host.clearTempUnschedulableCalls)
}
if len(host.clearedTempUnschedulableAccountIDs) != 1 || host.clearedTempUnschedulableAccountIDs[0] != "account-1" {
t.Fatalf("clearedTempUnschedulableAccountIDs = %v, want [account-1]", host.clearedTempUnschedulableAccountIDs)
}
if host.completionCalls != 1 {
t.Fatalf("completionCalls = %d, want 1", host.completionCalls)
}
}
type fakeRepairHost struct {
disableErr error
clearTempUnschedulableErr error
disableCalls int
clearTempUnschedulableCalls int
completionCalls int
clearedTempUnschedulableAccountIDs []string
}
func (f *fakeRepairHost) EnsureSubscriptionAccess(_ context.Context, _ sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) {
return sub2api.SubscriptionAccessRef{}, nil
}
func (f *fakeRepairHost) AssignSubscription(_ context.Context, _ sub2api.AssignSubscriptionRequest) (sub2api.SubscriptionRef, error) {
return sub2api.SubscriptionRef{}, nil
}
func (f *fakeRepairHost) CheckGatewayAccess(_ context.Context, _ sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error) {
return sub2api.GatewayAccessResult{}, nil
}
func (f *fakeRepairHost) CheckGatewayCompletion(_ context.Context, _ sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) {
f.completionCalls++
return sub2api.GatewayCompletionResult{OK: true, StatusCode: 200}, nil
}
func (f *fakeRepairHost) DisableOpenAIResponsesAPI(_ context.Context, _ []string) error {
f.disableCalls++
return f.disableErr
}
func (f *fakeRepairHost) ClearTempUnschedulable(_ context.Context, accountIDs []string) error {
f.clearTempUnschedulableCalls++
f.clearedTempUnschedulableAccountIDs = append([]string(nil), accountIDs...)
return f.clearTempUnschedulableErr
}