package batch import ( "context" "strings" "testing" "sub2api-cn-relay-manager/internal/host/sub2api" "sub2api-cn-relay-manager/internal/store/sqlite" ) func TestValidationService(t *testing.T) { t.Parallel() t.Run("confirmed or advisory with chat 200 becomes active", func(t *testing.T) { t.Parallel() for _, confirmationStatus := range []string{string(ConfirmationConfirmed), string(ConfirmationAdvisory)} { confirmationStatus := confirmationStatus t.Run(confirmationStatus, func(t *testing.T) { t.Parallel() itemStore := &fakeValidationItemStore{} runStore := &fakeValidationRunStore{ run: sqlite.ImportRun{RunID: "run-1", TotalItems: 1}, } service := ValidationService{ ItemStore: itemStore, RunStore: runStore, Validator: func(ctx context.Context, item sqlite.ImportRunItem) (sub2api.GatewayCompletionResult, error) { return sub2api.GatewayCompletionResult{OK: true, StatusCode: 200, ContentType: "application/json"}, nil }, } item := sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", CurrentStage: string(ItemStageValidate), ConfirmationStatus: confirmationStatus, AccessStatus: string(AccessStatusUnknown), ResolvedSmokeModel: "kimi-k2.6", APIKeyFingerprint: "sha256:abc", MatchedAccountState: string(MatchedAccountStateActive), AccountResolution: string(AccountResolutionReused), } if err := service.ValidateItem(context.Background(), item); err != nil { t.Fatalf("ValidateItem() error = %v", err) } got := itemStore.last if got.AccessStatus != string(AccessStatusActive) { t.Fatalf("AccessStatus = %q, want active", got.AccessStatus) } if got.CurrentStage != string(ItemStageDone) { t.Fatalf("CurrentStage = %q, want done", got.CurrentStage) } if runStore.updated.ActiveItems != 1 || runStore.updated.CompletedItems != 1 { t.Fatalf("run summary = %+v, want active/completed increment", runStore.updated) } }) } }) t.Run("exhausted transient completion becomes degraded", func(t *testing.T) { t.Parallel() itemStore := &fakeValidationItemStore{} runStore := &fakeValidationRunStore{ run: sqlite.ImportRun{RunID: "run-1", TotalItems: 1}, } service := ValidationService{ ItemStore: itemStore, RunStore: runStore, Validator: func(ctx context.Context, item sqlite.ImportRunItem) (sub2api.GatewayCompletionResult, error) { return sub2api.GatewayCompletionResult{ OK: false, StatusCode: 503, BodyPreview: `{"error":{"message":"no available accounts"}}`, }, nil }, } item := sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", CurrentStage: string(ItemStageValidate), ConfirmationStatus: string(ConfirmationConfirmed), AccessStatus: string(AccessStatusUnknown), ResolvedSmokeModel: "kimi-k2.6", APIKeyFingerprint: "sha256:abc", MatchedAccountState: string(MatchedAccountStateActive), AccountResolution: string(AccountResolutionReused), } if err := service.ValidateItem(context.Background(), item); err != nil { t.Fatalf("ValidateItem() error = %v", err) } if itemStore.last.AccessStatus != string(AccessStatusDegraded) { t.Fatalf("AccessStatus = %q, want degraded", itemStore.last.AccessStatus) } if !strings.Contains(itemStore.last.AdvisoryMessagesJSON, "gateway_warmup_retry_succeeded") { t.Fatalf("AdvisoryMessagesJSON = %q, want warmup advisory", itemStore.last.AdvisoryMessagesJSON) } if runStore.updated.DegradedItems != 1 || runStore.updated.WarningItems != 1 { t.Fatalf("run summary = %+v, want degraded/warning increment", runStore.updated) } }) t.Run("definitive invalid path becomes broken", func(t *testing.T) { t.Parallel() itemStore := &fakeValidationItemStore{} runStore := &fakeValidationRunStore{ run: sqlite.ImportRun{RunID: "run-1", TotalItems: 1}, } service := ValidationService{ ItemStore: itemStore, RunStore: runStore, Validator: func(ctx context.Context, item sqlite.ImportRunItem) (sub2api.GatewayCompletionResult, error) { return sub2api.GatewayCompletionResult{ OK: false, StatusCode: 404, BodyPreview: `{"error":"route missing"}`, }, nil }, } item := sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", CurrentStage: string(ItemStageValidate), ConfirmationStatus: string(ConfirmationConfirmed), AccessStatus: string(AccessStatusUnknown), ResolvedSmokeModel: "kimi-k2.6", APIKeyFingerprint: "sha256:abc", MatchedAccountState: string(MatchedAccountStateActive), AccountResolution: string(AccountResolutionReused), } if err := service.ValidateItem(context.Background(), item); err != nil { t.Fatalf("ValidateItem() error = %v", err) } if itemStore.last.AccessStatus != string(AccessStatusBroken) { t.Fatalf("AccessStatus = %q, want broken", itemStore.last.AccessStatus) } if runStore.updated.BrokenItems != 1 || runStore.updated.CompletedItems != 1 { t.Fatalf("run summary = %+v, want broken/completed increment", runStore.updated) } }) t.Run("only validation stage may write access status", func(t *testing.T) { t.Parallel() itemStore := &fakeValidationItemStore{} runStore := &fakeValidationRunStore{ run: sqlite.ImportRun{RunID: "run-1", TotalItems: 1}, } service := ValidationService{ ItemStore: itemStore, RunStore: runStore, Validator: func(ctx context.Context, item sqlite.ImportRunItem) (sub2api.GatewayCompletionResult, error) { return sub2api.GatewayCompletionResult{OK: true, StatusCode: 200}, nil }, } item := sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", CurrentStage: string(ItemStageConfirm), ConfirmationStatus: string(ConfirmationConfirmed), AccessStatus: string(AccessStatusUnknown), ResolvedSmokeModel: "kimi-k2.6", APIKeyFingerprint: "sha256:abc", MatchedAccountState: string(MatchedAccountStateActive), AccountResolution: string(AccountResolutionReused), } err := service.ValidateItem(context.Background(), item) if err == nil { t.Fatal("ValidateItem() error = nil, want validation stage guard") } if itemStore.calls != 0 { t.Fatalf("item upsert calls = %d, want 0", itemStore.calls) } }) } type fakeValidationItemStore struct { last sqlite.ImportRunItem calls int } func (f *fakeValidationItemStore) Upsert(ctx context.Context, item sqlite.ImportRunItem) error { f.last = item f.calls++ return nil } type fakeValidationRunStore struct { run sqlite.ImportRun updated sqlite.ImportRun } func (f *fakeValidationRunStore) GetByRunID(ctx context.Context, runID string) (sqlite.ImportRun, error) { return f.run, nil } func (f *fakeValidationRunStore) Update(ctx context.Context, run sqlite.ImportRun) error { f.updated = run f.run = run return nil }