diff --git a/internal/store/sqlite/provider_accounts_repo_test.go b/internal/store/sqlite/provider_accounts_repo_test.go index d00abe10..46ab3a91 100644 --- a/internal/store/sqlite/provider_accounts_repo_test.go +++ b/internal/store/sqlite/provider_accounts_repo_test.go @@ -375,3 +375,91 @@ func TestSyncProviderAccountsFromImportBatchInfersRouteFromShadowBinding(t *test t.Fatalf("provider account view route binding = %+v", view) } } + +func TestSyncProviderAccountsFromImportBatchLeavesRouteEmptyOnAmbiguousShadowBinding(t *testing.T) { + t.Parallel() + + store := openTestDBWithFK(t) + ctx := context.Background() + hostID := createTestHost(t, store) + hostRow, err := store.Hosts().GetByID(ctx, hostID) + if err != nil { + t.Fatalf("Hosts().GetByID() error = %v", err) + } + packID := createTestPack(t, store) + providerID, err := store.Providers().Create(ctx, Provider{ + PackID: packID, + ProviderID: "asxs-provider", + DisplayName: "ASXS Provider", + BaseURL: "https://api.asxs.top/v1", + Platform: "openai", + }) + if err != nil { + t.Fatalf("Providers().Create() error = %v", err) + } + for _, groupID := range []string{"gpt-shared-a", "gpt-shared-b"} { + if _, err := store.LogicalGroups().Create(ctx, LogicalGroup{ + LogicalGroupID: groupID, + DisplayName: groupID, + Status: "active", + }); err != nil { + t.Fatalf("LogicalGroups().Create(%s) error = %v", groupID, err) + } + } + for _, routeID := range []string{"route-a", "route-b"} { + logicalGroupID := "gpt-shared-a" + if routeID == "route-b" { + logicalGroupID = "gpt-shared-b" + } + if _, err := store.LogicalGroupRoutes().Create(ctx, LogicalGroupRoute{ + RouteID: routeID, + LogicalGroupID: logicalGroupID, + Name: routeID, + Status: "active", + Priority: 10, + Weight: 100, + ShadowGroupID: "group-1", + ShadowHostID: hostRow.HostID, + }); err != nil { + t.Fatalf("LogicalGroupRoutes().Create(%s) error = %v", routeID, err) + } + } + batchID, err := store.ImportBatches().Create(ctx, ImportBatch{ + HostID: hostID, + PackID: packID, + ProviderID: providerID, + Mode: "strict", + BatchStatus: "succeeded", + AccessStatus: "subscription_ready", + }) + if err != nil { + t.Fatalf("ImportBatches().Create() error = %v", err) + } + if _, err := store.ImportBatchItems().Create(ctx, ImportBatchItem{ + BatchID: batchID, + KeyFingerprint: "sha256:key1", + AccountStatus: "passed", + ProbeSummaryJSON: `{"account_id":"account-1","probe_status":"passed"}`, + }); err != nil { + t.Fatalf("ImportBatchItems().Create() error = %v", err) + } + for _, resource := range []ManagedResource{ + {BatchID: batchID, HostID: hostID, ResourceType: "group", HostResourceID: "group-1", ResourceName: "ASXS Group"}, + {BatchID: batchID, HostID: hostID, ResourceType: "account", HostResourceID: "account-1", ResourceName: "asxs-01"}, + } { + if _, err := store.ManagedResources().Create(ctx, resource); err != nil { + t.Fatalf("ManagedResources().Create(%s) error = %v", resource.ResourceType, err) + } + } + + if err := SyncProviderAccountsFromImportBatch(ctx, store, batchID); err != nil { + t.Fatalf("SyncProviderAccountsFromImportBatch() error = %v", err) + } + account, err := store.ProviderAccounts().GetByHostIDAndAccountID(ctx, hostID, "account-1") + if err != nil { + t.Fatalf("ProviderAccounts().GetByHostIDAndAccountID() error = %v", err) + } + if account.RouteID != "" { + t.Fatalf("provider account route binding = %+v, want empty route on ambiguous shadow binding", account) + } +} diff --git a/internal/store/sqlite/provider_accounts_sync.go b/internal/store/sqlite/provider_accounts_sync.go index 341c5f55..098528c2 100644 --- a/internal/store/sqlite/provider_accounts_sync.go +++ b/internal/store/sqlite/provider_accounts_sync.go @@ -134,6 +134,9 @@ func resolveProviderAccountRouteBinding(ctx context.Context, store *DB, shadowHo if err == sql.ErrNoRows { return LogicalGroupRoute{}, err } + if isAmbiguousProviderAccountRouteBinding(err) { + return LogicalGroupRoute{}, sql.ErrNoRows + } return LogicalGroupRoute{}, fmt.Errorf("resolve logical route for provider account shadow binding %q/%q: %w", shadowHostID, shadowGroupID, err) } return route, nil @@ -209,3 +212,10 @@ func preserveManagedProviderAccountStatus(row *ProviderAccount, existing Provide } } } + +func isAmbiguousProviderAccountRouteBinding(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "multiple logical group routes match shadow binding") +}