diff --git a/internal/api/monitored_system_limit_enforcement.go b/internal/api/monitored_system_limit_enforcement.go index 631333bb3..8b62e47ab 100644 --- a/internal/api/monitored_system_limit_enforcement.go +++ b/internal/api/monitored_system_limit_enforcement.go @@ -85,8 +85,10 @@ func maxMonitoredSystemsLimitForContext(ctx context.Context) int { limit := status.MaxMonitoredSystems - // Apply onboarding overflow bonus for free-tier orgs. - if status.Tier == licenseTierFreeValue { + // Apply onboarding overflow bonus for free-tier orgs. The bonus only + // makes sense on plans that actually have a cap — adding +1 to an + // uncapped limit (0) would convert "unlimited" into a cap of 1. + if status.Tier == licenseTierFreeValue && limit > 0 { var overflowGrantedAt *int64 // Try evaluator first (covers hosted path with DatabaseSource). diff --git a/internal/api/monitored_system_limit_enforcement_test.go b/internal/api/monitored_system_limit_enforcement_test.go index 16ac70020..7bb9d57a7 100644 --- a/internal/api/monitored_system_limit_enforcement_test.go +++ b/internal/api/monitored_system_limit_enforcement_test.go @@ -550,6 +550,30 @@ func TestVMwareAdmissionEnforcementSkipsUsageAndInventoryForDisabledConnections( ) } +func TestOnboardingOverflowBonusSkipsUncappedPlans(t *testing.T) { + enforcement := readAPIPackageFile(t, "monitored_system_limit_enforcement.go") + overflowSegment := requireSourceSegment( + t, + enforcement, + "func maxMonitoredSystemsLimitForContext", + "\n}\n", + ) + requireSnippetBefore( + t, + overflowSegment, + "limit > 0", + "overflowBonusFromLicensing", + ) + + entitlements := readAPIPackageFile(t, "subscription_entitlements.go") + requireSnippetCountAtLeast( + t, + entitlements, + "if status.MaxMonitoredSystems > 0 {", + 2, + ) +} + func TestMonitoredSystemCountNilMonitor(t *testing.T) { got := monitoredSystemCount(nil) if got != 0 { diff --git a/internal/api/subscription_entitlements.go b/internal/api/subscription_entitlements.go index bfa346c3c..eeb214589 100644 --- a/internal/api/subscription_entitlements.go +++ b/internal/api/subscription_entitlements.go @@ -40,11 +40,15 @@ func (h *LicenseHandlers) buildCommercialEntitlementPayload( usage := h.entitlementUsageSnapshot(ctx) trialEndsAtUnix := trialEndsAtUnixFromService(svc) - // Onboarding overflow: +1 agent for 14 days on free tier. + // Onboarding overflow: +1 agent for 14 days on free tier. Only applies + // on plans that have an actual cap — adding the bonus to an uncapped + // limit (0) would surface as a cap of 1 to the UI. overflowGrantedAt := h.ensureOnboardingOverflow(ctx, status.Tier) now := time.Now() - if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { - status.MaxMonitoredSystems += bonus + if status.MaxMonitoredSystems > 0 { + if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { + status.MaxMonitoredSystems += bonus + } } payload := buildEntitlementPayloadWithUsage(status, svc.SubscriptionState(), usage, trialEndsAtUnix) @@ -123,8 +127,10 @@ func (h *LicenseHandlers) HandleRuntimeCapabilities(w http.ResponseWriter, r *ht overflowGrantedAt := h.ensureOnboardingOverflow(r.Context(), status.Tier) now := time.Now() - if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { - status.MaxMonitoredSystems += bonus + if status.MaxMonitoredSystems > 0 { + if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { + status.MaxMonitoredSystems += bonus + } } payload := buildRuntimeCapabilitiesPayloadWithUsage(status, svc.SubscriptionState(), usage)