Exclude placeholder capabilities from dev entitlements

This commit is contained in:
rcourtman
2026-03-26 22:45:07 +00:00
parent dc987d2d41
commit 48dc1a997d
5 changed files with 29 additions and 0 deletions
@@ -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
@@ -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
@@ -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)
}
+2
View File
@@ -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:
+8
View File
@@ -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) {