Files
sub2api-cn-relay-manager/internal/provision/import_service_test.go
phamnazage-jpg 9134afed9f fix(provision): stabilize kimi a7m import closure
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.
2026-05-22 12:33:12 +08:00

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
}