Files
sub2api-cn-relay-manager/internal/provision/runtime_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

527 lines
19 KiB
Go

package provision
import (
"context"
"database/sql"
"fmt"
"path/filepath"
"strings"
"testing"
_ "modernc.org/sqlite"
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/pack"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
func TestRuntimeImportServicePersistsOperationalState(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
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 := NewRuntimeImportService(store, host)
result, err := svc.Import(context.Background(), RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: sampleProviderManifest(),
Mode: ImportModePartial,
Keys: []string{" key-1 ", "key-2", "key-1"},
Access: AccessRequest{
Mode: AccessModeSelfService,
ProbeAPIKey: "user-key",
},
})
if err != nil {
t.Fatalf("RuntimeImportService.Import() error = %v", err)
}
if result.BatchID <= 0 {
t.Fatalf("BatchID = %d, want positive id", result.BatchID)
}
if result.Report.BatchStatus != BatchStatusSucceeded {
t.Fatalf("BatchStatus = %q, want %q", result.Report.BatchStatus, BatchStatusSucceeded)
}
if got := queryCount(t, store.SQLDB(), "hosts"); got != 1 {
t.Fatalf("hosts row count = %d, want 1", got)
}
if got := queryCount(t, store.SQLDB(), "packs"); got != 1 {
t.Fatalf("packs row count = %d, want 1", got)
}
if got := queryCount(t, store.SQLDB(), "providers"); got != 1 {
t.Fatalf("providers row count = %d, want 1", got)
}
if got := queryCount(t, store.SQLDB(), "import_batches"); got != 1 {
t.Fatalf("import_batches row count = %d, want 1", got)
}
if got := queryCount(t, store.SQLDB(), "import_batch_items"); got != 2 {
t.Fatalf("import_batch_items row count = %d, want 2", got)
}
if got := queryCount(t, store.SQLDB(), "managed_resources"); got != 4 {
t.Fatalf("managed_resources row count = %d, want 4", got)
}
if got := queryCount(t, store.SQLDB(), "probe_results"); got != 2 {
t.Fatalf("probe_results row count = %d, want 2", got)
}
if got := queryCount(t, store.SQLDB(), "access_closure_records"); got != 1 {
t.Fatalf("access_closure_records row count = %d, want 1", got)
}
var batchStatus string
var accessStatus string
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT batch_status, access_status FROM import_batches WHERE id = ?", result.BatchID).Scan(&batchStatus, &accessStatus); err != nil {
t.Fatalf("query import batch state: %v", err)
}
if batchStatus != BatchStatusSucceeded {
t.Fatalf("persisted batch_status = %q, want %q", batchStatus, BatchStatusSucceeded)
}
if accessStatus != AccessStatusSelfServiceReady {
t.Fatalf("persisted access_status = %q, want %q", accessStatus, AccessStatusSelfServiceReady)
}
var fingerprint string
var accountStatus string
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT key_fingerprint, account_status FROM import_batch_items ORDER BY id LIMIT 1").Scan(&fingerprint, &accountStatus); err != nil {
t.Fatalf("query import batch item: %v", err)
}
if fingerprint == "key-1" || fingerprint == "key-2" || len(fingerprint) < 10 {
t.Fatalf("key_fingerprint = %q, want hashed fingerprint instead of raw key", fingerprint)
}
if accountStatus != "passed" {
t.Fatalf("account_status = %q, want passed", accountStatus)
}
}
func TestRuntimeImportServicePersistsFailedBatchAfterStrictRollback(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
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 := NewRuntimeImportService(store, host)
result, err := svc.Import(context.Background(), RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: sampleProviderManifest(),
Mode: ImportModeStrict,
Keys: []string{"key-1", "key-2"},
Access: AccessRequest{
Mode: AccessModeSelfService,
ProbeAPIKey: "user-key",
},
})
if err == nil {
t.Fatal("RuntimeImportService.Import() error = nil, want strict failure")
}
if result.BatchID <= 0 {
t.Fatalf("BatchID = %d, want positive id", result.BatchID)
}
var batchStatus string
var accessStatus string
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT batch_status, access_status FROM import_batches WHERE id = ?", result.BatchID).Scan(&batchStatus, &accessStatus); err != nil {
t.Fatalf("query failed import batch state: %v", err)
}
if batchStatus != BatchStatusFailed {
t.Fatalf("persisted batch_status = %q, want %q", batchStatus, BatchStatusFailed)
}
if accessStatus != AccessStatusBroken {
t.Fatalf("persisted access_status = %q, want %q", accessStatus, AccessStatusBroken)
}
if got := queryCount(t, store.SQLDB(), "managed_resources"); got != 0 {
t.Fatalf("managed_resources row count = %d, want 0 after strict rollback", got)
}
if got := queryCount(t, store.SQLDB(), "probe_results"); got != 2 {
t.Fatalf("probe_results row count = %d, want 2", got)
}
if got := queryCount(t, store.SQLDB(), "access_closure_records"); got != 1 {
t.Fatalf("access_closure_records row count = %d, want 1", got)
}
}
func TestRuntimeImportServicePersistsWarningAccountStatusForAdvisoryProbeFailure(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}},
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,
},
}
svc := NewRuntimeImportService(store, host)
result, err := svc.Import(context.Background(), RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: sampleProviderManifest(),
Mode: ImportModePartial,
Keys: []string{"key-1"},
Access: AccessRequest{
Mode: AccessModeSelfService,
ProbeAPIKey: "user-key",
},
})
if err != nil {
t.Fatalf("RuntimeImportService.Import() error = %v", err)
}
if result.Report.BatchStatus != BatchStatusSucceeded {
t.Fatalf("BatchStatus = %q, want %q", result.Report.BatchStatus, BatchStatusSucceeded)
}
var accountStatus string
var summary string
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT account_status, probe_summary_json FROM import_batch_items WHERE batch_id = ? ORDER BY id LIMIT 1", result.BatchID).Scan(&accountStatus, &summary); err != nil {
t.Fatalf("query import batch item: %v", err)
}
if accountStatus != AccountStatusWarning {
t.Fatalf("account_status = %q, want %q", accountStatus, AccountStatusWarning)
}
if !strings.Contains(summary, "\"probe_advisory\":true") {
t.Fatalf("probe_summary_json = %s, want probe_advisory=true", summary)
}
}
func TestRuntimeImportServicePersistsWarningAccountStatusForForbiddenProbeRace(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}},
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,
},
}
svc := NewRuntimeImportService(store, host)
result, err := svc.Import(context.Background(), RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: sampleProviderManifest(),
Mode: ImportModePartial,
Keys: []string{"key-1"},
Access: AccessRequest{
Mode: AccessModeSelfService,
ProbeAPIKey: "user-key",
},
})
if err != nil {
t.Fatalf("RuntimeImportService.Import() error = %v", err)
}
if result.Report.BatchStatus != BatchStatusSucceeded {
t.Fatalf("BatchStatus = %q, want %q", result.Report.BatchStatus, BatchStatusSucceeded)
}
var accountStatus string
var summary string
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT account_status, probe_summary_json FROM import_batch_items WHERE batch_id = ? ORDER BY id LIMIT 1", result.BatchID).Scan(&accountStatus, &summary); err != nil {
t.Fatalf("query import batch item: %v", err)
}
if accountStatus != AccountStatusWarning {
t.Fatalf("account_status = %q, want %q", accountStatus, AccountStatusWarning)
}
if !strings.Contains(summary, "\"probe_advisory\":true") {
t.Fatalf("probe_summary_json = %s, want probe_advisory=true", summary)
}
}
func TestRuntimeImportServicePersistsPartialManagedResourcesOnAccessFailure(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
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"}},
},
assignErr: fmt.Errorf("group is not a subscription type"),
}
svc := NewRuntimeImportService(store, host)
result, err := svc.Import(context.Background(), RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: sampleProviderManifest(),
Mode: ImportModePartial,
Keys: []string{"key-1"},
Access: AccessRequest{
Mode: AccessModeSubscription,
ProbeAPIKey: "user-key",
Subscriptions: []SubscriptionTarget{{UserID: "1", DurationDays: 30}},
},
})
if err == nil {
t.Fatal("RuntimeImportService.Import() error = nil, want partial failure")
}
if result.BatchID <= 0 {
t.Fatalf("BatchID = %d, want positive id", result.BatchID)
}
if got := queryCount(t, store.SQLDB(), "managed_resources"); got != 4 {
t.Fatalf("managed_resources row count = %d, want 4 persisted resources on partial failure", got)
}
var batchStatus string
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT batch_status FROM import_batches WHERE id = ?", result.BatchID).Scan(&batchStatus); err != nil {
t.Fatalf("query import batch status: %v", err)
}
if batchStatus != BatchStatusPartial {
t.Fatalf("persisted batch_status = %q, want %q", batchStatus, BatchStatusPartial)
}
}
func TestRuntimeImportServiceRepeatedImportReusesManagedResources(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "key-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"}},
}
svc := NewRuntimeImportService(store, host)
request := RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: sampleProviderManifest(),
Mode: ImportModePartial,
Keys: []string{"key-1"},
Access: AccessRequest{
Mode: AccessModeSelfService,
ProbeAPIKey: "user-key",
},
}
first, err := svc.Import(context.Background(), request)
if err != nil {
t.Fatalf("first Import() error = %v", err)
}
second, err := svc.Import(context.Background(), request)
if err != nil {
t.Fatalf("second Import() error = %v", err)
}
if second.BatchID <= first.BatchID {
t.Fatalf("second BatchID = %d, want > first BatchID %d", second.BatchID, first.BatchID)
}
if got := queryCount(t, store.SQLDB(), "managed_resources"); got != 3 {
t.Fatalf("managed_resources row count = %d, want 3 after reused import", got)
}
if got := queryCount(t, store.SQLDB(), "import_batches"); got != 2 {
t.Fatalf("import_batches row count = %d, want 2", got)
}
}
func TestRuntimeImportServiceImportReconcilesExistingChannelConfiguration(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "minimax-01"}},
testResults: map[string]sub2api.ProbeResult{
"account_1": {OK: true, Status: "passed"},
},
models: map[string][]sub2api.AccountModel{
"account_1": {{ID: "MiniMax-M2.7-highspeed"}},
},
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"MiniMax-M2.5-highspeed", "MiniMax-M2.7-highspeed"}},
managedSnapshot: sub2api.ManagedResourceSnapshot{
Groups: []sub2api.NamedResource{{ID: "group_existing", Name: "MiniMax 默认分组-self-service"}},
Channels: []sub2api.NamedResource{{ID: "channel_existing", Name: "MiniMax 默认渠道-self-service"}},
Plans: []sub2api.NamedResource{{ID: "plan_existing", Name: "MiniMax 默认套餐-self-service"}},
},
}
provider := sampleProviderManifest()
provider.ProviderID = "minimax"
provider.DisplayName = "MiniMax OpenAI Compatible"
provider.BaseURL = "https://v2.aicodee.com/v1"
provider.DefaultModels = []string{"MiniMax-M2.5-highspeed", "MiniMax-M2.7-highspeed"}
provider.SmokeTestModel = "MiniMax-M2.7-highspeed"
provider.GroupTemplate.Name = "MiniMax 默认分组"
provider.ChannelTemplate = pack.ChannelTemplate{
Name: "MiniMax 默认渠道",
ModelMapping: map[string]string{"MiniMax-M2.5-highspeed": "MiniMax-M2.5-highspeed", "MiniMax-M2.7-highspeed": "MiniMax-M2.7-highspeed"},
}
provider.PlanTemplate.Name = "MiniMax 默认套餐"
svc := NewRuntimeImportService(store, host)
result, err := svc.Import(context.Background(), RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: provider,
Mode: ImportModePartial,
Keys: []string{"key-1"},
Access: AccessRequest{
Mode: AccessModeSelfService,
ProbeAPIKey: "user-key",
},
})
if err != nil {
t.Fatalf("RuntimeImportService.Import() error = %v", err)
}
if result.Report.Channel.ID != "channel_existing" {
t.Fatalf("Channel.ID = %q, want reused channel_existing", result.Report.Channel.ID)
}
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))
}
if got := host.updateChannelReq.ModelPricing[0].Models; len(got) != 2 || got[0] != "MiniMax-M2.5-highspeed" || got[1] != "MiniMax-M2.7-highspeed" {
t.Fatalf("UpdateChannel().ModelPricing[0].Models = %v, want minimax default models", got)
}
if host.updateChannelReq.ModelPricing[0].BillingMode != "token" {
t.Fatalf("UpdateChannel().ModelPricing[0].BillingMode = %q, want token", host.updateChannelReq.ModelPricing[0].BillingMode)
}
}
func openProvisionTestStore(t *testing.T) *sqlite.DB {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "state.db")
dsn := fmt.Sprintf("file:%s?_busy_timeout=5000&_pragma=foreign_keys(0)", filepath.ToSlash(dbPath))
store, err := sqlite.Open(context.Background(), dsn)
if err != nil {
t.Fatalf("sqlite.Open() error = %v", err)
}
return store
}
func closeProvisionTestStore(t *testing.T, store *sqlite.DB) {
t.Helper()
if err := store.Close(); err != nil {
t.Fatalf("store.Close() error = %v", err)
}
}
func seedProvisionHost(t *testing.T, store *sqlite.DB, hostID, baseURL string) int64 {
t.Helper()
id, err := store.Hosts().Create(context.Background(), sqlite.Host{
HostID: hostID,
BaseURL: baseURL,
HostVersion: "0.1.126",
AuthType: "apikey",
AuthToken: "test-host-token",
})
if err != nil {
t.Fatalf("Hosts().Create() error = %v", err)
}
return id
}
func queryCount(t *testing.T, db *sql.DB, table string) int {
t.Helper()
var count int
if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM "+table).Scan(&count); err != nil {
t.Fatalf("count rows for %s: %v", table, err)
}
return count
}