Files
sub2api-cn-relay-manager/internal/provision/batch_detail_service_test.go

344 lines
15 KiB
Go

package provision
import (
"context"
"testing"
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
func TestBatchDetailServiceGetReturnsPersistedArtifacts(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
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"}},
}
batchID := seedRuntimeImportForReconcile(t, store, host)
providerRow, err := store.Providers().ListByProviderID(context.Background(), sampleProviderManifest().ProviderID)
if err != nil {
t.Fatalf("Providers().ListByProviderID() error = %v", err)
}
if len(providerRow) != 1 {
t.Fatalf("providers = %d, want 1", len(providerRow))
}
batchRow, err := store.ImportBatches().GetByID(context.Background(), batchID)
if err != nil {
t.Fatalf("ImportBatches().GetByID() error = %v", err)
}
if _, err := store.ReconcileRuns().Create(context.Background(), sqlite.ReconcileRun{BatchID: batchID, HostID: batchRow.HostID, ProviderID: providerRow[0].ID, Status: "active", SummaryJSON: `{"missing_count":0}`}); err != nil {
t.Fatalf("ReconcileRuns().Create() error = %v", err)
}
result, err := NewBatchDetailService(store).Get(context.Background(), batchID)
if err != nil {
t.Fatalf("Get() error = %v", err)
}
if result.Batch.ID != batchID {
t.Fatalf("Batch.ID = %d, want %d", result.Batch.ID, batchID)
}
if len(result.Items) != 2 {
t.Fatalf("len(Items) = %d, want 2", len(result.Items))
}
if len(result.ManagedResources) != 4 {
t.Fatalf("len(ManagedResources) = %d, want 4", len(result.ManagedResources))
}
if len(result.AccessClosures) != 1 {
t.Fatalf("len(AccessClosures) = %d, want 1", len(result.AccessClosures))
}
if len(result.ReconcileRuns) != 1 {
t.Fatalf("len(ReconcileRuns) = %d, want 1", len(result.ReconcileRuns))
}
}
func TestBatchDetailServiceScopesReconcileRunsByBatchID(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
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"}},
}
ctx := context.Background()
batchID := seedRuntimeImportForReconcile(t, store, host)
batchRow, err := store.ImportBatches().GetByID(ctx, batchID)
if err != nil {
t.Fatalf("ImportBatches().GetByID() error = %v", err)
}
providerRow, err := store.Providers().ListByProviderID(ctx, sampleProviderManifest().ProviderID)
if err != nil {
t.Fatalf("Providers().ListByProviderID() error = %v", err)
}
if len(providerRow) != 1 {
t.Fatalf("providers = %d, want 1", len(providerRow))
}
secondBatchID, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: batchRow.HostID, PackID: batchRow.PackID, ProviderID: providerRow[0].ID, Mode: ImportModePartial, BatchStatus: BatchStatusSucceeded, AccessStatus: AccessStatusSelfServiceReady})
if err != nil {
t.Fatalf("ImportBatches().Create(second) error = %v", err)
}
if _, err := store.ReconcileRuns().Create(ctx, sqlite.ReconcileRun{BatchID: batchID, HostID: batchRow.HostID, ProviderID: providerRow[0].ID, Status: "drifted", SummaryJSON: `{"batch":"first"}`}); err != nil {
t.Fatalf("ReconcileRuns().Create(first batch) error = %v", err)
}
if _, err := store.ReconcileRuns().Create(ctx, sqlite.ReconcileRun{BatchID: secondBatchID, HostID: batchRow.HostID, ProviderID: providerRow[0].ID, Status: "active", SummaryJSON: `{"batch":"second"}`}); err != nil {
t.Fatalf("ReconcileRuns().Create(second batch) error = %v", err)
}
result, err := NewBatchDetailService(store).Get(ctx, secondBatchID)
if err != nil {
t.Fatalf("Get() error = %v", err)
}
if len(result.ReconcileRuns) != 1 {
t.Fatalf("len(ReconcileRuns) = %d, want 1 for this batch only", len(result.ReconcileRuns))
}
if result.ReconcileRuns[0].Status != "active" {
t.Fatalf("ReconcileRuns[0].Status = %q, want active", result.ReconcileRuns[0].Status)
}
}
func TestBatchDetailServiceGetValidatesStore(t *testing.T) {
_, err := (*BatchDetailService)(nil).Get(context.Background(), 1)
if err == nil || err.Error() != "store is required" {
t.Fatalf("nil service Get() error = %v, want store is required", err)
}
}
func TestAccountIDFromProbeSummary(t *testing.T) {
accountID, err := accountIDFromProbeSummary(`{"account_id":" account_1 "}`)
if err != nil {
t.Fatalf("accountIDFromProbeSummary() error = %v", err)
}
if accountID != "account_1" {
t.Fatalf("accountID = %q, want account_1", accountID)
}
if _, err := accountIDFromProbeSummary(`{`); err == nil {
t.Fatal("accountIDFromProbeSummary() error = nil, want JSON decode error")
}
blank, err := accountIDFromProbeSummary("")
if err != nil {
t.Fatalf("accountIDFromProbeSummary(blank) error = %v", err)
}
if blank != "" {
t.Fatalf("blank accountID = %q, want empty", blank)
}
}
func TestReconcileServiceRerunAccessClosureWithoutProbeKeyUsesLatestStatus(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
status, checked, err := NewReconcileService(store, &fakeHostAdapter{}).rerunAccessClosure(context.Background(), 1, []sqlite.AccessClosureRecord{{ClosureType: AccessModeSubscription, Status: AccessStatusSubscriptionReady}}, "", "deepseek-chat")
if err != nil {
t.Fatalf("rerunAccessClosure() error = %v", err)
}
if checked {
t.Fatal("checked = true, want false without probe key")
}
if status != AccessStatusSubscriptionReady {
t.Fatalf("status = %q, want %q", status, AccessStatusSubscriptionReady)
}
}
func TestReconcileServiceRerunAccessClosureMarksBrokenWhenGatewayCheckFails(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
hostSeed := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
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"}},
}
batchID := seedRuntimeImportForReconcile(t, store, hostSeed)
host := &fakeHostAdapter{gatewayResult: sub2api.GatewayAccessResult{OK: false, StatusCode: 403, HasExpectedModel: false}}
status, checked, err := NewReconcileService(store, host).rerunAccessClosure(context.Background(), batchID, []sqlite.AccessClosureRecord{{ClosureType: AccessModeSelfService, Status: AccessStatusSelfServiceReady}}, "user-key", "deepseek-chat")
if err != nil {
t.Fatalf("rerunAccessClosure() error = %v", err)
}
if !checked {
t.Fatal("checked = false, want true")
}
if status != AccessStatusBroken {
t.Fatalf("status = %q, want %q", status, AccessStatusBroken)
}
if got := queryCount(t, store.SQLDB(), "access_closure_records"); got != 2 {
t.Fatalf("access_closure_records row count = %d, want 2 after rerun", got)
}
if host.gatewayProbe.ExpectedModel != "deepseek-chat" {
t.Fatalf("ExpectedModel = %q, want deepseek-chat", host.gatewayProbe.ExpectedModel)
}
}
func TestDiffManagedResourcesCountsMissingAndExtra(t *testing.T) {
missing, extra := diffManagedResources(
[]sqlite.ManagedResource{
{ResourceType: "group", HostResourceID: "group_1"},
{ResourceType: "account", HostResourceID: "account_1"},
},
sub2api.ManagedResourceSnapshot{
Groups: []sub2api.NamedResource{{ID: "group_1"}},
Accounts: []sub2api.NamedResource{{ID: "account_2"}},
},
)
if missing != 1 || extra != 1 {
t.Fatalf("diffManagedResources() = (%d, %d), want (1, 1)", missing, extra)
}
}
func TestDeriveProviderStatus(t *testing.T) {
tests := []struct {
name string
batchStatus string
accessStatus string
reconcileStatus string
want string
}{
{name: "recovered success beats stale reconcile", batchStatus: BatchStatusSucceeded, accessStatus: AccessStatusSelfServiceReady, reconcileStatus: "degraded", want: ProviderStatusActive},
{name: "succeeded batch", batchStatus: BatchStatusSucceeded, reconcileStatus: "not_run", want: ProviderStatusActive},
{name: "failed batch", batchStatus: BatchStatusFailed, want: ProviderStatusFailed},
{name: "running batch", batchStatus: "running", want: "running"},
{name: "unknown fallback", batchStatus: " pending ", want: "pending"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := deriveProviderStatus(tc.batchStatus, tc.accessStatus, tc.reconcileStatus); got != tc.want {
t.Fatalf("deriveProviderStatus(%q, %q, %q) = %q, want %q", tc.batchStatus, tc.accessStatus, tc.reconcileStatus, got, tc.want)
}
})
}
}
func TestProviderStatusServiceAggregatesLatestAccessModesAcrossBatches(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
ctx := context.Background()
hostID := seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
packID, err := store.Packs().Create(ctx, sqlite.Pack{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", Checksum: "checksum-1"})
if err != nil {
t.Fatalf("Packs().Create() error = %v", err)
}
providerID, err := store.Providers().Create(ctx, sqlite.Provider{PackID: packID, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.deepseek.com", Platform: "openai"})
if err != nil {
t.Fatalf("Providers().Create() error = %v", err)
}
batchSubscription, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: hostID, PackID: packID, ProviderID: providerID, Mode: ImportModePartial, BatchStatus: BatchStatusSucceeded, AccessStatus: AccessStatusSubscriptionReady})
if err != nil {
t.Fatalf("ImportBatches().Create(subscription) error = %v", err)
}
if _, err := store.AccessClosures().Create(ctx, sqlite.AccessClosureRecord{BatchID: batchSubscription, ClosureType: AccessModeSubscription, Status: AccessStatusSubscriptionReady, DetailsJSON: "{}"}); err != nil {
t.Fatalf("AccessClosures().Create(subscription) error = %v", err)
}
batchSelfService, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: hostID, PackID: packID, ProviderID: providerID, Mode: ImportModePartial, BatchStatus: BatchStatusSucceeded, AccessStatus: AccessStatusSelfServiceReady})
if err != nil {
t.Fatalf("ImportBatches().Create(self_service) error = %v", err)
}
if _, err := store.AccessClosures().Create(ctx, sqlite.AccessClosureRecord{BatchID: batchSelfService, ClosureType: AccessModeSelfService, Status: AccessStatusSelfServiceReady, DetailsJSON: "{}"}); err != nil {
t.Fatalf("AccessClosures().Create(self_service) error = %v", err)
}
if _, err := store.ReconcileRuns().Create(ctx, sqlite.ReconcileRun{BatchID: batchSelfService, HostID: hostID, ProviderID: providerID, Status: "drifted", SummaryJSON: `{"missing_count":1}`}); err != nil {
t.Fatalf("ReconcileRuns().Create() error = %v", err)
}
snapshot, err := NewProviderStatusService(store).GetStatus(ctx, ProviderQuery{ProviderID: "deepseek", PackID: "openai-cn-pack", HostID: "host-1"})
if err != nil {
t.Fatalf("GetStatus() error = %v", err)
}
if snapshot.LatestAccessStatus != AccessStatusFullyReady {
t.Fatalf("LatestAccessStatus = %q, want %q", snapshot.LatestAccessStatus, AccessStatusFullyReady)
}
if snapshot.ProviderStatus != ProviderStatusActive {
t.Fatalf("ProviderStatus = %q, want %q", snapshot.ProviderStatus, ProviderStatusActive)
}
if snapshot.LatestReconcileStatus != "drifted" {
t.Fatalf("LatestReconcileStatus = %q, want drifted", snapshot.LatestReconcileStatus)
}
}
func TestBuildPackAndProviderRecord(t *testing.T) {
packRow, err := buildPackRecord(sampleLoadedPack())
if err != nil {
t.Fatalf("buildPackRecord() error = %v", err)
}
if packRow.PackID != "openai-cn-pack" || packRow.TargetHost != "sub2api" {
t.Fatalf("packRow = %#v, want populated pack metadata", packRow)
}
providerRow, err := buildProviderRecord(7, sampleProviderManifest())
if err != nil {
t.Fatalf("buildProviderRecord() error = %v", err)
}
if providerRow.PackID != 7 || providerRow.ProviderID != sampleProviderManifest().ProviderID {
t.Fatalf("providerRow = %#v, want persisted provider metadata", providerRow)
}
if providerRow.DefaultModelsJSON == "" || providerRow.ManifestJSON == "" {
t.Fatalf("providerRow JSON fields = %#v, want serialized JSON", providerRow)
}
}
func TestFirstNonEmptyAndFingerprintKey(t *testing.T) {
if got := firstNonEmpty(" ", "value", "other"); got != "value" {
t.Fatalf("firstNonEmpty() = %q, want value", got)
}
if got := fingerprintKey([]string{" key-1 "}, 0); got == "key-1" || got == "sha256:" || len(got) < 20 {
t.Fatalf("fingerprintKey() = %q, want sha256 fingerprint", got)
}
if got := fingerprintKey(nil, 3); got != "key-4" {
t.Fatalf("fingerprintKey(nil, 3) = %q, want key-4", got)
}
}
func TestProviderStatusServiceGetResourcesRequiresProviderID(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
_, err := NewProviderStatusService(store).GetResources(context.Background(), ProviderQuery{})
if err == nil || err.Error() != "provider_id is required" {
t.Fatalf("GetResources() error = %v, want provider_id is required", err)
}
}
func TestResourceSlugFallsBackToProvider(t *testing.T) {
if got := resourceSlug(" !!! "); got != "provider" {
t.Fatalf("resourceSlug() = %q, want provider", got)
}
provider := sampleProviderManifest()
provider.ProviderID = " DeepSeek CN / Prod "
provider.GroupTemplate.Name = ""
provider.ChannelTemplate.Name = ""
provider.PlanTemplate.Name = ""
if got := SuggestAccountNamePrefix(provider); got != "deepseek-cn-prod-" {
t.Fatalf("SuggestAccountNamePrefix() = %q, want deepseek-cn-prod-", got)
}
resourceNames := SuggestResourceNames(provider)
if resourceNames.Group != "crm-deepseek-cn-prod-group" {
t.Fatalf("SuggestResourceNames() = %#v, want slugged resource names", resourceNames)
}
}