From 48dc1a997d03c41da9d6dc5ecc011110550ddf54 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 26 Mar 2026 22:45:07 +0000 Subject: [PATCH] Exclude placeholder capabilities from dev entitlements --- .../v6/internal/subsystems/api-contracts.md | 5 +++++ .../release-control/v6/internal/subsystems/cloud-paid.md | 5 +++++ internal/api/entitlement_handlers_test.go | 9 +++++++++ pkg/licensing/dev_mode_features.go | 2 ++ pkg/licensing/service_activate_test.go | 8 ++++++++ 5 files changed, 29 insertions(+) diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 44e9fe2c4..bf953c72f 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -325,6 +325,11 @@ That widening still has to respect runtime feature flags. A capability like `multi_tenant` must stay absent from dev/demo entitlement payloads until the process also has `PULSE_MULTI_TENANT_ENABLED=true`; otherwise admin shells drift into impossible routes that the same backend still rejects as disabled. +The same rule applies to placeholder or plan-marker capabilities as well: +dev/demo entitlement payloads must not advertise non-operable entries like +`white_label`, `multi_user`, or `unlimited` just because they exist in tier +metadata, when the current runtime does not expose a corresponding usable +feature surface. The `/api/resources` serializer now also refreshes canonical identity and policy metadata through the shared unified-resource helper before it writes the payload, so backend and frontend contract tests stay aligned on one diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 32e34557e..7e9f5ff4f 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -692,6 +692,11 @@ That dev widening is still bounded by runtime readiness rather than marketing intent: `multi_tenant` is not a valid advertised dev capability unless the process also has `PULSE_MULTI_TENANT_ENABLED=true`, because the org API surface is intentionally disabled otherwise. +The same runtime-readiness rule also excludes placeholder and plan-marker +entries like `white_label`, `multi_user`, and `unlimited` from dev/demo +entitlement payloads when there is no corresponding operable runtime surface; +those belong in tier metadata and plan semantics, not in live capability +advertising for a free dev shell. Cloud/MSP live price IDs are no longer an open fill-in task either. The audit record `docs/release-control/v6/records/cloud-msp-price-audit-2026-03-13.md` verified that the 13 canonical Cloud/MSP v6 `price_*` IDs are present in the diff --git a/internal/api/entitlement_handlers_test.go b/internal/api/entitlement_handlers_test.go index 4d142527c..35fb7a9f6 100644 --- a/internal/api/entitlement_handlers_test.go +++ b/internal/api/entitlement_handlers_test.go @@ -390,6 +390,15 @@ func TestEntitlementHandler_DevModeMirrorsFeatureGateCapabilities(t *testing.T) if containsCapability(payload.Capabilities, license.FeatureMultiTenant) { t.Fatalf("expected dev entitlements to omit %q while runtime flag is disabled, got %v", license.FeatureMultiTenant, payload.Capabilities) } + for _, feature := range []string{ + license.FeatureMultiUser, + license.FeatureWhiteLabel, + license.FeatureUnlimited, + } { + if containsCapability(payload.Capabilities, feature) { + t.Fatalf("expected dev entitlements to omit non-runtime capability %q, got %v", feature, payload.Capabilities) + } + } if len(payload.UpgradeReasons) != 0 { t.Fatalf("expected no upgrade reasons in dev mode, got %v", payload.UpgradeReasons) } diff --git a/pkg/licensing/dev_mode_features.go b/pkg/licensing/dev_mode_features.go index a5dbc5c92..b5799c3d1 100644 --- a/pkg/licensing/dev_mode_features.go +++ b/pkg/licensing/dev_mode_features.go @@ -20,6 +20,8 @@ func devModeFeatures() []string { func devModeFeatureEnabled(feature string) bool { switch feature { + case FeatureMultiUser, FeatureWhiteLabel, FeatureUnlimited: + return false case FeatureMultiTenant: return strings.EqualFold(strings.TrimSpace(os.Getenv("PULSE_MULTI_TENANT_ENABLED")), "true") default: diff --git a/pkg/licensing/service_activate_test.go b/pkg/licensing/service_activate_test.go index 545f915a3..2dcb4c2c7 100644 --- a/pkg/licensing/service_activate_test.go +++ b/pkg/licensing/service_activate_test.go @@ -194,6 +194,14 @@ func TestServiceStatus_DevModeAdvertisesOnlyRuntimeEnabledFeaturesWithoutLicense if containsStringValue(status.Features, FeatureMultiTenant) { t.Fatalf("status.Features unexpectedly includes %q when runtime flag is disabled: %v", FeatureMultiTenant, status.Features) } + for _, feature := range []string{FeatureMultiUser, FeatureWhiteLabel, FeatureUnlimited} { + if svc.HasFeature(feature) { + t.Fatalf("HasFeature(%q)=true, want false for non-runtime dev capability", feature) + } + if containsStringValue(status.Features, feature) { + t.Fatalf("status.Features unexpectedly includes %q in dev mode: %v", feature, status.Features) + } + } } func TestServiceStatus_DevModeIncludesMultiTenantWhenRuntimeEnabled(t *testing.T) {