package provision import ( "context" "database/sql" "encoding/json" "fmt" "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" "sub2api-cn-relay-manager/internal/testutil" ) 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 len(result.Report.HostOverlays) != 0 { t.Fatalf("HostOverlays = %+v, want none for sample provider", result.Report.HostOverlays) } 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) } if got := queryCount(t, store.SQLDB(), "provider_accounts"); got != 2 { t.Fatalf("provider_accounts row count = %d, want 2", 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) } var inventoryStatus string var inventoryShadowGroup string if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT account_status, shadow_group_id FROM provider_accounts WHERE host_account_id = ? ORDER BY id LIMIT 1", "account_1").Scan(&inventoryStatus, &inventoryShadowGroup); err != nil { t.Fatalf("query provider account inventory: %v", err) } if inventoryStatus != sqlite.ProviderAccountStatusActive { t.Fatalf("provider_accounts.account_status = %q, want %q", inventoryStatus, sqlite.ProviderAccountStatusActive) } if inventoryShadowGroup == "" { t.Fatal("provider_accounts.shadow_group_id = empty, want group id") } } func TestRuntimeImportServiceIncludesMatchingHostOverlaysInReport(t *testing.T) { store := openProvisionTestStore(t) defer closeProvisionTestStore(t, store) seedProvisionHost(t, store, "host-1", "https://sub2api.example.com") host := &fakeHostAdapter{ hostVersion: "0.1.129", 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: "kimi-k2.6"}}}, gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"kimi-k2.6"}, CompletionOK: true, CompletionStatus: 200}, } provider := sampleProviderManifest() provider.ProviderID = "kimi-a7m" provider.DefaultModels = []string{"kimi-k2.6"} provider.SmokeTestModel = "kimi-k2.6" provider.ChannelTemplate.ModelMapping = map[string]string{"kimi-k2.6": "kimi-k2.6"} provider.HostOverlays = []pack.HostOverlay{{ OverlayID: "sub2api-stock-v0129-kimi-a7m", DisplayName: "sub2api stock v0.1.129 Kimi A7M overlay", TargetHost: "sub2api", MinHostVersion: "0.1.129", MaxHostVersion: "0.1.129", Reason: "stock host still routes chat traffic into unsupported Responses path", }} result, err := NewRuntimeImportService(store, host).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: AccessModeSubscription, Subscriptions: []SubscriptionTarget{{UserID: "crm-user", DurationDays: 30}}, }, }) if err != nil { t.Fatalf("RuntimeImportService.Import() error = %v", err) } if len(result.Report.HostOverlays) != 1 || result.Report.HostOverlays[0].OverlayID != "sub2api-stock-v0129-kimi-a7m" { t.Fatalf("HostOverlays = %+v, want matching overlay", result.Report.HostOverlays) } } 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 TestRuntimeImportServicePersistsSelfServiceProbeKeyInAccessClosure(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: "deepseek-01"}}, 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"}, CompletionOK: true, CompletionStatus: 200}, } result, err := NewRuntimeImportService(store, host).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-probe-key", }, }) if err != nil { t.Fatalf("RuntimeImportService.Import() error = %v", err) } closures, err := store.AccessClosures().GetByBatchID(context.Background(), result.BatchID) if err != nil { t.Fatalf("AccessClosures().GetByBatchID() error = %v", err) } if len(closures) != 1 { t.Fatalf("access closures = %d, want 1", len(closures)) } var payload map[string]any if err := json.Unmarshal([]byte(closures[0].DetailsJSON), &payload); err != nil { t.Fatalf("decode access closure details: %v", err) } if got, _ := payload["probe_api_key"].(string); got != "user-probe-key" { t.Fatalf("probe_api_key = %q, want user-probe-key", got) } if got, _ := payload["requested_probe_api_key"].(string); got != "user-probe-key" { t.Fatalf("requested_probe_api_key = %q, want user-probe-key", got) } if got, _ := payload["effective_probe_key_source"].(string); got != "requested_probe_api_key" { t.Fatalf("effective_probe_key_source = %q, want requested_probe_api_key", got) } if got, _ := payload["effective_probe_key_fingerprint"].(string); got != fingerprintSecret("user-probe-key") { t.Fatalf("effective_probe_key_fingerprint = %q, want hashed user-probe-key", got) } } func TestRuntimeImportServicePersistsSubscriptionMetadataInAccessClosure(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: "deepseek-01"}}, 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"}, CompletionOK: true, CompletionStatus: 200}, } result, err := NewRuntimeImportService(store, host).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, Subscriptions: []SubscriptionTarget{{UserID: "user-42", DurationDays: 30}}, }, }) if err != nil { t.Fatalf("RuntimeImportService.Import() error = %v", err) } closures, err := store.AccessClosures().GetByBatchID(context.Background(), result.BatchID) if err != nil { t.Fatalf("AccessClosures().GetByBatchID() error = %v", err) } if len(closures) != 1 { t.Fatalf("access closures = %d, want 1", len(closures)) } var payload map[string]any if err := json.Unmarshal([]byte(closures[0].DetailsJSON), &payload); err != nil { t.Fatalf("decode access closure details: %v", err) } users, _ := payload["subscription_users"].([]any) if len(users) != 1 || users[0] != "user-42" { t.Fatalf("subscription_users = %#v, want [user-42]", users) } if got, _ := payload["subscription_days"].(float64); int(got) != 30 { t.Fatalf("subscription_days = %v, want 30", got) } if _, ok := payload["probe_api_key"]; ok { t.Fatalf("probe_api_key should be omitted for subscription closure, got %#v", payload["probe_api_key"]) } if got, _ := payload["effective_probe_key_source"].(string); got != "managed_subscription" { t.Fatalf("effective_probe_key_source = %q, want managed_subscription", got) } if got, _ := payload["effective_probe_key_fingerprint"].(string); got != fingerprintSecret("managed-subscription-key") { t.Fatalf("effective_probe_key_fingerprint = %q, want managed-subscription-key fingerprint", got) } } 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 TestRuntimeImportServiceResolvesHostByBaseURL(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"}}, }, gatewayResult: sub2api.GatewayAccessResult{ OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}, CompletionOK: true, CompletionStatus: 200, }, } result, err := NewRuntimeImportService(store, host).Import(context.Background(), RuntimeImportRequest{ 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.BatchID <= 0 { t.Fatalf("BatchID = %d, want positive id", result.BatchID) } } func TestRuntimeImportServiceRejectsUnregisteredHostBaseURL(t *testing.T) { store := openProvisionTestStore(t) defer closeProvisionTestStore(t, store) _, err := NewRuntimeImportService(store, &fakeHostAdapter{}).Import(context.Background(), RuntimeImportRequest{ HostBaseURL: "https://missing.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 || !strings.Contains(err.Error(), `host_id is required for unregistered host_base_url "https://missing.example.com"`) { t.Fatalf("RuntimeImportService.Import() error = %v, want unregistered host_base_url error", err) } } func TestRuntimeImportServiceRejectsHostBaseURLMismatch(t *testing.T) { store := openProvisionTestStore(t) defer closeProvisionTestStore(t, store) seedProvisionHost(t, store, "host-1", "https://sub2api.example.com") _, err := NewRuntimeImportService(store, &fakeHostAdapter{}).Import(context.Background(), RuntimeImportRequest{ HostID: "host-1", HostBaseURL: "https://other.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 || err.Error() != `host "host-1" base_url mismatch: registered=https://sub2api.example.com runtime=https://other.example.com` { t.Fatalf("RuntimeImportService.Import() error = %v, want base_url mismatch", err) } } 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() return testutil.OpenSQLiteStore(t, testutil.SQLiteTestDSN(t, "state.db", true)) } func closeProvisionTestStore(t *testing.T, store *sqlite.DB) { t.Helper() testutil.CloseSQLiteStore(t, store) } 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 }