package app import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "sub2api-cn-relay-manager/internal/pack" "sub2api-cn-relay-manager/internal/provision" "sub2api-cn-relay-manager/internal/store/sqlite" "sub2api-cn-relay-manager/internal/testutil" ) func TestRunReconcileBackgroundSweepCreatesReconcileRunForLatestSuccessfulBatch(t *testing.T) { t.Parallel() server := httptest.NewServer(newBatchImportActionStubServer(t)) defer server.Close() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, hostPK, _ := seedReconcileBackgroundRuntimeImport(t, store, server.URL) if err := runReconcileBackgroundSweep(context.Background(), store, 10*time.Minute, time.Now()); err != nil { t.Fatalf("runReconcileBackgroundSweep() error = %v", err) } providers, err := store.Providers().ListByProviderID(context.Background(), "deepseek") if err != nil { t.Fatalf("Providers().ListByProviderID() error = %v", err) } runs, err := store.ReconcileRuns().GetByProviderIDAndHostID(context.Background(), providers[0].ID, hostPK) if err != nil { t.Fatalf("ReconcileRuns().GetByProviderIDAndHostID() error = %v", err) } if len(runs) != 1 { t.Fatalf("reconcile runs = %d, want 1", len(runs)) } if runs[0].BatchID != batchID { t.Fatalf("reconcile batch_id = %d, want %d", runs[0].BatchID, batchID) } if runs[0].Status == "" { t.Fatal("reconcile status = empty, want persisted result") } } func TestRunReconcileBackgroundSweepSkipsRecentReconcileRun(t *testing.T) { t.Parallel() server := httptest.NewServer(newBatchImportActionStubServer(t)) defer server.Close() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, hostPK, providerPK := seedReconcileBackgroundRuntimeImport(t, store, server.URL) if _, err := store.ReconcileRuns().Create(context.Background(), sqlite.ReconcileRun{ BatchID: batchID, HostID: hostPK, ProviderID: providerPK, Status: "active", SummaryJSON: `{"seed":true}`, }); err != nil { t.Fatalf("ReconcileRuns().Create() error = %v", err) } if err := runReconcileBackgroundSweep(context.Background(), store, 10*time.Minute, time.Now()); err != nil { t.Fatalf("runReconcileBackgroundSweep() error = %v", err) } runs, err := store.ReconcileRuns().GetByProviderIDAndHostID(context.Background(), providerPK, hostPK) if err != nil { t.Fatalf("ReconcileRuns().GetByProviderIDAndHostID() error = %v", err) } if len(runs) != 1 { t.Fatalf("reconcile runs = %d, want 1 recent run only", len(runs)) } } func openReconcileBackgroundTestStore(t *testing.T) *sqlite.DB { t.Helper() store := testutil.OpenSQLiteStore(t, testutil.SQLiteTestDSN(t, "state.db", true)) if _, err := store.SQLDB().Exec("PRAGMA foreign_keys = OFF"); err != nil { t.Fatalf("disable foreign keys pragma error = %v", err) } return store } func seedReconcileBackgroundRuntimeImport(t *testing.T, store *sqlite.DB, baseURL string) (int64, int64, int64) { t.Helper() hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-1", BaseURL: baseURL, HostVersion: "0.1.126", CapabilityProbeJSON: "{}", AuthType: "apikey", AuthToken: "host-token", }) if err != nil { t.Fatalf("Hosts().Create() error = %v", err) } client, err := newSub2APIClient(baseURL, CreateHostAuth{Type: "apikey", Token: "host-token"}) if err != nil { t.Fatalf("newSub2APIClient() error = %v", err) } loadedPack := pack.LoadedPack{ Manifest: pack.Manifest{ PackID: "openai-cn-pack", Version: "1.0.0", Vendor: "OpenAI CN", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x", }, Providers: []pack.ProviderManifest{{ ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.deepseek.example", Platform: "openai", AccountType: "openai", DefaultModels: []string{"kimi-k2.6"}, SmokeTestModel: "kimi-k2.6", GroupTemplate: pack.GroupTemplate{Name: "DeepSeek 默认分组", RateMultiplier: 1}, ChannelTemplate: pack.ChannelTemplate{ Name: "DeepSeek 默认渠道", ModelMapping: map[string]string{"kimi-k2.6": "kimi-k2.6"}, }, PlanTemplate: pack.PlanTemplate{Name: "DeepSeek 默认套餐", Price: 0, ValidityDays: 30, ValidityUnit: "day"}, Import: pack.ImportOptions{SupportsMultiKey: true, SupportsStrict: true, SupportsPartial: true}, }}, Checksum: "checksum-1", } result, err := provision.NewRuntimeImportService(store, client).Import(context.Background(), provision.RuntimeImportRequest{ HostID: "host-1", HostBaseURL: baseURL, Pack: loadedPack, Provider: loadedPack.Providers[0], Mode: provision.ImportModePartial, Keys: []string{"entry-key"}, Access: provision.AccessRequest{ Mode: provision.AccessModeSelfService, ProbeAPIKey: "gateway-key", }, }) if err != nil { t.Fatalf("RuntimeImportService.Import() error = %v", err) } packRow, err := store.Packs().GetByPackID(context.Background(), loadedPack.Manifest.PackID) if err != nil { t.Fatalf("Packs().GetByPackID() error = %v", err) } providerRow, err := store.Providers().GetByPackIDAndProviderID(context.Background(), packRow.ID, loadedPack.Providers[0].ProviderID) if err != nil { t.Fatalf("Providers().GetByPackIDAndProviderID() error = %v", err) } return result.BatchID, hostPK, providerRow.ID } func TestRunReconcileBackgroundSweepRequiresStore(t *testing.T) { t.Parallel() err := runReconcileBackgroundSweep(context.Background(), nil, time.Minute, time.Now()) if err == nil || err.Error() != "store is required" { t.Fatalf("runReconcileBackgroundSweep() error = %v, want store is required", err) } } func TestRunReconcileBackgroundSweepReturnsContextErrorBeforeCandidateRun(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) seedReconcileBackgroundBatch(t, store) ctx, cancel := context.WithCancel(context.Background()) cancel() err := runReconcileBackgroundSweep(ctx, store, time.Minute, time.Now()) if !errors.Is(err, context.Canceled) { t.Fatalf("runReconcileBackgroundSweep() error = %v, want wrapped %v", err, context.Canceled) } } func TestRunReconcileBackgroundSweepReturnsJoinedCandidateErrors(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, _, _ := seedReconcileBackgroundBatch(t, store) err := runReconcileBackgroundSweep(context.Background(), store, time.Minute, time.Now()) if err == nil { t.Fatal("runReconcileBackgroundSweep() error = nil, want candidate failure") } want := fmt.Sprintf("run reconcile for batch %d: access closure not found for batch %d", batchID, batchID) if !strings.Contains(err.Error(), want) { t.Fatalf("runReconcileBackgroundSweep() error = %v, want contains %q", err, want) } } func TestReconcileRunDue(t *testing.T) { t.Parallel() now := time.Date(2026, 5, 23, 10, 0, 0, 0, time.UTC) tests := []struct { name string run *sqlite.ReconcileRun interval time.Duration want bool }{ {name: "nil run", run: nil, interval: time.Minute, want: true}, {name: "non positive interval", run: &sqlite.ReconcileRun{CreatedAt: "2026-05-23 09:59:59"}, interval: 0, want: true}, {name: "invalid timestamp", run: &sqlite.ReconcileRun{CreatedAt: "bad-time"}, interval: time.Minute, want: true}, {name: "recent run not due", run: &sqlite.ReconcileRun{CreatedAt: "2026-05-23 09:59:30"}, interval: time.Minute, want: false}, {name: "old run due", run: &sqlite.ReconcileRun{CreatedAt: "2026-05-23 09:58:00"}, interval: time.Minute, want: true}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := reconcileRunDue(now, tc.run, tc.interval); got != tc.want { t.Fatalf("reconcileRunDue() = %v, want %v", got, tc.want) } }) } } func TestParseAccessClosureDetailsReturnsEmptyMapForInvalidJSON(t *testing.T) { t.Parallel() got := parseAccessClosureDetails("{") if len(got) != 0 { t.Fatalf("parseAccessClosureDetails() = %#v, want empty map", got) } } func TestParseJSONStringArrayAndParseJSONInt(t *testing.T) { t.Parallel() values := parseJSONStringArray([]any{" user-1 ", 42, "", "user-2"}) if len(values) != 2 || values[0] != "user-1" || values[1] != "user-2" { t.Fatalf("parseJSONStringArray() = %v, want [user-1 user-2]", values) } if got := parseJSONStringArray("wrong-type"); got != nil { t.Fatalf("parseJSONStringArray(wrong-type) = %v, want nil", got) } if got := parseJSONInt(float64(30)); got != 30 { t.Fatalf("parseJSONInt(float64) = %d, want 30", got) } if got := parseJSONInt(15); got != 15 { t.Fatalf("parseJSONInt(int) = %d, want 15", got) } if got := parseJSONInt("30"); got != 0 { t.Fatalf("parseJSONInt(string) = %d, want 0", got) } } func TestStoredLoadedPackFallsBackToColumns(t *testing.T) { t.Parallel() loaded, err := storedLoadedPack(sqlite.Pack{ PackID: "openai-cn-pack", Version: "1.0.0", Checksum: "checksum-1", Vendor: "OpenAI CN", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x", ManifestJSON: "{}", }) if err != nil { t.Fatalf("storedLoadedPack() error = %v", err) } if loaded.Manifest.PackID != "openai-cn-pack" || loaded.Manifest.TargetHost != "sub2api" || loaded.Checksum != "checksum-1" { t.Fatalf("storedLoadedPack() = %+v, want fallback fields populated", loaded) } } func TestStoredLoadedPackRejectsInvalidManifestJSON(t *testing.T) { t.Parallel() _, err := storedLoadedPack(sqlite.Pack{ManifestJSON: "{"}) if err == nil || !strings.Contains(err.Error(), "decode stored pack manifest") { t.Fatalf("storedLoadedPack() error = %v, want decode stored pack manifest", err) } } func TestStoredProviderManifestFallsBackToColumns(t *testing.T) { t.Parallel() provider, err := storedProviderManifest(sqlite.Provider{ ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.example.com", Platform: "openai", AccountType: "openai", SmokeTestModel: "deepseek-chat", ManifestJSON: "{}", }) if err != nil { t.Fatalf("storedProviderManifest() error = %v", err) } if provider.ProviderID != "deepseek" || provider.AccountType != "openai" || provider.SmokeTestModel != "deepseek-chat" { t.Fatalf("storedProviderManifest() = %+v, want fallback fields populated", provider) } } func TestStoredProviderManifestRejectsInvalidManifestJSON(t *testing.T) { t.Parallel() _, err := storedProviderManifest(sqlite.Provider{ManifestJSON: "{"}) if err == nil || !strings.Contains(err.Error(), "decode stored provider manifest") { t.Fatalf("storedProviderManifest() error = %v, want decode stored provider manifest", err) } } func TestResolveManagedResourceHostIDByBatch(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, hostPK, _ := seedReconcileBackgroundBatch(t, store) if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{ BatchID: batchID, HostID: hostPK, ResourceType: "group", HostResourceID: "group_1", ResourceName: "group one", }); err != nil { t.Fatalf("ManagedResources().Create() error = %v", err) } groupID, err := resolveManagedResourceHostIDByBatch(context.Background(), store, batchID, "group") if err != nil { t.Fatalf("resolveManagedResourceHostIDByBatch() error = %v", err) } if groupID != "group_1" { t.Fatalf("groupID = %q, want group_1", groupID) } if _, err := resolveManagedResourceHostIDByBatch(context.Background(), store, batchID, "plan"); err == nil || err.Error() != fmt.Sprintf("managed resource %q not found for batch %d", "plan", batchID) { t.Fatalf("resolveManagedResourceHostIDByBatch(plan) error = %v, want missing resource error", err) } } func TestReconcileProbeAPIKeySelfService(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, _, _ := seedReconcileBackgroundBatch(t, store) hostRow := mustGetBackgroundHost(t, store) tests := []struct { name string record sqlite.AccessClosureRecord wantAPIKey string wantErr string }{ { name: "prefers access api key", record: sqlite.AccessClosureRecord{ BatchID: batchID, ClosureType: provision.AccessModeSelfService, Status: "self_service_ready", DetailsJSON: `{"access_api_key":" access-key ","probe_api_key":"probe-key"}`, }, wantAPIKey: "access-key", }, { name: "falls back to probe api key", record: sqlite.AccessClosureRecord{ BatchID: batchID, ClosureType: provision.AccessModeSelfService, Status: "self_service_ready", DetailsJSON: `{"probe_api_key":" probe-key "}`, }, wantAPIKey: "probe-key", }, { name: "requires api key", record: sqlite.AccessClosureRecord{ BatchID: batchID, ClosureType: provision.AccessModeSelfService, Status: "self_service_ready", DetailsJSON: `{}`, }, wantErr: "self_service access closure missing probe api key", }, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() got, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{tc.record}) if tc.wantErr != "" { if err == nil || err.Error() != tc.wantErr { t.Fatalf("reconcileProbeAPIKey() error = %v, want %q", err, tc.wantErr) } return } if err != nil { t.Fatalf("reconcileProbeAPIKey() error = %v", err) } if got != tc.wantAPIKey { t.Fatalf("reconcileProbeAPIKey() = %q, want %q", got, tc.wantAPIKey) } }) } } func TestReconcileProbeAPIKeyRejectsMissingSubscriptionUsers(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, _, _ := seedReconcileBackgroundBatch(t, store) hostRow := mustGetBackgroundHost(t, store) _, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{ BatchID: batchID, ClosureType: provision.AccessModeSubscription, Status: "subscription_ready", DetailsJSON: `{}`, }}) if err == nil || err.Error() != "subscription access closure missing subscription_users" { t.Fatalf("reconcileProbeAPIKey() error = %v, want missing subscription_users", err) } } func TestReconcileProbeAPIKeyRejectsUnsupportedClosureType(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, _, _ := seedReconcileBackgroundBatch(t, store) hostRow := mustGetBackgroundHost(t, store) _, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{ BatchID: batchID, ClosureType: "other", Status: "unknown", }}) if err == nil || err.Error() != `unsupported access closure type "other"` { t.Fatalf("reconcileProbeAPIKey() error = %v, want unsupported type", err) } } func TestReconcileProbeAPIKeyRequiresAccessClosure(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) batchID, _, _ := seedReconcileBackgroundBatch(t, store) hostRow := mustGetBackgroundHost(t, store) _, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, nil) if err == nil || err.Error() != fmt.Sprintf("access closure not found for batch %d", batchID) { t.Fatalf("reconcileProbeAPIKey() error = %v, want missing access closure", err) } } func TestReconcileProbeAPIKeySubscriptionSuccess(t *testing.T) { t.Parallel() var assignCalls int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/users?"): writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"items": []map[string]any{}}}) case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/users": writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 84, "email": "relay-sub-user-1@sub2api.local"}}) case r.Method == http.MethodPut && r.URL.Path == "/api/v1/admin/users/84": writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 84}}) case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/users/84/balance": writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 84}}) case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/subscriptions/assign": assignCalls++ writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 401}}) case r.Method == http.MethodPost && r.URL.Path == "/api/v1/auth/login": writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"access_token": "user-jwt"}}) case r.Method == http.MethodPost && r.URL.Path == "/api/v1/keys": writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 501, "key": "sk-relay-key", "name": "managed-key"}}) case r.Method == http.MethodPut && r.URL.Path == "/api/v1/admin/api-keys/501": writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"api_key": map[string]any{"id": 501}}}) default: http.NotFound(w, r) } })) defer server.Close() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-subscription", BaseURL: server.URL, HostVersion: "0.1.126", CapabilityProbeJSON: "{}", AuthType: "bearer", AuthToken: "admin-token", }) if err != nil { t.Fatalf("Hosts().Create() error = %v", err) } packPK := createBackgroundPack(t, store) providerPK := createBackgroundProvider(t, store, packPK) batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{ HostID: hostPK, PackID: packPK, ProviderID: providerPK, Mode: provision.ImportModePartial, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSubscriptionReady, }) if err != nil { t.Fatalf("ImportBatches().Create() error = %v", err) } if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{ BatchID: batchID, HostID: hostPK, ResourceType: "group", HostResourceID: "101", ResourceName: "group-101", }); err != nil { t.Fatalf("ManagedResources().Create(group) error = %v", err) } hostRow := sqlite.Host{HostID: "host-subscription", BaseURL: server.URL, AuthType: "bearer", AuthToken: "admin-token"} got, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{ BatchID: batchID, ClosureType: provision.AccessModeSubscription, Status: provision.AccessStatusSubscriptionReady, DetailsJSON: `{"subscription_users":["crm-user-1"],"subscription_days":0}`, }}) if err != nil { t.Fatalf("reconcileProbeAPIKey() error = %v", err) } if !strings.HasPrefix(got, "sk-relay-") { t.Fatalf("reconcileProbeAPIKey() = %q, want sk-relay-*", got) } if assignCalls != 1 { t.Fatalf("subscription assign calls = %d, want 1 (EnsureSubscriptionAccess only)", assignCalls) } } func TestReconcileProbeAPIKeySubscriptionRequiresHostAuth(t *testing.T) { t.Parallel() store := openReconcileBackgroundTestStore(t) defer closeAppTestStore(t, store) hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-subscription", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", CapabilityProbeJSON: "{}", AuthType: "bearer", AuthToken: "admin-token", }) if err != nil { t.Fatalf("Hosts().Create() error = %v", err) } packPK := createBackgroundPack(t, store) providerPK := createBackgroundProvider(t, store, packPK) batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{ HostID: hostPK, PackID: packPK, ProviderID: providerPK, Mode: provision.ImportModePartial, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSubscriptionReady, }) if err != nil { t.Fatalf("ImportBatches().Create() error = %v", err) } if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{ BatchID: batchID, HostID: hostPK, ResourceType: "group", HostResourceID: "101", ResourceName: "group-101", }); err != nil { t.Fatalf("ManagedResources().Create(group) error = %v", err) } _, err = reconcileProbeAPIKey(context.Background(), store, sqlite.Host{BaseURL: "https://sub2api.example.com", AuthType: "bearer"}, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{ BatchID: batchID, ClosureType: provision.AccessModeSubscription, Status: provision.AccessStatusSubscriptionReady, DetailsJSON: `{"subscription_users":["crm-user-1"],"subscription_days":30}`, }}) if err == nil || !strings.Contains(err.Error(), "auth.token is required") { t.Fatalf("reconcileProbeAPIKey() error = %v, want auth.token is required", err) } } func createBackgroundPack(t *testing.T, store *sqlite.DB) int64 { t.Helper() packPK, err := store.Packs().Create(context.Background(), sqlite.Pack{ PackID: "openai-cn-pack", Version: "1.0.0", Checksum: "checksum-1", Vendor: "OpenAI CN", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x", ManifestJSON: `{"pack_id":"openai-cn-pack","version":"1.0.0","target_host":"sub2api"}`, }) if err != nil { t.Fatalf("Packs().Create() error = %v", err) } return packPK } func createBackgroundProvider(t *testing.T, store *sqlite.DB, packPK int64) int64 { t.Helper() providerPK, err := store.Providers().Create(context.Background(), sqlite.Provider{ PackID: packPK, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.example.com", Platform: "openai", AccountType: "openai", SmokeTestModel: "deepseek-chat", ManifestJSON: `{"provider_id":"deepseek","base_url":"https://api.example.com","platform":"openai","account_type":"openai","smoke_test_model":"deepseek-chat"}`, }) if err != nil { t.Fatalf("Providers().Create() error = %v", err) } return providerPK } func mustGetBackgroundHost(t *testing.T, store *sqlite.DB) sqlite.Host { t.Helper() host, err := store.Hosts().GetByHostID(context.Background(), "host-1") if err != nil { t.Fatalf("Hosts().GetByHostID() error = %v", err) } return host } func seedReconcileBackgroundBatch(t *testing.T, store *sqlite.DB) (int64, int64, int64) { t.Helper() hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-1", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", CapabilityProbeJSON: "{}", AuthType: "apikey", AuthToken: "host-token", }) if err != nil { t.Fatalf("Hosts().Create() error = %v", err) } packPK, err := store.Packs().Create(context.Background(), sqlite.Pack{ PackID: "openai-cn-pack", Version: "1.0.0", Checksum: "checksum-1", Vendor: "OpenAI CN", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x", ManifestJSON: `{"pack_id":"openai-cn-pack","version":"1.0.0","target_host":"sub2api"}`, }) if err != nil { t.Fatalf("Packs().Create() error = %v", err) } providerPK, err := store.Providers().Create(context.Background(), sqlite.Provider{ PackID: packPK, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.example.com", Platform: "openai", AccountType: "openai", SmokeTestModel: "deepseek-chat", ManifestJSON: `{"provider_id":"deepseek","base_url":"https://api.example.com","platform":"openai","account_type":"openai","smoke_test_model":"deepseek-chat"}`, }) if err != nil { t.Fatalf("Providers().Create() error = %v", err) } batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{ HostID: hostPK, PackID: packPK, ProviderID: providerPK, Mode: provision.ImportModePartial, BatchStatus: "partially_succeeded", AccessStatus: "self_service_ready", }) if err != nil { t.Fatalf("ImportBatches().Create() error = %v", err) } return batchID, hostPK, providerPK }