package app import ( "context" "database/sql" "strings" "testing" "sub2api-cn-relay-manager/internal/batch" "sub2api-cn-relay-manager/internal/host/sub2api" "sub2api-cn-relay-manager/internal/provision" "sub2api-cn-relay-manager/internal/store/sqlite" ) func TestBatchImportResumeJobNameAndParseJSONStringList(t *testing.T) { t.Parallel() if got := (batchImportResumeJob{}).Name(); got != "batch import runtime scheduler" { t.Fatalf("batchImportResumeJob.Name() = %q, want batch import runtime scheduler", got) } values := parseJSONStringList(`[" user-1 ","user-2"]`) if len(values) != 2 || values[0] != " user-1 " || values[1] != "user-2" { t.Fatalf("parseJSONStringList() = %v, want raw decoded values", values) } if got := parseJSONStringList("{"); len(got) != 0 { t.Fatalf("parseJSONStringList(invalid) = %v, want empty", got) } } func TestBatchImportResumeJobRunAndReconcileSweepJobName(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) mustCreateAppImportRun(t, store, sqlite.ImportRun{ RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "completed", }) job := batchImportResumeJob{sqliteDSN: appTestDSN(t, store)} if err := job.Run(context.Background()); err != nil { t.Fatalf("batchImportResumeJob.Run() error = %v", err) } if got := (reconcileSweepJob{}).Name(); got != "reconcile background scheduler" { t.Fatalf("reconcileSweepJob.Name() = %q, want reconcile background scheduler", got) } } func TestDefaultBackgroundSchedulersAndNewActionSet(t *testing.T) { t.Parallel() schedulers := defaultBackgroundSchedulers() if schedulers.runBatchImport == nil || schedulers.runReconcile == nil { t.Fatalf("defaultBackgroundSchedulers() = %+v, want non-nil functions", schedulers) } actions := NewActionSet("file:/tmp/nonexistent.db") if actions.CreateBatchImportRun == nil || actions.ListBatchImportRuns == nil || actions.GetBatchImportRun == nil || actions.ListBatchImportRunItems == nil || actions.GetBatchImportRunItem == nil { t.Fatalf("NewActionSet() returned nil batch actions: %+v", actions) } if actions.CreateHost == nil || actions.ListPacks == nil || actions.GetPack == nil || actions.ListPackProviders == nil { t.Fatalf("NewActionSet() returned nil app actions: %+v", actions) } } func TestCreateHostAuthFromLegacyFields(t *testing.T) { t.Parallel() if got := createHostAuthFromLegacyFields(" api-key ", " bearer-token "); got.Type != "bearer" || got.Token != "bearer-token" { t.Fatalf("createHostAuthFromLegacyFields() = %+v, want bearer token", got) } if got := createHostAuthFromLegacyFields(" api-key ", ""); got.Type != "apikey" || got.Token != "api-key" { t.Fatalf("createHostAuthFromLegacyFields() = %+v, want apikey", got) } } func TestHostRecordToInfoAndPackRecordToInfo(t *testing.T) { t.Parallel() hostInfo := hostRecordToInfo(sqlite.Host{ HostID: "host-1", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", AuthType: "apikey", CapabilityProbeJSON: `{"groups":true,"channels":true,"plans":true,"accounts":true,"account_test":true,"account_models":true,"subscriptions":true}`, }) if hostInfo.Status != "supported" || hostInfo.Capabilities == nil || !hostInfo.Capabilities.Subscriptions { t.Fatalf("hostRecordToInfo() = %+v, want supported capabilities", hostInfo) } hostInfo = hostRecordToInfo(sqlite.Host{ HostID: "host-2", BaseURL: "https://bad.example.com", CapabilityProbeJSON: "{", }) if hostInfo.Capabilities != nil || hostInfo.Status != "" { t.Fatalf("hostRecordToInfo(invalid json) = %+v, want nil capabilities and empty status", hostInfo) } packInfo := packRecordToInfo(sqlite.Pack{ PackID: "openai-cn-pack", Version: "1.0.0", Vendor: "OpenAI CN", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x", }) if packInfo.PackID != "openai-cn-pack" || packInfo.TargetHost != "sub2api" { t.Fatalf("packRecordToInfo() = %+v, want projected pack info", packInfo) } } func TestDeriveAccessStatusAndDefaultPositiveInt(t *testing.T) { t.Parallel() if got := deriveAccessStatus(sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true, CompletionOK: true}); got != provision.AccessStatusSubscriptionReady { t.Fatalf("deriveAccessStatus(ready) = %q, want %q", got, provision.AccessStatusSubscriptionReady) } if got := deriveAccessStatus(sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true, CompletionOK: false}); got != provision.AccessStatusBroken { t.Fatalf("deriveAccessStatus(broken) = %q, want %q", got, provision.AccessStatusBroken) } if got := defaultPositiveInt(3, 9); got != 3 { t.Fatalf("defaultPositiveInt(3, 9) = %d, want 3", got) } if got := defaultPositiveInt(0, 9); got != 9 { t.Fatalf("defaultPositiveInt(0, 9) = %d, want 9", got) } } func TestMatchesItemFilters(t *testing.T) { t.Parallel() view := batch.ItemSummaryProjection{ ItemID: "item-1", BaseURL: "https://kimi.example.com/v1", ProviderID: "kimi-a7m", CurrentStage: "done", ConfirmationStatus: "advisory", AccessStatus: "active", MatchedAccountState: "active", AccountResolution: "reused", AdvisoryMessages: []string{"warning"}, } hasWarning := true if !matchesItemFilters(view, ListBatchImportRunItemsRequest{ CurrentStage: "done", ConfirmationStatus: "advisory", AccessStatus: "active", ProviderID: "kimi-a7m", MatchedAccountState: "active", AccountResolution: "reused", Query: "kimi", HasWarning: &hasWarning, }) { t.Fatal("matchesItemFilters() = false, want match") } noWarning := false if matchesItemFilters(view, ListBatchImportRunItemsRequest{HasWarning: &noWarning}) { t.Fatal("matchesItemFilters(has_warning=false) = true, want false") } if matchesItemFilters(view, ListBatchImportRunItemsRequest{Query: "other"}) { t.Fatal("matchesItemFilters(query=other) = true, want false") } } func TestBuildGetBatchImportRunActionAndGetItemAction(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) mustCreateAppImportRun(t, store, sqlite.ImportRun{ RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running", }) mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:00' WHERE run_id = 'run-1'`) mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", BaseURL: "https://kimi.example.com/v1", ProviderID: "kimi-a7m", APIKeyFingerprint: "sha256:1", CurrentStage: "done", ConfirmationStatus: "confirmed", AccessStatus: "active", MatchedAccountState: "active", AccountResolution: "reused", AdvisoryMessagesJSON: `["gateway_warmup_retry_succeeded"]`, }) action := buildGetBatchImportRunAction(appTestDSN(t, store)) run, err := action(context.Background(), "run-1") if err != nil { t.Fatalf("buildGetBatchImportRunAction() error = %v", err) } if run.RunID != "run-1" { t.Fatalf("run.RunID = %q, want run-1", run.RunID) } if _, err := action(context.Background(), "missing"); err == nil || err.Error() != "run not found: missing" { t.Fatalf("missing run error = %v, want run not found", err) } itemAction := buildGetBatchImportRunItemAction(appTestDSN(t, store)) item, err := itemAction(context.Background(), GetBatchImportRunItemRequest{RunID: "run-1", ItemID: "item-1"}) if err != nil { t.Fatalf("buildGetBatchImportRunItemAction() error = %v", err) } if item.ItemID != "item-1" || item.AccountResolution != "reused" { t.Fatalf("item = %+v, want projected item", item) } if _, err := itemAction(context.Background(), GetBatchImportRunItemRequest{RunID: "run-2", ItemID: "item-1"}); err == nil || err.Error() != "item not found in run run-2" { t.Fatalf("wrong run error = %v, want item not found in run", err) } } func TestResolveProvidersForQueryAndLatestAccessStatus(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) host1, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-1", BaseURL: "https://one.example.com", HostVersion: "0.1.126", AuthToken: "token-1", }) if err != nil { t.Fatalf("Hosts().Create(host1) error = %v", err) } host2, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-2", BaseURL: "https://two.example.com", HostVersion: "0.1.126", AuthToken: "token-2", }) if err != nil { t.Fatalf("Hosts().Create(host2) error = %v", err) } packID, err := store.Packs().Create(context.Background(), sqlite.Pack{ PackID: "openai-cn-pack", Version: "1.0.0", Checksum: "checksum-1", }) if err != nil { t.Fatalf("Packs().Create() error = %v", err) } providerID, err := store.Providers().Create(context.Background(), sqlite.Provider{ PackID: packID, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.example.com", Platform: "openai", }) if err != nil { t.Fatalf("Providers().Create() error = %v", err) } batch1, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{ HostID: host1, PackID: packID, ProviderID: providerID, Mode: provision.ImportModePartial, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSelfServiceReady, }) if err != nil { t.Fatalf("ImportBatches().Create(batch1) error = %v", err) } batch2, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{ HostID: host2, PackID: packID, ProviderID: providerID, Mode: provision.ImportModeStrict, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSubscriptionReady, }) if err != nil { t.Fatalf("ImportBatches().Create(batch2) error = %v", err) } if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{ BatchID: batch1, ClosureType: provision.AccessModeSelfService, Status: provision.AccessStatusSelfServiceReady, }); err != nil { t.Fatalf("AccessClosures().Create(batch1) error = %v", err) } if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{ BatchID: batch2, ClosureType: provision.AccessModeSubscription, Status: provision.AccessStatusSubscriptionReady, }); err != nil { t.Fatalf("AccessClosures().Create(batch2) error = %v", err) } if _, err := resolveProvidersForQuery(context.Background(), nil, ProviderQueryRequest{}); err == nil || err.Error() != "store is required" { t.Fatalf("resolveProvidersForQuery(nil store) error = %v, want store is required", err) } if _, err := resolveProvidersForQuery(context.Background(), store, ProviderQueryRequest{}); err == nil || err.Error() != "provider_id is required" { t.Fatalf("resolveProvidersForQuery(missing provider) error = %v, want provider_id is required", err) } providers, err := resolveProvidersForQuery(context.Background(), store, ProviderQueryRequest{ProviderID: "deepseek", PackID: "openai-cn-pack"}) if err != nil { t.Fatalf("resolveProvidersForQuery() error = %v", err) } if len(providers) != 1 || providers[0].ProviderID != "deepseek" { t.Fatalf("resolveProvidersForQuery() = %+v, want deepseek provider", providers) } if _, err := resolveLatestAccessStatus(context.Background(), nil, sqlite.Provider{}, ""); err == nil || err.Error() != "store is required" { t.Fatalf("resolveLatestAccessStatus(nil store) error = %v, want store is required", err) } status, err := resolveLatestAccessStatus(context.Background(), store, providers[0], "host-1") if err != nil { t.Fatalf("resolveLatestAccessStatus(host-1) error = %v", err) } if status != provision.AccessStatusSelfServiceReady { t.Fatalf("resolveLatestAccessStatus(host-1) = %q, want %q", status, provision.AccessStatusSelfServiceReady) } if _, err := resolveLatestAccessStatus(context.Background(), store, providers[0], ""); err == nil || err.Error() != "provider exists on multiple hosts; host_id is required" { t.Fatalf("resolveLatestAccessStatus(multi-host) error = %v, want multi-host error", err) } } func TestBuildListBatchImportRunsActionAndItemsAction(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) mustCreateAppImportRun(t, store, sqlite.ImportRun{ RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running", TotalItems: 1, WarningItems: 1, ActiveItems: 1, BrokenItems: 0, DegradedItems: 0, }) mustCreateAppImportRun(t, store, sqlite.ImportRun{ RunID: "run-2", HostID: "host-1", Mode: "strict", AccessMode: "subscription", State: "completed", }) mustCreateAppImportRun(t, store, sqlite.ImportRun{ RunID: "run-3", HostID: "host-1", Mode: "strict", AccessMode: "subscription", State: "completed", }) mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:03' WHERE run_id = 'run-1'`) mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:02' WHERE run_id = 'run-2'`) mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:01' WHERE run_id = 'run-3'`) mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", BaseURL: "https://kimi.example.com/v1", ProviderID: "kimi-a7m", APIKeyFingerprint: "sha256:1", CurrentStage: "done", ConfirmationStatus: "advisory", AccessStatus: "active", MatchedAccountState: "active", AccountResolution: "reused", AdvisoryMessagesJSON: `["warning"]`, }) mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{ ItemID: "item-2", RunID: "run-2", BaseURL: "https://other.example.com/v1", ProviderID: "deepseek", APIKeyFingerprint: "sha256:2", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "broken", MatchedAccountState: "broken", AccountResolution: "created", AdvisoryMessagesJSON: `[]`, }) mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{ ItemID: "item-3", RunID: "run-3", BaseURL: "https://other.example.com/v1", ProviderID: "deepseek-backup", APIKeyFingerprint: "sha256:3", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "active", MatchedAccountState: "active", AccountResolution: "reused", AdvisoryMessagesJSON: `[]`, }) listRuns := buildListBatchImportRunsAction(appTestDSN(t, store)) runs, err := listRuns(context.Background(), ListBatchImportRunsRequest{ State: "completed", Query: "other.example.com", Limit: 1, }) if err != nil { t.Fatalf("buildListBatchImportRunsAction() error = %v", err) } if len(runs.Runs) != 1 || runs.Runs[0].RunID != "run-2" { t.Fatalf("runs = %+v, want [run-2]", runs.Runs) } if runs.NextCursor == nil || *runs.NextCursor != "run-3" { t.Fatalf("runs.NextCursor = %v, want run-3", runs.NextCursor) } listItems := buildListBatchImportRunItemsAction(appTestDSN(t, store)) hasWarning := true items, err := listItems(context.Background(), ListBatchImportRunItemsRequest{ RunID: "run-1", HasWarning: &hasWarning, Query: "kimi", Limit: 10, }) if err != nil { t.Fatalf("buildListBatchImportRunItemsAction() error = %v", err) } if len(items.Items) != 1 || items.Items[0].ItemID != "item-1" { t.Fatalf("items = %+v, want [item-1]", items.Items) } if items.NextCursor != nil { t.Fatalf("items.NextCursor = %v, want nil", items.NextCursor) } if _, err := listItems(context.Background(), ListBatchImportRunItemsRequest{RunID: "missing"}); err == nil || err.Error() != "run not found: missing" { t.Fatalf("missing items run error = %v, want run not found", err) } } func appTestDSN(t *testing.T, store *sqlite.DB) string { t.Helper() row := store.SQLDB().QueryRow(`PRAGMA database_list`) var seq int var name string var file string if err := row.Scan(&seq, &name, &file); err != nil { t.Fatalf("PRAGMA database_list scan error = %v", err) } return "file:" + file + "?_busy_timeout=5000&_pragma=foreign_keys(0)" } func mustCreateAppImportRun(t *testing.T, store *sqlite.DB, run sqlite.ImportRun) { t.Helper() if err := store.ImportRuns().Create(context.Background(), run); err != nil { t.Fatalf("ImportRuns().Create() error = %v", err) } } func mustCreateAppImportRunItem(t *testing.T, store *sqlite.DB, item sqlite.ImportRunItem) { t.Helper() if err := store.ImportRunItems().Create(context.Background(), item); err != nil { t.Fatalf("ImportRunItems().Create() error = %v", err) } } func mustExecSQL(t *testing.T, store *sqlite.DB, query string, args ...any) { t.Helper() if _, err := store.SQLDB().Exec(query, args...); err != nil { t.Fatalf("Exec(%q) error = %v", query, err) } } func TestResolveManagedHostAndNewSub2APIClient(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) if _, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-1", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", AuthType: "", AuthToken: "host-token", }); err != nil { t.Fatalf("Hosts().Create() error = %v", err) } hostRow, client, err := resolveManagedHost(context.Background(), store, "host-1", "", CreateHostAuth{}) if err != nil { t.Fatalf("resolveManagedHost() error = %v", err) } if hostRow.HostID != "host-1" || client == nil { t.Fatalf("resolveManagedHost() = (%+v, %v), want host-1 and client", hostRow, client) } if _, _, err := resolveManagedHost(context.Background(), store, "host-1", "https://other.example.com", CreateHostAuth{}); err == nil || !strings.Contains(err.Error(), `host "host-1" base_url mismatch`) { t.Fatalf("resolveManagedHost(mismatch) error = %v, want mismatch", err) } if _, _, err := resolveManagedHost(context.Background(), store, "", "", CreateHostAuth{}); err == nil || err.Error() != "host_id is required" { t.Fatalf("resolveManagedHost(empty) error = %v, want host_id is required", err) } if auth := authFromStoredHost(sqlite.Host{AuthType: "", AuthToken: " token "}); auth.Type != "apikey" || auth.Token != "token" { t.Fatalf("authFromStoredHost(default) = %+v, want apikey/token", auth) } if _, err := newSub2APIClient("https://sub2api.example.com", CreateHostAuth{Type: "other", Token: "t"}); err == nil || !strings.Contains(err.Error(), `unsupported auth type "other"`) { t.Fatalf("newSub2APIClient(unsupported) error = %v, want unsupported auth type", err) } if _, err := newSub2APIClient("https://sub2api.example.com", CreateHostAuth{Type: "apikey"}); err == nil || !strings.Contains(err.Error(), "auth.token is required") { t.Fatalf("newSub2APIClient(missing token) error = %v, want auth.token is required", err) } } func TestHandlerWrappersForPackAndHostRoutes(t *testing.T) { t.Parallel() t.Run("handleListPacks returns empty array", func(t *testing.T) { t.Parallel() req := httptestRequest(t, "GET", "/packs", map[string]any{}, "") rec := &responseRecorder{header: map[string][]string{}} handleListPacks(rec, req, func(context.Context) ([]PackInfo, error) { return nil, nil }) assertStatusCode(t, rec, 200) packs, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "packs") if !ok || len(packs) != 0 { t.Fatalf("packs = %#v, want empty array", packs) } }) t.Run("handleGetPack requires pack id", func(t *testing.T) { t.Parallel() req := httptestRequest(t, "GET", "/packs/", map[string]any{}, "") rec := &responseRecorder{header: map[string][]string{}} handleGetPack(rec, req, func(context.Context, string) (PackInfo, error) { return PackInfo{}, nil }) assertStatusCode(t, rec, 400) assertJSONContains(t, rec.Body().Bytes(), "error.message", "pack_id is required") }) t.Run("handleGetPack returns payload", func(t *testing.T) { t.Parallel() req := httptestRequest(t, "GET", "/packs/openai-cn-pack", map[string]any{}, "") req.SetPathValue("packID", "openai-cn-pack") rec := &responseRecorder{header: map[string][]string{}} handleGetPack(rec, req, func(_ context.Context, packID string) (PackInfo, error) { if packID != "openai-cn-pack" { t.Fatalf("packID = %q, want openai-cn-pack", packID) } return PackInfo{PackID: "openai-cn-pack", Version: "1.0.0"}, nil }) assertStatusCode(t, rec, 200) assertJSONContains(t, rec.Body().Bytes(), "pack_id", "openai-cn-pack") }) t.Run("handleListPackProviders returns array", func(t *testing.T) { t.Parallel() req := httptestRequest(t, "GET", "/packs/openai-cn-pack/providers", map[string]any{}, "") req.SetPathValue("packID", "openai-cn-pack") rec := &responseRecorder{header: map[string][]string{}} handleListPackProviders(rec, req, func(_ context.Context, packID string) ([]PackProviderInfo, error) { if packID != "openai-cn-pack" { t.Fatalf("packID = %q, want openai-cn-pack", packID) } return nil, nil }) assertStatusCode(t, rec, 200) providers, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "providers") if !ok || len(providers) != 0 { t.Fatalf("providers = %#v, want empty array", providers) } }) t.Run("handleCreateHost decodes request", func(t *testing.T) { t.Parallel() req := httptestRequest(t, "POST", "/hosts", map[string]any{ "name": "host-1", "base_url": "https://sub2api.example.com", "auth": map[string]any{"type": "apikey", "token": "host-token"}, }, "") rec := &responseRecorder{header: map[string][]string{}} handleCreateHost(rec, req, func(_ context.Context, req CreateHostRequest) (HostInfo, error) { if req.BaseURL != "https://sub2api.example.com" || req.Auth.Token != "host-token" { t.Fatalf("request = %+v, want decoded create host request", req) } return HostInfo{HostID: "host-1", BaseURL: req.BaseURL}, nil }) assertStatusCode(t, rec, 200) assertJSONContains(t, rec.Body().Bytes(), "host_id", "host-1") }) t.Run("handleProbeHost requires host id", func(t *testing.T) { t.Parallel() req := httptestRequest(t, "POST", "/hosts/probe", map[string]any{ "auth": map[string]any{"type": "apikey", "token": "host-token"}, }, "") rec := &responseRecorder{header: map[string][]string{}} handleProbeHost(rec, req, func(context.Context, ProbeHostRequest) (HostInfo, error) { return HostInfo{}, nil }) assertStatusCode(t, rec, 400) assertJSONContains(t, rec.Body().Bytes(), "error.message", "host_id is required") }) } func TestBuildListBatchImportRunItemsActionCursor(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) mustCreateAppImportRun(t, store, sqlite.ImportRun{ RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running", }) mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:00' WHERE run_id = 'run-1'`) mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", BaseURL: "https://a.example.com/v1", ProviderID: "a", APIKeyFingerprint: "sha256:a", CurrentStage: "done", ConfirmationStatus: "confirmed", AccessStatus: "active", MatchedAccountState: "active", AccountResolution: "created", }) mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{ ItemID: "item-2", RunID: "run-1", BaseURL: "https://b.example.com/v1", ProviderID: "b", APIKeyFingerprint: "sha256:b", CurrentStage: "done", ConfirmationStatus: "confirmed", AccessStatus: "active", MatchedAccountState: "active", AccountResolution: "created", }) action := buildListBatchImportRunItemsAction(appTestDSN(t, store)) result, err := action(context.Background(), ListBatchImportRunItemsRequest{ RunID: "run-1", Cursor: "item-1", Limit: 1, }) if err != nil { t.Fatalf("buildListBatchImportRunItemsAction(cursor) error = %v", err) } if len(result.Items) != 1 || result.Items[0].ItemID != "item-2" { t.Fatalf("result.Items = %+v, want [item-2]", result.Items) } } func TestBuildListBatchImportRunsActionCursorAndDefaults(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) mustCreateAppImportRun(t, store, sqlite.ImportRun{RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running"}) mustCreateAppImportRun(t, store, sqlite.ImportRun{RunID: "run-2", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running"}) mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:02' WHERE run_id = 'run-1'`) mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:01' WHERE run_id = 'run-2'`) action := buildListBatchImportRunsAction(appTestDSN(t, store)) result, err := action(context.Background(), ListBatchImportRunsRequest{Cursor: "run-1", Limit: -1}) if err != nil { t.Fatalf("buildListBatchImportRunsAction(cursor) error = %v", err) } if len(result.Runs) != 1 || result.Runs[0].RunID != "run-2" { t.Fatalf("result.Runs = %+v, want [run-2]", result.Runs) } } func TestBuildGetBatchImportRunActionPropagatesDBError(t *testing.T) { t.Parallel() action := buildGetBatchImportRunAction("file:/definitely-missing-path/does-not-exist.db?mode=ro") if _, err := action(context.Background(), "run-1"); err == nil { t.Fatal("buildGetBatchImportRunAction() error = nil, want open db error") } } func TestBuildListBatchImportRunItemsActionPropagatesDBError(t *testing.T) { t.Parallel() action := buildListBatchImportRunItemsAction("file:/definitely-missing-path/does-not-exist.db?mode=ro") if _, err := action(context.Background(), ListBatchImportRunItemsRequest{RunID: "run-1"}); err == nil { t.Fatal("buildListBatchImportRunItemsAction() error = nil, want open db error") } } func TestBuildListBatchImportRunsActionPropagatesDBError(t *testing.T) { t.Parallel() action := buildListBatchImportRunsAction("file:/definitely-missing-path/does-not-exist.db?mode=ro") if _, err := action(context.Background(), ListBatchImportRunsRequest{}); err == nil { t.Fatal("buildListBatchImportRunsAction() error = nil, want open db error") } } func TestBuildGetBatchImportRunItemActionPropagatesMissingItem(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) action := buildGetBatchImportRunItemAction(appTestDSN(t, store)) if _, err := action(context.Background(), GetBatchImportRunItemRequest{RunID: "run-1", ItemID: "missing"}); err == nil || err.Error() != "item not found: missing" { t.Fatalf("missing item error = %v, want item not found", err) } } func TestResumePendingBatchImportRunsNoRunningRuns(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) mustCreateAppImportRun(t, store, sqlite.ImportRun{ RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "completed", }) if err := resumePendingBatchImportRuns(context.Background(), appTestDSN(t, store)); err != nil { t.Fatalf("resumePendingBatchImportRuns() error = %v", err) } } func TestNewBatchImportRuntimeRunnerFromStoredRun(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) if _, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-1", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", AuthToken: "host-token", }); err != nil { t.Fatalf("Hosts().Create() error = %v", err) } runner, err := newBatchImportRuntimeRunnerFromStoredRun(context.Background(), store, sqlite.ImportRun{ RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "subscription", SubscriptionUsersJSON: `["user-1","user-2"]`, SubscriptionDays: 30, ProbeAPIKey: "probe-key", }) if err != nil { t.Fatalf("newBatchImportRuntimeRunnerFromStoredRun() error = %v", err) } if runner.request.HostID != "host-1" || len(runner.request.SubscriptionUsers) != 2 || runner.request.ProbeAPIKey != "probe-key" { t.Fatalf("runner.request = %+v, want parsed stored run request", runner.request) } } func TestBuildGetBatchImportRunActionClassifiesNotFoundOnlyOnSQLNoRows(t *testing.T) { t.Parallel() store := openAppTestStore(t) defer closeAppTestStore(t, store) action := buildGetBatchImportRunAction(appTestDSN(t, store)) _, err := action(context.Background(), "missing") if err == nil || err.Error() != "run not found: missing" { t.Fatalf("missing run error = %v, want run not found", err) } } func TestSQLNoRowsReference(t *testing.T) { t.Parallel() if sql.ErrNoRows == nil { t.Fatal("sql.ErrNoRows = nil") } }