Downgrade the first third-party account test 403 to an advisory warning when models are already present, and retry transient gateway completion 503 responses during access closure. Add regression coverage for the probe race and completion retry paths, update the execution board, and store the final v0.1.129 Kimi A7M fresh-host acceptance artifact that now reaches succeeded/active/subscription_ready.
776 lines
30 KiB
Go
776 lines
30 KiB
Go
package provision
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sub2api-cn-relay-manager/internal/host/sub2api"
|
|
"sub2api-cn-relay-manager/internal/pack"
|
|
)
|
|
|
|
func TestImportServiceImportSubscriptionFlow(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
svc := NewImportService(host)
|
|
report, err := svc.Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{
|
|
Mode: AccessModeSubscription,
|
|
ProbeAPIKey: "user-key",
|
|
Subscriptions: []SubscriptionTarget{{UserID: "user_1", DurationDays: 30}},
|
|
},
|
|
Keys: []string{" key-1 ", "key-2", "key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
|
|
if report.BatchStatus != BatchStatusSucceeded {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded)
|
|
}
|
|
if report.ProviderStatus != ProviderStatusActive {
|
|
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive)
|
|
}
|
|
if report.AccessStatus != AccessStatusSubscriptionReady {
|
|
t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSubscriptionReady)
|
|
}
|
|
if !reflect.DeepEqual(report.AcceptedKeys, []string{"key-1", "key-2"}) {
|
|
t.Fatalf("AcceptedKeys = %#v, want deduped normalized keys", report.AcceptedKeys)
|
|
}
|
|
if len(host.assignedSubscriptions) != 1 {
|
|
t.Fatalf("assigned subscriptions = %d, want 1", len(host.assignedSubscriptions))
|
|
}
|
|
if host.createGroupReq.SubscriptionType != "subscription" {
|
|
t.Fatalf("CreateGroup subscription_type = %q, want %q", host.createGroupReq.SubscriptionType, "subscription")
|
|
}
|
|
if host.createGroupReq.Platform != "openai" {
|
|
t.Fatalf("CreateGroup platform = %q, want %q", host.createGroupReq.Platform, "openai")
|
|
}
|
|
if host.gatewayProbe.ExpectedModel != "deepseek-chat" {
|
|
t.Fatalf("gateway probe model = %q, want %q", host.gatewayProbe.ExpectedModel, "deepseek-chat")
|
|
}
|
|
if host.testedModels["account_1"] != "deepseek-chat" || host.testedModels["account_2"] != "deepseek-chat" {
|
|
t.Fatalf("testedModels = %#v, want deepseek-chat for all created accounts", host.testedModels)
|
|
}
|
|
}
|
|
|
|
func TestImportServiceStrictModeFailsWhenAnyAccountProbeFails(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: false, Status: "failed", Message: "bad key"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
}
|
|
|
|
svc := NewImportService(host)
|
|
report, err := svc.Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModeStrict,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1", "key-2"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Import() error = nil, want strict mode failure")
|
|
}
|
|
if report.BatchStatus != BatchStatusFailed {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusFailed)
|
|
}
|
|
if report.ProviderStatus != ProviderStatusFailed {
|
|
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusFailed)
|
|
}
|
|
}
|
|
|
|
func TestImportServiceRejectsUnknownMode(t *testing.T) {
|
|
svc := NewImportService(&fakeHostAdapter{})
|
|
_, err := svc.Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: "unknown",
|
|
Access: AccessRequest{Mode: AccessModeSelfService},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Import() error = nil, want mode validation error")
|
|
}
|
|
}
|
|
|
|
func TestImportServiceMarksBrokenWhenCompletionSmokeFails(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
completionResult: sub2api.GatewayCompletionResult{OK: false, StatusCode: 502, ContentType: "application/json", BodyPreview: `{"error":"upstream_error"}`},
|
|
}
|
|
|
|
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if report.BatchStatus != BatchStatusPartial {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusPartial)
|
|
}
|
|
if report.ProviderStatus != ProviderStatusDegraded {
|
|
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusDegraded)
|
|
}
|
|
if report.AccessStatus != AccessStatusBroken {
|
|
t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusBroken)
|
|
}
|
|
}
|
|
|
|
func TestImportServiceTreatsResponsesOnlyProbeFailureAsAdvisoryWhenGatewaySucceeds(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "minimax-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {
|
|
OK: false,
|
|
Status: "failed",
|
|
Message: "账号本身可正常使用,但当前测试接口仅支持 Responses API 路径。请直接通过实际 API 调用验证。",
|
|
},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{
|
|
OK: true,
|
|
StatusCode: 200,
|
|
HasExpectedModel: true,
|
|
Models: []string{"deepseek-chat"},
|
|
CompletionOK: true,
|
|
CompletionStatus: 200,
|
|
CompletionType: "text/event-stream",
|
|
},
|
|
}
|
|
|
|
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if report.BatchStatus != BatchStatusSucceeded {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded)
|
|
}
|
|
if report.ProviderStatus != ProviderStatusActive {
|
|
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive)
|
|
}
|
|
if report.AccessStatus != AccessStatusSelfServiceReady {
|
|
t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady)
|
|
}
|
|
if len(report.Accounts) != 1 {
|
|
t.Fatalf("Accounts len = %d, want 1", len(report.Accounts))
|
|
}
|
|
if got := report.Accounts[0].ValidationStatus(); got != AccountStatusWarning {
|
|
t.Fatalf("ValidationStatus = %q, want %q", got, AccountStatusWarning)
|
|
}
|
|
}
|
|
|
|
func TestImportServiceTreatsTransientProbeFailureAsAdvisoryWhenGatewaySucceeds(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {
|
|
OK: false,
|
|
Status: "failed",
|
|
Message: "API returned 429: {\"error\":{\"message\":\"Rate limited (429); user=1997/2000 model=49/50; daily_exhausted=False\",\"type\":\"rate_limit_error\",\"code\":\"rate_limit_exceeded\"}}",
|
|
},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{
|
|
OK: true,
|
|
StatusCode: 200,
|
|
HasExpectedModel: true,
|
|
Models: []string{"deepseek-chat"},
|
|
CompletionOK: true,
|
|
CompletionStatus: 200,
|
|
CompletionType: "application/json",
|
|
},
|
|
}
|
|
|
|
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if report.BatchStatus != BatchStatusSucceeded {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded)
|
|
}
|
|
if report.ProviderStatus != ProviderStatusActive {
|
|
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive)
|
|
}
|
|
if report.AccessStatus != AccessStatusSelfServiceReady {
|
|
t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady)
|
|
}
|
|
if got := report.Accounts[0].ValidationStatus(); got != AccountStatusWarning {
|
|
t.Fatalf("ValidationStatus = %q, want %q", got, AccountStatusWarning)
|
|
}
|
|
}
|
|
|
|
func TestImportServiceTreatsForbiddenProbeRaceAsAdvisoryWhenGatewaySucceeds(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {
|
|
OK: false,
|
|
Status: "failed",
|
|
Message: "API returned 403: Forbidden",
|
|
},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{
|
|
OK: true,
|
|
StatusCode: 200,
|
|
HasExpectedModel: true,
|
|
Models: []string{"deepseek-chat"},
|
|
CompletionOK: true,
|
|
CompletionStatus: 200,
|
|
CompletionType: "application/json",
|
|
},
|
|
}
|
|
|
|
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if report.BatchStatus != BatchStatusSucceeded {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded)
|
|
}
|
|
if report.ProviderStatus != ProviderStatusActive {
|
|
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive)
|
|
}
|
|
if report.AccessStatus != AccessStatusSelfServiceReady {
|
|
t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady)
|
|
}
|
|
if got := report.Accounts[0].ValidationStatus(); got != AccountStatusWarning {
|
|
t.Fatalf("ValidationStatus = %q, want %q", got, AccountStatusWarning)
|
|
}
|
|
}
|
|
|
|
func TestImportServiceRetriesTransientGatewayCompletionFailure(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {
|
|
OK: false,
|
|
Status: "failed",
|
|
Message: "API returned 403: Forbidden",
|
|
},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{
|
|
OK: true,
|
|
StatusCode: 200,
|
|
HasExpectedModel: true,
|
|
Models: []string{"deepseek-chat"},
|
|
},
|
|
completionResults: []sub2api.GatewayCompletionResult{
|
|
{OK: false, StatusCode: 503, ContentType: "application/json", BodyPreview: `{"error":{"message":"Service temporarily unavailable","type":"api_error"}}`},
|
|
{OK: true, StatusCode: 200, ContentType: "application/json"},
|
|
},
|
|
}
|
|
|
|
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if report.BatchStatus != BatchStatusSucceeded {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded)
|
|
}
|
|
if report.ProviderStatus != ProviderStatusActive {
|
|
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive)
|
|
}
|
|
if report.AccessStatus != AccessStatusSelfServiceReady {
|
|
t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady)
|
|
}
|
|
if !report.Gateway.CompletionOK || report.Gateway.CompletionStatus != 200 {
|
|
t.Fatalf("Gateway completion = %+v, want retried success", report.Gateway)
|
|
}
|
|
if host.completionCalls != 2 {
|
|
t.Fatalf("completion calls = %d, want 2", host.completionCalls)
|
|
}
|
|
}
|
|
|
|
func TestImportServiceStrictModeRollsBackCreatedResources(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: false, Status: "failed", Message: "bad key"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
}
|
|
|
|
svc := NewImportService(host)
|
|
_, err := svc.Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModeStrict,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1", "key-2"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Import() error = nil, want strict mode failure")
|
|
}
|
|
|
|
want := []string{"account:account_2", "account:account_1", "channel:channel_1", "group:group_1"}
|
|
if !reflect.DeepEqual(host.deletedResources, want) {
|
|
t.Fatalf("deleted resources = %#v, want %#v", host.deletedResources, want)
|
|
}
|
|
}
|
|
|
|
func TestImportReusesExistingGroup(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "ready"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
managedSnapshot: sub2api.ManagedResourceSnapshot{
|
|
Groups: []sub2api.NamedResource{{ID: "group_existing", Name: "DeepSeek 默认分组-self-service"}},
|
|
},
|
|
}
|
|
|
|
svc := NewImportService(host)
|
|
report, err := svc.Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if report.Group.ID != "group_existing" {
|
|
t.Fatalf("Group.ID = %q, want reused group_existing", report.Group.ID)
|
|
}
|
|
if host.createGroupCalls != 0 {
|
|
t.Fatalf("CreateGroup() calls = %d, want 0 when group already exists", host.createGroupCalls)
|
|
}
|
|
if host.createChannelCalls != 1 {
|
|
t.Fatalf("CreateChannel() calls = %d, want 1", host.createChannelCalls)
|
|
}
|
|
}
|
|
|
|
func TestImportCreatesChannelWithManifestModelMapping(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "ready"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
_, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if host.createChannelReq.Name != "DeepSeek 默认渠道-self-service" {
|
|
t.Fatalf("CreateChannel().Name = %q, want DeepSeek 默认渠道-self-service", host.createChannelReq.Name)
|
|
}
|
|
if len(host.createChannelReq.GroupIDs) != 1 || host.createChannelReq.GroupIDs[0] != "group_1" {
|
|
t.Fatalf("CreateChannel().GroupIDs = %v, want [group_1]", host.createChannelReq.GroupIDs)
|
|
}
|
|
if got := host.createChannelReq.ModelMapping["deepseek-chat"]; got != "deepseek-chat" {
|
|
t.Fatalf("CreateChannel().ModelMapping = %+v, want deepseek-chat passthrough", host.createChannelReq.ModelMapping)
|
|
}
|
|
if !host.createChannelReq.RestrictModels {
|
|
t.Fatal("CreateChannel().RestrictModels = false, want true")
|
|
}
|
|
if host.createChannelReq.BillingModelSource != "channel_mapped" {
|
|
t.Fatalf("CreateChannel().BillingModelSource = %q, want channel_mapped", host.createChannelReq.BillingModelSource)
|
|
}
|
|
if len(host.createChannelReq.ModelPricing) != 1 {
|
|
t.Fatalf("CreateChannel().ModelPricing len = %d, want 1", len(host.createChannelReq.ModelPricing))
|
|
}
|
|
if len(host.createChannelReq.ModelPricing[0].Models) != 2 {
|
|
t.Fatalf("CreateChannel().ModelPricing[0].Models = %v, want default model coverage", host.createChannelReq.ModelPricing[0].Models)
|
|
}
|
|
if host.createChannelReq.ModelPricing[0].BillingMode != "token" {
|
|
t.Fatalf("CreateChannel().ModelPricing[0].BillingMode = %q, want token", host.createChannelReq.ModelPricing[0].BillingMode)
|
|
}
|
|
if len(host.batchCreateReq.Accounts) != 1 {
|
|
t.Fatalf("BatchCreateAccounts().Accounts len = %d, want 1", len(host.batchCreateReq.Accounts))
|
|
}
|
|
credentials := host.batchCreateReq.Accounts[0].Credentials
|
|
switch rawMapping := credentials["model_mapping"].(type) {
|
|
case map[string]string:
|
|
if got := rawMapping["deepseek-chat"]; got != "deepseek-chat" {
|
|
t.Fatalf("BatchCreateAccounts().Credentials.model_mapping = %+v, want deepseek-chat passthrough", rawMapping)
|
|
}
|
|
case map[string]any:
|
|
if got, _ := rawMapping["deepseek-chat"].(string); got != "deepseek-chat" {
|
|
t.Fatalf("BatchCreateAccounts().Credentials.model_mapping = %+v, want deepseek-chat passthrough", rawMapping)
|
|
}
|
|
default:
|
|
t.Fatalf("BatchCreateAccounts().Credentials = %+v, want model_mapping map", credentials)
|
|
}
|
|
}
|
|
|
|
func sampleProviderManifest() pack.ProviderManifest {
|
|
return pack.ProviderManifest{
|
|
ProviderID: "deepseek",
|
|
DisplayName: "DeepSeek OpenAI Compatible",
|
|
BaseURL: "https://api.deepseek.com",
|
|
Platform: "openai",
|
|
AccountType: "apikey",
|
|
DefaultModels: []string{"deepseek-chat", "deepseek-reasoner"},
|
|
SmokeTestModel: "deepseek-chat",
|
|
GroupTemplate: pack.GroupTemplate{Name: "DeepSeek 默认分组", RateMultiplier: 1},
|
|
ChannelTemplate: pack.ChannelTemplate{Name: "DeepSeek 默认渠道", ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"}},
|
|
PlanTemplate: pack.PlanTemplate{Name: "DeepSeek 默认套餐", Price: 19.9, ValidityDays: 30, ValidityUnit: "day"},
|
|
}
|
|
}
|
|
|
|
func TestImportReconcilesExistingChannelConfiguration(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "ready"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
managedSnapshot: sub2api.ManagedResourceSnapshot{
|
|
Groups: []sub2api.NamedResource{{ID: "group_existing", Name: "DeepSeek 默认分组-self-service"}},
|
|
Channels: []sub2api.NamedResource{{ID: "channel_existing", Name: "DeepSeek 默认渠道-self-service"}},
|
|
},
|
|
}
|
|
|
|
_, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if host.createChannelCalls != 0 {
|
|
t.Fatalf("CreateChannel() calls = %d, want 0 when channel already exists", host.createChannelCalls)
|
|
}
|
|
if host.updateChannelCalls != 1 {
|
|
t.Fatalf("UpdateChannel() calls = %d, want 1", host.updateChannelCalls)
|
|
}
|
|
if host.updateChannelID != "channel_existing" {
|
|
t.Fatalf("UpdateChannel() id = %q, want channel_existing", host.updateChannelID)
|
|
}
|
|
if len(host.updateChannelReq.ModelPricing) != 1 {
|
|
t.Fatalf("UpdateChannel().ModelPricing len = %d, want 1", len(host.updateChannelReq.ModelPricing))
|
|
}
|
|
}
|
|
|
|
func TestImportDeletesExistingProviderAccountsBeforeGatewayClosure(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_new_1", Name: "deepseek-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_new_1": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_new_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
managedSnapshot: sub2api.ManagedResourceSnapshot{
|
|
Accounts: []sub2api.NamedResource{{ID: "account_old_1", Name: "deepseek-01"}, {ID: "account_old_2", Name: "deepseek-02"}},
|
|
},
|
|
}
|
|
|
|
_, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if host.listManagedReq.AccountNamePrefix != "deepseek-" {
|
|
t.Fatalf("AccountNamePrefix = %q, want %q", host.listManagedReq.AccountNamePrefix, "deepseek-")
|
|
}
|
|
wantDeleted := []string{"account:account_old_2", "account:account_old_1"}
|
|
if !reflect.DeepEqual(host.deletedResources, wantDeleted) {
|
|
t.Fatalf("deleted resources = %#v, want %#v", host.deletedResources, wantDeleted)
|
|
}
|
|
if !reflect.DeepEqual(host.callSequence, []string{"deleteAccount:account_old_2", "deleteAccount:account_old_1", "gateway", "completion"}) {
|
|
t.Fatalf("call sequence = %#v, want stale-account cleanup before gateway probe", host.callSequence)
|
|
}
|
|
}
|
|
|
|
func TestImportKeepsExistingAccountsWhenReplacementValidationFails(t *testing.T) {
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_new_1", Name: "deepseek-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_new_1": {OK: false, Status: "failed", Message: "bad key"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_new_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
managedSnapshot: sub2api.ManagedResourceSnapshot{
|
|
Accounts: []sub2api.NamedResource{{ID: "account_old_1", Name: "deepseek-01"}},
|
|
},
|
|
}
|
|
|
|
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
|
|
Keys: []string{"key-1"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Import() error = %v", err)
|
|
}
|
|
if report.BatchStatus != BatchStatusPartial {
|
|
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusPartial)
|
|
}
|
|
if len(host.deletedResources) != 0 {
|
|
t.Fatalf("deleted resources = %#v, want no stale-account cleanup when replacement validation fails", host.deletedResources)
|
|
}
|
|
}
|
|
|
|
type fakeHostAdapter struct {
|
|
batchAccounts []sub2api.AccountRef
|
|
batchCreateReq sub2api.BatchCreateAccountsRequest
|
|
testResults map[string]sub2api.ProbeResult
|
|
models map[string][]sub2api.AccountModel
|
|
gatewayResult sub2api.GatewayAccessResult
|
|
batchCreateErr error
|
|
assignErr error
|
|
gatewayErr error
|
|
hostVersion string
|
|
assignedSubscriptions []sub2api.AssignSubscriptionRequest
|
|
gatewayProbe sub2api.GatewayAccessCheckRequest
|
|
completionProbe sub2api.GatewayCompletionCheckRequest
|
|
deletedResources []string
|
|
managedSnapshot sub2api.ManagedResourceSnapshot
|
|
listManagedReq sub2api.ListManagedResourcesRequest
|
|
createGroupCalls int
|
|
createChannelCalls int
|
|
updateChannelCalls int
|
|
createPlanCalls int
|
|
createGroupReq sub2api.CreateGroupRequest
|
|
createChannelReq sub2api.CreateChannelRequest
|
|
updateChannelID string
|
|
updateChannelReq sub2api.CreateChannelRequest
|
|
callSequence []string
|
|
completionCalls int
|
|
completionResults []sub2api.GatewayCompletionResult
|
|
completionResult sub2api.GatewayCompletionResult
|
|
completionErr error
|
|
testedModels map[string]string
|
|
}
|
|
|
|
func (f *fakeHostAdapter) GetHostVersion(context.Context) (string, error) {
|
|
if strings.TrimSpace(f.hostVersion) == "" {
|
|
return "0.1.126", nil
|
|
}
|
|
return f.hostVersion, nil
|
|
}
|
|
func (f *fakeHostAdapter) ProbeCapabilities(context.Context) (sub2api.HostCapabilities, error) {
|
|
return sub2api.HostCapabilities{}, nil
|
|
}
|
|
func (f *fakeHostAdapter) CreateGroup(_ context.Context, req sub2api.CreateGroupRequest) (sub2api.GroupRef, error) {
|
|
f.createGroupCalls++
|
|
f.createGroupReq = req
|
|
return sub2api.GroupRef{ID: "group_1", Name: "g"}, nil
|
|
}
|
|
func (f *fakeHostAdapter) DeleteGroup(_ context.Context, groupID string) error {
|
|
f.deletedResources = append(f.deletedResources, "group:"+groupID)
|
|
return nil
|
|
}
|
|
func (f *fakeHostAdapter) CreateChannel(_ context.Context, req sub2api.CreateChannelRequest) (sub2api.ChannelRef, error) {
|
|
f.createChannelCalls++
|
|
f.createChannelReq = req
|
|
return sub2api.ChannelRef{ID: "channel_1", Name: "c"}, nil
|
|
}
|
|
func (f *fakeHostAdapter) UpdateChannel(_ context.Context, channelID string, req sub2api.CreateChannelRequest) error {
|
|
f.updateChannelCalls++
|
|
f.updateChannelID = channelID
|
|
f.updateChannelReq = req
|
|
return nil
|
|
}
|
|
func (f *fakeHostAdapter) DeleteChannel(_ context.Context, channelID string) error {
|
|
f.deletedResources = append(f.deletedResources, "channel:"+channelID)
|
|
return nil
|
|
}
|
|
func (f *fakeHostAdapter) CreatePlan(context.Context, sub2api.CreatePlanRequest) (sub2api.PlanRef, error) {
|
|
f.createPlanCalls++
|
|
return sub2api.PlanRef{ID: "plan_1", Name: "p"}, nil
|
|
}
|
|
func (f *fakeHostAdapter) DeletePlan(_ context.Context, planID string) error {
|
|
f.deletedResources = append(f.deletedResources, "plan:"+planID)
|
|
return nil
|
|
}
|
|
func (f *fakeHostAdapter) CreateAccount(context.Context, sub2api.CreateAccountRequest) (sub2api.AccountRef, error) {
|
|
return sub2api.AccountRef{}, errors.New("unused")
|
|
}
|
|
func (f *fakeHostAdapter) BatchCreateAccounts(_ context.Context, req sub2api.BatchCreateAccountsRequest) ([]sub2api.AccountRef, error) {
|
|
f.batchCreateReq = req
|
|
if f.batchCreateErr != nil {
|
|
return nil, f.batchCreateErr
|
|
}
|
|
return f.batchAccounts, nil
|
|
}
|
|
func (f *fakeHostAdapter) DeleteAccount(_ context.Context, accountID string) error {
|
|
f.callSequence = append(f.callSequence, "deleteAccount:"+accountID)
|
|
f.deletedResources = append(f.deletedResources, "account:"+accountID)
|
|
return nil
|
|
}
|
|
func (f *fakeHostAdapter) TestAccount(_ context.Context, accountID, modelID string) (sub2api.ProbeResult, error) {
|
|
if f.testedModels == nil {
|
|
f.testedModels = map[string]string{}
|
|
}
|
|
f.testedModels[accountID] = modelID
|
|
result, ok := f.testResults[accountID]
|
|
if !ok {
|
|
return sub2api.ProbeResult{}, fmt.Errorf("missing test result for %s", accountID)
|
|
}
|
|
return result, nil
|
|
}
|
|
func (f *fakeHostAdapter) GetAccountModels(_ context.Context, accountID string) ([]sub2api.AccountModel, error) {
|
|
models, ok := f.models[accountID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing models for %s", accountID)
|
|
}
|
|
return models, nil
|
|
}
|
|
func (f *fakeHostAdapter) EnsureSubscriptionAccess(_ context.Context, req sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) {
|
|
return sub2api.SubscriptionAccessRef{UserID: req.UserSelector, APIKey: "managed-subscription-key"}, nil
|
|
}
|
|
func (f *fakeHostAdapter) AssignSubscription(_ context.Context, req sub2api.AssignSubscriptionRequest) (sub2api.SubscriptionRef, error) {
|
|
if f.assignErr != nil {
|
|
return sub2api.SubscriptionRef{}, f.assignErr
|
|
}
|
|
f.assignedSubscriptions = append(f.assignedSubscriptions, req)
|
|
return sub2api.SubscriptionRef{ID: "subscription_1"}, nil
|
|
}
|
|
func (f *fakeHostAdapter) CheckGatewayAccess(_ context.Context, req sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error) {
|
|
f.callSequence = append(f.callSequence, "gateway")
|
|
f.gatewayProbe = req
|
|
if f.gatewayErr != nil {
|
|
return sub2api.GatewayAccessResult{}, f.gatewayErr
|
|
}
|
|
return f.gatewayResult, nil
|
|
}
|
|
func (f *fakeHostAdapter) CheckGatewayCompletion(_ context.Context, req sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) {
|
|
f.callSequence = append(f.callSequence, "completion")
|
|
f.completionProbe = req
|
|
f.completionCalls++
|
|
if f.completionErr != nil {
|
|
return sub2api.GatewayCompletionResult{}, f.completionErr
|
|
}
|
|
if len(f.completionResults) > 0 {
|
|
idx := f.completionCalls - 1
|
|
if idx >= len(f.completionResults) {
|
|
idx = len(f.completionResults) - 1
|
|
}
|
|
return f.completionResults[idx], nil
|
|
}
|
|
if f.completionResult.StatusCode == 0 && !f.completionResult.OK {
|
|
return sub2api.GatewayCompletionResult{OK: true, StatusCode: 200, ContentType: "application/json"}, nil
|
|
}
|
|
return f.completionResult, nil
|
|
}
|
|
func (f *fakeHostAdapter) ListManagedResources(_ context.Context, req sub2api.ListManagedResourcesRequest) (sub2api.ManagedResourceSnapshot, error) {
|
|
f.listManagedReq = req
|
|
return sub2api.ManagedResourceSnapshot{
|
|
Groups: filterNamedResourcesByExactName(f.managedSnapshot.Groups, req.GroupName),
|
|
Channels: filterNamedResourcesByExactName(f.managedSnapshot.Channels, req.ChannelName),
|
|
Plans: filterNamedResourcesByExactName(f.managedSnapshot.Plans, req.PlanName),
|
|
Accounts: filterNamedResourcesByPrefix(f.managedSnapshot.Accounts, req.AccountNamePrefix),
|
|
}, nil
|
|
}
|
|
|
|
func filterNamedResourcesByExactName(resources []sub2api.NamedResource, expected string) []sub2api.NamedResource {
|
|
expected = strings.TrimSpace(expected)
|
|
if expected == "" {
|
|
return nil
|
|
}
|
|
filtered := make([]sub2api.NamedResource, 0, len(resources))
|
|
for _, resource := range resources {
|
|
if strings.TrimSpace(resource.Name) == expected {
|
|
filtered = append(filtered, resource)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func filterNamedResourcesByPrefix(resources []sub2api.NamedResource, prefix string) []sub2api.NamedResource {
|
|
prefix = strings.TrimSpace(prefix)
|
|
if prefix == "" {
|
|
return resources
|
|
}
|
|
filtered := make([]sub2api.NamedResource, 0, len(resources))
|
|
for _, resource := range resources {
|
|
if strings.HasPrefix(strings.TrimSpace(resource.Name), prefix) {
|
|
filtered = append(filtered, resource)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|