Files
sub2api-cn-relay-manager/internal/store/sqlite/import_runs_repo_test.go

469 lines
18 KiB
Go

package sqlite
import (
"context"
"reflect"
"testing"
"time"
)
func TestRunStateStore(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openTestDB(t)
run := ImportRun{
RunID: "run-1",
HostID: "host-1",
Mode: "strict",
AccessMode: "subscription",
SubscriptionUsersJSON: `["user-1"]`,
SubscriptionDays: 30,
ProbeAPIKey: "probe-key",
State: "running",
TotalItems: 1,
}
if err := store.ImportRuns().Create(ctx, run); err != nil {
t.Fatalf("ImportRuns().Create() error = %v", err)
}
run.State = "completed_with_warnings"
run.CompletedItems = 1
run.ActiveItems = 1
run.WarningItems = 1
run.FinishedAt = "2026-05-22T12:00:00Z"
if err := store.ImportRuns().Update(ctx, run); err != nil {
t.Fatalf("ImportRuns().Update() error = %v", err)
}
gotRun, err := store.ImportRuns().GetByRunID(ctx, "run-1")
if err != nil {
t.Fatalf("ImportRuns().GetByRunID() error = %v", err)
}
if gotRun.State != "completed_with_warnings" {
t.Fatalf("run.State = %q, want completed_with_warnings", gotRun.State)
}
if gotRun.WarningItems != 1 {
t.Fatalf("run.WarningItems = %d, want 1", gotRun.WarningItems)
}
if gotRun.HostID != "host-1" {
t.Fatalf("run.HostID = %q, want host-1", gotRun.HostID)
}
if gotRun.SubscriptionUsersJSON != `["user-1"]` {
t.Fatalf("run.SubscriptionUsersJSON = %q, want persisted subscription users", gotRun.SubscriptionUsersJSON)
}
if gotRun.SubscriptionDays != 30 {
t.Fatalf("run.SubscriptionDays = %d, want 30", gotRun.SubscriptionDays)
}
if gotRun.ProbeAPIKey != "probe-key" {
t.Fatalf("run.ProbeAPIKey = %q, want probe-key", gotRun.ProbeAPIKey)
}
legacyBatchID := int64(88)
reusedAccountID := int64(321)
channelID := int64(66)
accountID := int64(77)
item := ImportRunItem{
ItemID: "item-1",
RunID: "run-1",
BaseURL: "https://api.deepseek.com/v1",
ProviderID: "api-deepseek-12345678",
APIKeyFingerprint: "fp_abc123",
RequestedModelsJSON: `["kimi 2.6"]`,
RawModelsJSON: `["kimi-k2.6"]`,
NormalizedModelsJSON: `["kimi-k2.6"]`,
CanonicalFamiliesJSON: `["kimi-2.6"]`,
RecommendedModelsJSON: `["kimi-k2.6"]`,
CurrentStage: "confirm",
ConfirmationStatus: "pending",
AccessStatus: "unknown",
MatchedAccountState: "deprecated",
AccountResolution: "reactivated",
ProvisionReused: true,
ReusedFromProviderID: "api-deepseek-87654321",
ReusedFromAccountID: &reusedAccountID,
ChannelID: &channelID,
AccountID: &accountID,
RetryCount: 2,
ConfirmationAttempts: 3,
LastRetryAt: "2026-05-22T12:01:00Z",
NextRetryAt: "2026-05-22T12:02:00Z",
LeaseOwner: "worker-1",
LeaseUntil: "2026-05-22T12:03:00Z",
AdvisoryMessagesJSON: `["warmup"]`,
LastErrorStage: "confirm",
LastError: "no available accounts",
LegacyBatchID: &legacyBatchID,
LegacyProviderID: "legacy-provider",
CapabilityProfileJSON: `{"transport_profile":{"supports_openai_chat_completions":true}}`,
ResolvedSmokeModel: "kimi-k2.6",
}
if err := store.ImportRunItems().Upsert(ctx, item); err != nil {
t.Fatalf("ImportRunItems().Upsert() error = %v", err)
}
gotItem, err := store.ImportRunItems().GetByItemID(ctx, "item-1")
if err != nil {
t.Fatalf("ImportRunItems().GetByItemID() error = %v", err)
}
if gotItem.APIKeyFingerprint != "fp_abc123" {
t.Fatalf("item.APIKeyFingerprint = %q, want fp_abc123", gotItem.APIKeyFingerprint)
}
if gotItem.CanonicalFamiliesJSON != `["kimi-2.6"]` {
t.Fatalf("item.CanonicalFamiliesJSON = %q, want canonical families json", gotItem.CanonicalFamiliesJSON)
}
if gotItem.MatchedAccountState != "deprecated" {
t.Fatalf("item.MatchedAccountState = %q, want deprecated", gotItem.MatchedAccountState)
}
if gotItem.AccountResolution != "reactivated" {
t.Fatalf("item.AccountResolution = %q, want reactivated", gotItem.AccountResolution)
}
if !gotItem.ProvisionReused {
t.Fatal("item.ProvisionReused = false, want true")
}
if gotItem.ReusedFromProviderID != "api-deepseek-87654321" {
t.Fatalf("item.ReusedFromProviderID = %q, want reused provider id", gotItem.ReusedFromProviderID)
}
if gotItem.ReusedFromAccountID == nil || *gotItem.ReusedFromAccountID != reusedAccountID {
t.Fatalf("item.ReusedFromAccountID = %#v, want %d", gotItem.ReusedFromAccountID, reusedAccountID)
}
if gotItem.LeaseOwner != "worker-1" || gotItem.LeaseUntil != "2026-05-22T12:03:00Z" {
t.Fatalf("lease = (%q, %q), want persisted lease fields", gotItem.LeaseOwner, gotItem.LeaseUntil)
}
event := ImportRunItemEvent{
EventID: "evt-1",
RunID: "run-1",
ItemID: "item-1",
EventType: "retry_scheduled",
Stage: "confirm",
Attempt: 2,
Message: "retry after warmup",
PayloadJSON: `{"next_retry_at":"2026-05-22T12:02:00Z"}`,
}
if err := store.ImportRunEvents().Append(ctx, event); err != nil {
t.Fatalf("ImportRunEvents().Append() error = %v", err)
}
events, err := store.ImportRunEvents().ListByItemID(ctx, "item-1")
if err != nil {
t.Fatalf("ImportRunEvents().ListByItemID() error = %v", err)
}
if len(events) != 1 {
t.Fatalf("len(events) = %d, want 1", len(events))
}
if events[0].EventType != "retry_scheduled" {
t.Fatalf("events[0].EventType = %q, want retry_scheduled", events[0].EventType)
}
items, err := store.ImportRunItems().ListByRunID(ctx, "run-1")
if err != nil {
t.Fatalf("ImportRunItems().ListByRunID() error = %v", err)
}
if len(items) != 1 {
t.Fatalf("len(items) = %d, want 1", len(items))
}
if !reflect.DeepEqual(items[0].AdvisoryMessagesJSON, `["warmup"]`) {
t.Fatalf("items[0].AdvisoryMessagesJSON = %q, want advisory json", items[0].AdvisoryMessagesJSON)
}
}
func TestImportRunsRepoList(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openTestDB(t)
for _, run := range []ImportRun{
{RunID: "run-a", HostID: "host-a", Mode: "strict", AccessMode: "subscription", State: "running"},
{RunID: "run-b", HostID: "host-b", Mode: "partial", AccessMode: "self_service", State: "completed"},
} {
if err := store.ImportRuns().Create(ctx, run); err != nil {
t.Fatalf("ImportRuns().Create(%q) error = %v", run.RunID, err)
}
}
runs, err := store.ImportRuns().List(ctx, 1)
if err != nil {
t.Fatalf("ImportRuns().List(limit=1) error = %v", err)
}
if len(runs) != 1 {
t.Fatalf("ImportRuns().List(limit=1) len = %d, want 1", len(runs))
}
runs, err = store.ImportRuns().List(ctx, 0)
if err != nil {
t.Fatalf("ImportRuns().List(limit=0) error = %v", err)
}
if len(runs) != 2 {
t.Fatalf("ImportRuns().List(limit=0) len = %d, want 2", len(runs))
}
}
func TestImportRunItemsRepoCreateUpdateAndLease(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openTestDB(t)
run := ImportRun{RunID: "run-lease", HostID: "host-lease", Mode: "strict", AccessMode: "subscription", State: "running"}
if err := store.ImportRuns().Create(ctx, run); err != nil {
t.Fatalf("ImportRuns().Create() error = %v", err)
}
item := ImportRunItem{
ItemID: "item-lease",
RunID: "run-lease",
BaseURL: "https://api.example.com/v1",
ProviderID: "provider-lease",
APIKeyFingerprint: "fp_lease",
CurrentStage: "confirm",
ConfirmationStatus: "pending",
AccessStatus: "unknown",
MatchedAccountState: "active",
AccountResolution: "created",
RequestedModelsJSON: `["model-a"]`,
AdvisoryMessagesJSON: `["first"]`,
}
if err := store.ImportRunItems().Create(ctx, item); err != nil {
t.Fatalf("ImportRunItems().Create() error = %v", err)
}
item.ConfirmationStatus = "confirmed"
item.CurrentStage = "validate"
item.AccessStatus = "active"
item.AdvisoryMessagesJSON = `["updated"]`
if err := store.ImportRunItems().Update(ctx, item); err != nil {
t.Fatalf("ImportRunItems().Update() error = %v", err)
}
got, err := store.ImportRunItems().GetByItemID(ctx, "item-lease")
if err != nil {
t.Fatalf("ImportRunItems().GetByItemID() error = %v", err)
}
if got.CurrentStage != "validate" || got.ConfirmationStatus != "confirmed" {
t.Fatalf("updated item = %+v, want validate/confirmed", got)
}
leaseItem := item
leaseItem.ItemID = "item-pending"
leaseItem.CurrentStage = "confirm"
leaseItem.ConfirmationStatus = "pending"
leaseItem.AccessStatus = "unknown"
if err := store.ImportRunItems().Create(ctx, leaseItem); err != nil {
t.Fatalf("ImportRunItems().Create(item-pending) error = %v", err)
}
now := time.Date(2026, 5, 23, 1, 2, 3, 0, time.UTC)
leased, ok, err := store.ImportRunItems().TryAcquireConfirmationLease(ctx, "item-pending", "worker-1", now, 2*time.Minute)
if err != nil {
t.Fatalf("TryAcquireConfirmationLease() error = %v", err)
}
if !ok {
t.Fatal("TryAcquireConfirmationLease() ok = false, want true")
}
if leased.LeaseOwner != "worker-1" {
t.Fatalf("LeaseOwner = %q, want worker-1", leased.LeaseOwner)
}
if leased.ConfirmationAttempts != 1 {
t.Fatalf("ConfirmationAttempts = %d, want 1", leased.ConfirmationAttempts)
}
_, ok, err = store.ImportRunItems().TryAcquireConfirmationLease(ctx, "item-pending", "worker-2", now.Add(30*time.Second), time.Minute)
if err != nil {
t.Fatalf("TryAcquireConfirmationLease(second) error = %v", err)
}
if ok {
t.Fatal("TryAcquireConfirmationLease(second) ok = true, want false while lease active")
}
}
func TestImportRunItemsRepoUpsertDefaultsOptionalJSONAndNullableFields(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openTestDB(t)
run := ImportRun{RunID: "run-upsert-defaults", HostID: "host-upsert", Mode: "strict", AccessMode: "subscription", State: "running"}
if err := store.ImportRuns().Create(ctx, run); err != nil {
t.Fatalf("ImportRuns().Create() error = %v", err)
}
if err := store.ImportRunItems().Upsert(ctx, ImportRunItem{
ItemID: "item-upsert-defaults",
RunID: "run-upsert-defaults",
BaseURL: "https://api.example.com/v1",
ProviderID: "provider-upsert",
APIKeyFingerprint: "fp-upsert",
CurrentStage: "confirm",
ConfirmationStatus: "pending",
AccessStatus: "unknown",
MatchedAccountState: "active",
AccountResolution: "created",
ResolvedSmokeModel: " gpt-5.4 ",
LastRetryAt: " ",
NextRetryAt: " ",
LeaseOwner: " worker-upsert ",
LeaseUntil: " 2026-05-23T12:00:00Z ",
LastErrorStage: " confirm ",
LastError: " timeout ",
LegacyProviderID: " legacy-provider ",
}); err != nil {
t.Fatalf("ImportRunItems().Upsert() error = %v", err)
}
got, err := store.ImportRunItems().GetByItemID(ctx, "item-upsert-defaults")
if err != nil {
t.Fatalf("ImportRunItems().GetByItemID() error = %v", err)
}
if got.RequestedModelsJSON != "[]" {
t.Fatalf("RequestedModelsJSON = %q, want []", got.RequestedModelsJSON)
}
if got.RawModelsJSON != "[]" {
t.Fatalf("RawModelsJSON = %q, want []", got.RawModelsJSON)
}
if got.NormalizedModelsJSON != "[]" {
t.Fatalf("NormalizedModelsJSON = %q, want []", got.NormalizedModelsJSON)
}
if got.CanonicalFamiliesJSON != "[]" {
t.Fatalf("CanonicalFamiliesJSON = %q, want []", got.CanonicalFamiliesJSON)
}
if got.RecommendedModelsJSON != "[]" {
t.Fatalf("RecommendedModelsJSON = %q, want []", got.RecommendedModelsJSON)
}
if got.CapabilityProfileJSON != "{}" {
t.Fatalf("CapabilityProfileJSON = %q, want {}", got.CapabilityProfileJSON)
}
if got.AdvisoryMessagesJSON != "[]" {
t.Fatalf("AdvisoryMessagesJSON = %q, want []", got.AdvisoryMessagesJSON)
}
if got.ResolvedSmokeModel != "gpt-5.4" {
t.Fatalf("ResolvedSmokeModel = %q, want gpt-5.4", got.ResolvedSmokeModel)
}
if got.LeaseOwner != "worker-upsert" {
t.Fatalf("LeaseOwner = %q, want worker-upsert", got.LeaseOwner)
}
if got.LeaseUntil != "2026-05-23T12:00:00Z" {
t.Fatalf("LeaseUntil = %q, want trimmed lease_until", got.LeaseUntil)
}
if got.LastErrorStage != "confirm" {
t.Fatalf("LastErrorStage = %q, want confirm", got.LastErrorStage)
}
if got.LastError != "timeout" {
t.Fatalf("LastError = %q, want timeout", got.LastError)
}
if got.LegacyProviderID != "legacy-provider" {
t.Fatalf("LegacyProviderID = %q, want legacy-provider", got.LegacyProviderID)
}
if got.LastRetryAt != "" || got.NextRetryAt != "" {
t.Fatalf("retry timestamps = (%q, %q), want empty strings", got.LastRetryAt, got.NextRetryAt)
}
}
func TestImportRunItemsRepoUpsertValidationErrors(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openTestDB(t)
run := ImportRun{RunID: "run-upsert-validation", HostID: "host-upsert-validation", Mode: "strict", AccessMode: "subscription", State: "running"}
if err := store.ImportRuns().Create(ctx, run); err != nil {
t.Fatalf("ImportRuns().Create() error = %v", err)
}
tests := []struct {
name string
item ImportRunItem
}{
{name: "missing item_id", item: ImportRunItem{RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", ProviderID: "provider", APIKeyFingerprint: "fp", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "unknown", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing run_id", item: ImportRunItem{ItemID: "item", BaseURL: "https://api.example.com/v1", ProviderID: "provider", APIKeyFingerprint: "fp", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "unknown", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing base_url", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", ProviderID: "provider", APIKeyFingerprint: "fp", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "unknown", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing provider_id", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", APIKeyFingerprint: "fp", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "unknown", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing fingerprint", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", ProviderID: "provider", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "unknown", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing current_stage", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", ProviderID: "provider", APIKeyFingerprint: "fp", ConfirmationStatus: "pending", AccessStatus: "unknown", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing confirmation_status", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", ProviderID: "provider", APIKeyFingerprint: "fp", CurrentStage: "confirm", AccessStatus: "unknown", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing access_status", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", ProviderID: "provider", APIKeyFingerprint: "fp", CurrentStage: "confirm", ConfirmationStatus: "pending", MatchedAccountState: "active", AccountResolution: "created"}},
{name: "missing matched_account_state", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", ProviderID: "provider", APIKeyFingerprint: "fp", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "unknown", AccountResolution: "created"}},
{name: "missing account_resolution", item: ImportRunItem{ItemID: "item", RunID: "run-upsert-validation", BaseURL: "https://api.example.com/v1", ProviderID: "provider", APIKeyFingerprint: "fp", CurrentStage: "confirm", ConfirmationStatus: "pending", AccessStatus: "unknown", MatchedAccountState: "active"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := store.ImportRunItems().Upsert(ctx, tt.item); err == nil {
t.Fatal("Upsert() error = nil, want validation error")
}
})
}
}
func TestImportRunEventsRepoCreateAndHelpers(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openTestDB(t)
if err := store.ImportRuns().Create(ctx, ImportRun{
RunID: "run-events",
HostID: "host-events",
Mode: "strict",
AccessMode: "subscription",
State: "running",
}); err != nil {
t.Fatalf("ImportRuns().Create() error = %v", err)
}
if err := store.ImportRunItems().Create(ctx, ImportRunItem{
ItemID: "item-events",
RunID: "run-events",
BaseURL: "https://api.example.com/v1",
ProviderID: "provider-events",
APIKeyFingerprint: "fp_events",
CurrentStage: "confirm",
ConfirmationStatus: "pending",
AccessStatus: "unknown",
MatchedAccountState: "active",
AccountResolution: "created",
}); err != nil {
t.Fatalf("ImportRunItems().Create() error = %v", err)
}
event := ImportRunItemEvent{
EventID: "evt-create",
RunID: "run-events",
ItemID: "item-events",
EventType: "confirmation",
Stage: "confirm",
Message: "created by wrapper",
}
if err := store.ImportRunEvents().Create(ctx, event); err != nil {
t.Fatalf("ImportRunEvents().Create() error = %v", err)
}
if err := store.ImportRunItemEvents().Create(ctx, ImportRunItemEvent{
EventID: "evt-alias",
RunID: "run-events",
ItemID: "item-events",
EventType: "alias",
Stage: "confirm",
Message: "created by alias accessor",
}); err != nil {
t.Fatalf("ImportRunItemEvents().Create() error = %v", err)
}
events, err := store.ImportRunEvents().ListByItemID(ctx, "item-events")
if err != nil {
t.Fatalf("ImportRunEvents().ListByItemID() error = %v", err)
}
if len(events) != 2 {
t.Fatalf("ImportRunEvents().ListByItemID() len = %d, want 2", len(events))
}
nullable := sqlNullInt64{Int64: 42, Valid: true}
if ptr := nullable.ptr(); ptr == nil || *ptr != 42 {
t.Fatalf("sqlNullInt64.ptr() = %#v, want 42", ptr)
}
if ptr := (sqlNullInt64{}).ptr(); ptr != nil {
t.Fatalf("sqlNullInt64{}.ptr() = %#v, want nil", ptr)
}
}