From 2ca0d347794e76ba51f63b41e72e36f819daaeb1 Mon Sep 17 00:00:00 2001 From: Manas Srivastava <[email protected]> Date: Sat, 6 Jun 2026 13:55:18 +0530 Subject: [PATCH 1/2] docs(provisioner): retire stale "-1 = unlimited (team/growth)" comment examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug-hunt iter4 P4-1. The strict-80 margin redesign (merged to common+api master 2026-06-05) made every tier's postgres_connections and redis_memory_mb finite (team=100 conns/1536 MB, growth=20 conns/1024 MB, pro=20 conns/512 MB). No tier passes the -1 "no limit" wire sentinel anymore. The comments still gave the misleading example "-1 = unlimited (team/growth)" / "team: unlimited (-1)". Comments-only for production code: the -1/<=0 sentinel branches stay (still the wire contract; the pool prewarm path still passes -1 deliberately), and the two dedicated-pod sizing literals (redis k8s maxmemoryMB, postgres k8s connLimit) are byte-identical — only their surrounding comments are corrected to note the runtime cap is reconciled to the finite registry value via Regrade. Also fixes one pre-existing FAILING test left behind by the strict-80 common bump: TestRegradeResource_Redis_ProTier_AppliesCap/team asserted maxmemory=0 (old team=-1) while the registry now returns 1536. Reworked to derive expected values from the live plans registry (rule 18) so a future registry change can never silently drift it again. No production behavior change — the server already applied the registry value; only the test expectation was stale. Coverage: Symptom: comment example "-1 = unlimited (team/growth)" + test want 0 Enumeration: rg -n 'unlimited|connLimit.*-1|-1.*unlimited' internal/ Sites found: stale tier-attributed examples across 6 source files + 2 tests Sites touched: all stale tier attributions corrected; pure-sentinel mech docs kept (accurate) with a post-strict-80 note on entry-point docs Coverage test: TestRegradeResource_Redis_ProTier_AppliesCap now registry-driven Live verified: doc-only/test-only — make gate green (build+vet+test ./...) Co-Authored-By: Claude Opus 4.8 --- internal/backend/postgres/backend.go | 8 +++-- internal/backend/postgres/k8s.go | 7 +++- internal/backend/postgres/local.go | 15 +++++--- internal/backend/redis/backend.go | 4 ++- internal/backend/redis/k8s.go | 54 +++++++++++++++++----------- internal/backend/redis/k8s_test.go | 4 +-- internal/server/server.go | 22 ++++++++---- internal/server/server_test.go | 31 +++++++++++----- 8 files changed, 98 insertions(+), 47 deletions(-) diff --git a/internal/backend/postgres/backend.go b/internal/backend/postgres/backend.go index 30e04c3..c97a794 100644 --- a/internal/backend/postgres/backend.go +++ b/internal/backend/postgres/backend.go @@ -35,9 +35,11 @@ func k8sEnvInt(key string, fallback int) int { // Backend is the interface every Postgres provisioning backend must implement. type Backend interface { // Provision creates the database and role for the given token. - // connLimit is the CONNECTION LIMIT to apply to the role (-1 = unlimited, - // 0 = unlimited). The caller (provisioner server) computes this from the - // shared plans.Registry so the provisioner stays a dumb executor. + // connLimit is the CONNECTION LIMIT to apply to the role; <= 0 is the "no cap" + // sentinel (Postgres treats both -1 and 0 as "no limit" here). The caller + // (provisioner server) computes this from the shared plans registry so the + // provisioner stays a dumb executor. Every tier's postgres_connections is finite + // post strict-80 (2026-06-05), so the registry does not pass the sentinel today. Provision(ctx context.Context, token, tier string, connLimit int) (*Credentials, error) StorageBytes(ctx context.Context, token, providerResourceID string) (int64, error) Deprovision(ctx context.Context, token, providerResourceID string) error diff --git a/internal/backend/postgres/k8s.go b/internal/backend/postgres/k8s.go index 416af9b..38c2c33 100644 --- a/internal/backend/postgres/k8s.go +++ b/internal/backend/postgres/k8s.go @@ -144,7 +144,12 @@ func sizingForTier(tier string) tierSizing { pvcGi: 200, qCPURequests: "1", qMemRequests: "4Gi", qCPULimits: "8", qMemLimits: "16Gi", - connLimit: -1, // unlimited; capped only by pod max_connections + // "no cap" sizing default; the enforced cap comes from the registry via + // Regrade (plans.yaml: team postgres_connections=100, growth=20 — both + // finite post strict-80, 2026-06-05). Note sz.connLimit is also what + // Provision's initDatabase uses, so the role is briefly uncapped until the + // reconciler's first Regrade applies the finite registry value. + connLimit: -1, } default: // Unknown tier → conservative hobby-equivalent sizing rather than fail-open. diff --git a/internal/backend/postgres/local.go b/internal/backend/postgres/local.go index e0f1cf0..a5072f3 100644 --- a/internal/backend/postgres/local.go +++ b/internal/backend/postgres/local.go @@ -119,9 +119,12 @@ func generatePassword(n int) (string, error) { } // Provision creates a Postgres database and user for the given token. -// connLimit is the CONNECTION LIMIT to apply to the role; -1 means unlimited -// (omits the clause). This value comes from the API handler via plans.Registry -// so the provisioner stays a dumb executor and the API remains the policy owner. +// connLimit is the CONNECTION LIMIT to apply to the role; <= 0 is the "no cap" +// sentinel (omits the clause; Postgres treats -1 as "no limit"). This value comes +// from the plans registry so the provisioner stays a dumb executor and the policy +// owner sets the cap. Every tier's postgres_connections is finite post the strict-80 +// margin redesign (2026-06-05), so the registry does not pass the sentinel today — +// it is retained for the wire contract. func (b *LocalBackend) Provision(ctx context.Context, token, tier string, connLimit int) (*Credentials, error) { dbName := dbNamePrefix + token username := userNamePrefix + token @@ -374,8 +377,10 @@ func (b *LocalBackend) Deprovision(ctx context.Context, token, providerResourceI // user on the shared local Postgres cluster. This is called by the entitlement // reconciler on plan upgrades/downgrades to ensure the role cap matches the new tier. // -// connLimit <= 0 means unlimited (pass -1 from plans.Registry). Postgres uses -1 -// internally for "no limit"; ALTER ROLE with CONNECTION LIMIT -1 removes any cap. +// connLimit <= 0 is the "no cap" sentinel. Postgres uses -1 internally for "no +// limit"; ALTER ROLE with CONNECTION LIMIT -1 removes any cap. The registry returns +// a finite value for every tier post strict-80 (2026-06-05), so this branch is the +// wire contract, not a path any current tier exercises. func (b *LocalBackend) Regrade(ctx context.Context, token, providerResourceID string, connLimit int) (RegradeResult, error) { // P0-2: a pool-claimed role is named from the pool token; resolve it from // provider_resource_id so ALTER ROLE targets the role that actually exists. diff --git a/internal/backend/redis/backend.go b/internal/backend/redis/backend.go index 5029be9..5bc1bc0 100644 --- a/internal/backend/redis/backend.go +++ b/internal/backend/redis/backend.go @@ -49,7 +49,9 @@ type Backend interface { // without error, and the reconciler leaves the row for the next sweep. type Regrader interface { // Regrade connects to the dedicated Redis pod and adjusts maxmemory to - // match targetMaxmemoryMB. targetMaxmemoryMB <= 0 means unlimited (maxmemory 0). + // match targetMaxmemoryMB. targetMaxmemoryMB <= 0 is the "no cap" sentinel + // (maxmemory 0); every tier's redis_memory_mb is finite post strict-80 + // (2026-06-05), so the registry does not pass the sentinel today. // Returns RegradeResult.Applied=false + SkipReason when the pod is already // correctly configured (idempotent no-op) or unreachable (soft skip). Regrade(ctx context.Context, token, providerResourceID string, targetMaxmemoryMB int) (RegradeResult, error) diff --git a/internal/backend/redis/k8s.go b/internal/backend/redis/k8s.go index b1e2cce..60b03b1 100644 --- a/internal/backend/redis/k8s.go +++ b/internal/backend/redis/k8s.go @@ -79,8 +79,11 @@ const ( // instead of "allkeys-lru" silently evicting the customer's oldest keys. // Silent eviction also contradicts --appendonly yes (durability). See P1-C. redisMaxmemoryPolicyCapped = "noeviction" - // redisMaxmemoryPolicyUnlimited is the policy for unlimited tiers (team): - // no cap, so eviction never triggers; "noeviction" is Redis's default. + // redisMaxmemoryPolicyUnlimited is the policy used when a tier resolves to the + // "no cap" sentinel (maxmemoryMB <= 0): no cap, so eviction never triggers; + // "noeviction" is Redis's default. Post the strict-80 margin redesign + // (2026-06-05) every tier's redis_memory_mb is finite, so no current tier uses + // this policy — it is retained for the sentinel contract. redisMaxmemoryPolicyUnlimited = "noeviction" // redisK8sOwnerTeamLabel is applied to dedicated customer namespaces. @@ -137,14 +140,19 @@ func routeKeyTTLForTier(tier string) time.Duration { // the default user (no ACL), so pod-level is the natural lever. Since each pod is // dedicated to one customer this is functionally a per-customer cap. // -// maxmemoryMB is the Redis --maxmemory flag value in MB. A value of -1 means -// unlimited (the flag is omitted entirely). This enforces the per-resource memory -// limit advertised in plans.yaml at the Redis level. The noeviction policy is used -// (see redisMaxmemoryPolicyCapped) so writes fail loudly with an OOM error at the -// cap rather than silently evicting customer data. Values mirror plans.yaml -// redis_memory_mb: +// maxmemoryMB is the Redis --maxmemory flag value in MB. A value of <= 0 is the +// "no cap" sentinel (the flag is omitted entirely). This enforces the per-resource +// memory limit at the Redis level. The noeviction policy is used (see +// redisMaxmemoryPolicyCapped) so writes fail loudly with an OOM error at the cap +// rather than silently evicting customer data. These pod-start values are the +// dedicated-pod sizing defaults; the runtime-enforced cap is reconciled to the +// plans registry by RegradeResource (server.go) on provision and plan change. // -// anonymous: 5 MB, hobby: 50 MB, pro: 512 MB, growth: 1024 MB, team: unlimited (-1) +// anonymous: 5 MB, hobby: 50 MB, pro: 512 MB, growth: 1024 MB, team: -1 (no flag) +// +// NOTE: the registry's redis_memory_mb is finite for every tier post the strict-80 +// margin redesign (2026-06-05; team=1536), so the team -1 below is a pod-start +// default that Regrade overrides — it no longer mirrors plans.yaml. type tierSizing struct { cpuReq, memReq string cpuLim, memLim string @@ -156,7 +164,9 @@ type tierSizing struct { // Bounds total simultaneous TCP clients connected to this pod. maxClients int // maxmemoryMB is the Redis --maxmemory limit in MB applied at pod start. - // -1 means unlimited (flag omitted). Mirrors plans.yaml redis_memory_mb. + // <= 0 is the "no cap" sentinel (flag omitted). The pod-start value is a sizing + // default; the runtime cap is reconciled to the plans registry by Regrade. The + // registry's redis_memory_mb is finite for every tier post strict-80 (2026-06-05). // The noeviction maxmemory-policy is applied alongside this limit (P1-C). maxmemoryMB int } @@ -225,15 +235,17 @@ func sizingForTier(tier string) tierSizing { maxmemoryMB: 1024, // plans.yaml: growth redis_memory_mb = 1024 } case "team", "team_yearly": - // team_yearly mirrors team (plans.yaml: identical -1 limits, annual billing). + // team_yearly mirrors team (identical limits, annual billing). return tierSizing{ cpuReq: "500m", memReq: "1Gi", cpuLim: "4", memLim: "4Gi", pvcMi: 51200, // 50Gi qCPURequests: "1", qMemRequests: "2Gi", qCPULimits: "8", qMemLimits: "8Gi", - maxClients: 1000, - maxmemoryMB: -1, // unlimited — team dedicated pods have no memory cap + maxClients: 1000, + // "no cap" pod-start default; Regrade reconciles to the registry value + // (plans.yaml: team redis_memory_mb = 1536, finite post strict-80). + maxmemoryMB: -1, } default: // Unknown tier → conservative hobby-equivalent sizing. @@ -624,8 +636,9 @@ type RegradeResult struct { // - targetMaxmemoryMB > 0 → set maxmemory to that many MB + noeviction policy // (P1-C: writes fail loudly at the cap, no silent eviction), then CONFIG // REWRITE so the cap survives a pod restart. -// - targetMaxmemoryMB <= 0 → unlimited tier (team/growth): set maxmemory to 0 -// (Redis "no cap") + CONFIG REWRITE so it explicitly overrides any leftover cap. +// - targetMaxmemoryMB <= 0 → "no cap" sentinel: set maxmemory to 0 (Redis "no +// cap") + CONFIG REWRITE so it explicitly overrides any leftover cap. No current +// tier resolves here post strict-80 (2026-06-05) — every redis_memory_mb is finite. // // Idempotent: reads CONFIG GET maxmemory first and short-circuits if the value // already matches, returning Applied=false + SkipReason="already correct". @@ -851,7 +864,7 @@ func (b *K8sBackend) regradeViaExec(ctx context.Context, ns, token string, targe return RegradeResult{Applied: false, SkipReason: "exec fallback: CONFIG GET output unparseable"}, nil } - // Compute target bytes (0 = unlimited for team tier). + // Compute target bytes (targetMaxmemoryMB <= 0 → 0 bytes = Redis "no cap"). var targetBytes int64 if targetMaxmemoryMB > 0 { targetBytes = int64(targetMaxmemoryMB) * 1024 * 1024 @@ -1104,10 +1117,11 @@ func (b *K8sBackend) applyDeployment(ctx context.Context, ns string, sz tierSizi "--dir", "/data", "--maxclients", fmt.Sprintf("%d", sz.maxClients), } - // Only add --maxmemory when the tier has a defined cap. - // -1 means unlimited (team/growth) — omit the flag so Redis - // uses its default (no cap). This matches plans.yaml semantics - // where -1 = unlimited. + // Only add --maxmemory when the sizing has a defined cap. + // sz.maxmemoryMB <= 0 is the "no cap" sentinel — omit the + // flag so Redis uses its default (no cap). This is a pod-start + // default; Regrade later reconciles the cap to the registry + // value, which is finite for every tier post strict-80. if sz.maxmemoryMB > 0 { cmd = append(cmd, "--maxmemory", fmt.Sprintf("%dmb", sz.maxmemoryMB), diff --git a/internal/backend/redis/k8s_test.go b/internal/backend/redis/k8s_test.go index 30dc035..fcf84f4 100644 --- a/internal/backend/redis/k8s_test.go +++ b/internal/backend/redis/k8s_test.go @@ -66,8 +66,8 @@ func TestSizingForTier_MaxmemoryMB_MatchesPlansYAML(t *testing.T) { {"hobby_plus_yearly", 50, true}, // plans.yaml: hobby_plus_yearly mirrors hobby_plus {"pro", 512, true}, // plans.yaml: pro redis_memory_mb = 512 {"pro_yearly", 512, true},// plans.yaml: pro_yearly mirrors pro - {"team", -1, false}, // unlimited — flag omitted - {"team_yearly", -1, false}, // plans.yaml: team_yearly mirrors team (unlimited) + {"team", -1, false}, // "no cap" pod-start default — flag omitted; Regrade reconciles to registry (team=1536, finite post strict-80) + {"team_yearly", -1, false}, // team_yearly mirrors team's pod-start sizing default {"growth", 1024, true}, // plans.yaml: growth redis_memory_mb = 1024 {"unknown", 50, true}, // unknown → hobby fallback } diff --git a/internal/server/server.go b/internal/server/server.go index 68449dc..b2503e6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -378,7 +378,11 @@ func isDedicatedTier(tier string) bool { func (s *Server) provisionPostgres(ctx context.Context, req *provisionerv1.ProvisionRequest) (*provisionerv1.ProvisionResponse, error) { // Resolve the connection limit for this tier from the shared plans registry. - // -1 = unlimited (team/growth); > 0 = enforced cap at the Postgres role level. + // <= 0 is the wire sentinel for "no role-level cap" (Postgres uses -1 for "no + // limit"); > 0 is an enforced cap at the Postgres role level. Post the strict-80 + // margin redesign (2026-06-05) every tier's postgres_connections is finite + // (e.g. pro=20, growth=20, team=100), so no tier passes the sentinel today — the + // branch is kept for the wire contract, not because any current tier uses it. // The provisioner owns the plans registry so the cap stays consistent with // what RegradeResource applies on plan upgrades. connLimit := regradeConnLimits.ConnectionsLimit(req.Tier, "postgres") @@ -1031,8 +1035,10 @@ func (s *Server) regradePostgres(ctx context.Context, req *provisionerv1.Regrade // redisBackend does not implement redis.Regrader) return {applied:false} gracefully so // the shared redis-provision pod is never accidentally capped. // -// Team/growth tiers (memory limit = -1 in plans.yaml) set maxmemory=0 (Redis "unlimited") -// so existing pods are never accidentally capped. +// A tier whose redis_memory_mb resolves to <= 0 sets maxmemory=0 (Redis "no cap"). +// Post the strict-80 margin redesign (2026-06-05) every tier's redis_memory_mb is +// finite in plans.yaml (e.g. pro=512, growth=1024, team=1536), so no tier hits this +// branch today — it remains for the sentinel contract, not for any current tier. func (s *Server) regradeRedis(ctx context.Context, req *provisionerv1.RegradeRequest) (*provisionerv1.RegradeResponse, error) { // Resolve the effective provider ID (k8s namespace). // Case 1: prid is already "instant-customer-" → use as-is. @@ -1095,12 +1101,16 @@ func (s *Server) regradeRedis(ctx context.Context, req *provisionerv1.RegradeReq // Memory cap comes from the shared plans registry. // StorageLimitMB("redis") returns plans.yaml redis_memory_mb: - // anonymous=5, hobby=50, pro=512, team/growth=-1 (unlimited). - // -1 (unlimited) → targetMaxmemoryMB=0 → Regrade sets maxmemory=0 (no cap). + // anonymous=5, hobby=50, pro=512, growth=1024, team=1536. + // All current tiers are finite post the strict-80 margin redesign (2026-06-05). + // The < 0 branch below is the wire sentinel for "no cap" (-> targetMaxmemoryMB=0 + // -> Regrade sets maxmemory=0); no current tier passes it, but it is kept so a + // future unlimited tier would behave correctly. memLimitMB := regradeConnLimits.StorageLimitMB(req.Tier, "redis") targetMaxmemoryMB := memLimitMB if memLimitMB < 0 { - // Unlimited tier — explicitly clear any cap on the pod. + // "No cap" sentinel (no current tier passes it post strict-80) — explicitly + // clear any cap on the pod. targetMaxmemoryMB = 0 } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 7bdd79c..26ce64f 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -8,6 +8,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "instant.dev/common/plans" commonv1 "instant.dev/proto/common/v1" provisionerv1 "instant.dev/proto/provisioner/v1" @@ -506,10 +507,24 @@ func (m *mockRegraderRedisBackend) Regrade(ctx context.Context, token, id string return redis.RegradeResult{Applied: true, AppliedMaxmemory: int64(targetMB) * 1024 * 1024}, nil } -// TestRegradeResource_Redis_ProTier_AppliesProCap verifies that a pro-tier Redis -// resource backed by a k8s-style provider_resource_id has maxmemory set to the -// pro cap (512 MB from plans.yaml). team/growth get maxmemory=0 (unlimited). +// TestRegradeResource_Redis_ProTier_AppliesCap verifies that a Redis resource +// backed by a k8s-style provider_resource_id has maxmemory set to the tier's +// registry-defined redis_memory_mb. Expectations are derived from the live plans +// registry (rule 18) rather than hand-typed, so a registry change (e.g. the +// strict-80 margin redesign that made every tier's redis_memory_mb finite) can +// never silently drift this test. A tier whose redis_memory_mb is <= 0 (the "no +// cap" sentinel; no current tier uses it post strict-80) yields maxmemory=0. func TestRegradeResource_Redis_ProTier_AppliesCap(t *testing.T) { + // wantRedisMB returns the maxmemory_mb the server is expected to apply for a + // tier, computed exactly as RegradeResource does: the registry value, with the + // <= 0 "no cap" sentinel mapped to 0. + wantRedisMB := func(tier string) int32 { + mb := plans.Default().StorageLimitMB(tier, "redis") + if mb < 0 { + mb = 0 + } + return int32(mb) + } cases := []struct { tier string wantApplied bool @@ -519,24 +534,22 @@ func TestRegradeResource_Redis_ProTier_AppliesCap(t *testing.T) { { tier: "pro", wantApplied: true, - wantAppliedMB: 512, // plans.yaml pro redis_memory_mb = 512 + wantAppliedMB: wantRedisMB("pro"), }, { tier: "hobby", wantApplied: true, - wantAppliedMB: 50, // plans.yaml hobby redis_memory_mb = 50 + wantAppliedMB: wantRedisMB("hobby"), }, { - // team tier: redis_memory_mb = -1 (unlimited) → targetMaxmemoryMB = 0 tier: "team", wantApplied: true, - wantAppliedMB: 0, // unlimited — maxmemory 0 + wantAppliedMB: wantRedisMB("team"), }, { - // growth tier: redis_memory_mb = 1024 (plans.yaml) → targetMaxmemoryMB = 1024 tier: "growth", wantApplied: true, - wantAppliedMB: 1024, // plans.yaml growth redis_memory_mb = 1024 + wantAppliedMB: wantRedisMB("growth"), }, } From bbbbf1fd58b9d28ed2f8e6a5525dae2b10099ac0 Mon Sep 17 00:00:00 2001 From: Manas Srivastava <[email protected]> Date: Sat, 6 Jun 2026 16:13:09 +0530 Subject: [PATCH 2/2] test(server): cover regradeRedis "no cap" sentinel branch (close 100%-patch gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment-only edit in this PR re-touched the diff hunk around regradeRedis's `if memLimitMB < 0 { targetMaxmemoryMB = 0 }` block, pulling that executable branch into the diff. diff-cover's 100%-patch gate (coverage.yml "Patch coverage gate") correctly flagged it uncovered — no production tier resolves a negative redis_memory_mb post the strict-80 margin redesign, so the branch had no test. Add a registry-seam test (SwapRegradeConnLimits + a fixture plans.yaml with redis_memory_mb: -1) that drives the sentinel and asserts maxmemory=0 is targeted (rule 18: registry-derived, not a hand-faked constant). The seam lives in a new export_test.go so the external server_test package can swap the unexported package-level registry. diff-cover now reports internal/server/server.go (100%), Missing: 0 lines. Co-Authored-By: Claude Opus 4.8 --- internal/server/export_test.go | 15 ++++++ internal/server/server_test.go | 90 +++++++++++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 internal/server/export_test.go diff --git a/internal/server/export_test.go b/internal/server/export_test.go new file mode 100644 index 0000000..39d0ade --- /dev/null +++ b/internal/server/export_test.go @@ -0,0 +1,15 @@ +package server + +import "instant.dev/common/plans" + +// SwapRegradeConnLimits temporarily replaces the package-level plans registry +// used by RegradeResource and returns a restore func. It exists so external +// tests can drive the "no cap" sentinel branch in regradeRedis (a tier whose +// redis_memory_mb resolves to <= 0), which no current production tier triggers +// post the strict-80 margin redesign — the branch is kept for the wire +// contract, so it needs an explicit registry-seam test rather than a live tier. +func SwapRegradeConnLimits(r *plans.Registry) (restore func()) { + prev := regradeConnLimits + regradeConnLimits = r + return func() { regradeConnLimits = prev } +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 26ce64f..d088a49 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -3,6 +3,8 @@ package server_test import ( "context" "errors" + "os" + "path/filepath" "testing" "google.golang.org/grpc/codes" @@ -224,7 +226,7 @@ func TestProvisionResource_BackendConnectError_ReturnsUnavailable(t *testing.T) &mockMongoBackend{}, &mockQueueBackend{}, nil, nil, nil, nil, nil, // storage + dedicated backends - nil, // pool + nil, // pool ) _, err := srv.ProvisionResource(context.Background(), &provisionerv1.ProvisionRequest{ Token: "tok", @@ -245,7 +247,7 @@ func TestProvisionResource_AlreadyExists_ReturnsAlreadyExists(t *testing.T) { &mockMongoBackend{}, &mockQueueBackend{}, nil, nil, nil, nil, nil, // storage + dedicated backends - nil, // pool + nil, // pool ) _, err := srv.ProvisionResource(context.Background(), &provisionerv1.ProvisionRequest{ Token: "tok", @@ -526,10 +528,10 @@ func TestRegradeResource_Redis_ProTier_AppliesCap(t *testing.T) { return int32(mb) } cases := []struct { - tier string - wantApplied bool - wantAppliedMB int32 // AppliedConnLimit field repurposed for Redis maxmemory_mb - wantSkipReason string + tier string + wantApplied bool + wantAppliedMB int32 // AppliedConnLimit field repurposed for Redis maxmemory_mb + wantSkipReason string }{ { tier: "pro", @@ -570,7 +572,7 @@ func TestRegradeResource_Redis_ProTier_AppliesCap(t *testing.T) { &mockMongoBackend{}, &mockQueueBackend{}, nil, nil, nil, nil, nil, // storage + dedicated backends - nil, // pool + nil, // pool ) resp, err := srv.RegradeResource(context.Background(), &provisionerv1.RegradeRequest{ Token: "tok-" + tc.tier, @@ -627,6 +629,80 @@ func TestRegradeResource_Redis_AlreadyCorrect_ReturnsSkip(t *testing.T) { } } +// TestRegradeResource_Redis_NoCapSentinel_SetsMaxmemoryZero exercises the "no +// cap" sentinel branch in regradeRedis: when a tier's redis_memory_mb resolves +// to a negative value, the server must target maxmemory=0 (Redis "no cap") +// rather than passing the negative value through. No production tier triggers +// this post the strict-80 margin redesign (every redis_memory_mb is finite), so +// the branch is driven via a registry seam (SwapRegradeConnLimits) loading a +// fixture tier with redis_memory_mb: -1 — a registry-derived test rather than a +// hand-faked constant (CLAUDE.md rule 18). +func TestRegradeResource_Redis_NoCapSentinel_SetsMaxmemoryZero(t *testing.T) { + // Minimal valid plans registry with one tier ("unlimited_redis") whose + // redis_memory_mb is the negative "no cap" sentinel. parse() requires an + // "anonymous" plan as the fallback, so include it. + yamlBody := `plans: + anonymous: + limits: + redis_memory_mb: 5 + unlimited_redis: + limits: + redis_memory_mb: -1 +` + dir := t.TempDir() + planPath := filepath.Join(dir, "plans.yaml") + if err := os.WriteFile(planPath, []byte(yamlBody), 0o600); err != nil { + t.Fatalf("write fixture plans.yaml: %v", err) + } + reg, err := plans.Load(planPath) + if err != nil { + t.Fatalf("plans.Load fixture: %v", err) + } + if got := reg.StorageLimitMB("unlimited_redis", "redis"); got >= 0 { + t.Fatalf("fixture precondition: redis_memory_mb for unlimited_redis = %d, want < 0", got) + } + restore := server.SwapRegradeConnLimits(reg) + defer restore() + + var gotTargetMB int + regraderBackend := &mockRegraderRedisBackend{ + regrade: func(_ context.Context, _, _ string, targetMB int) (redis.RegradeResult, error) { + gotTargetMB = targetMB + return redis.RegradeResult{ + Applied: true, + AppliedMaxmemory: int64(targetMB) * 1024 * 1024, + }, nil + }, + } + srv := server.NewWithBackends( + &config.Config{}, + &mockPostgresBackend{}, + regraderBackend, + &mockMongoBackend{}, + &mockQueueBackend{}, + nil, nil, nil, nil, nil, + nil, + ) + resp, err := srv.RegradeResource(context.Background(), &provisionerv1.RegradeRequest{ + Token: "tok-nocap", + ResourceType: commonv1.ResourceType_RESOURCE_TYPE_REDIS, + Tier: "unlimited_redis", + ProviderResourceId: "instant-customer-tok-nocap", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotTargetMB != 0 { + t.Fatalf("negative redis_memory_mb sentinel: target maxmemory_mb passed to backend = %d, want 0", gotTargetMB) + } + if !resp.Applied { + t.Fatal("expected Applied=true for the no-cap regrade") + } + if resp.AppliedConnLimit != 0 { + t.Fatalf("AppliedConnLimit (maxmemory_mb) = %d, want 0 for no-cap tier", resp.AppliedConnLimit) + } +} + func TestRegradeResource_NonK8sBackend_SkipsWithReason(t *testing.T) { // newTestServer wires the shared mockPostgresBackend, which is not a // *postgres.K8sBackend — the server should skip without touching it.