From 8a55f37d8d1fa7aa11c906a52d5b95e6973111ef Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:33:19 -0400 Subject: [PATCH 1/7] Add organization entitlements command Expose the API-resolved entitlement snapshot through kernel org entitlements without duplicating plan policy in the CLI. Upgrade the Go SDK to v0.92.0 and preserve null-as-unlimited values in both human and JSON output. --- cmd/org.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++- cmd/org_test.go | 81 ++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 4 files changed, 188 insertions(+), 5 deletions(-) diff --git a/cmd/org.go b/cmd/org.go index ffd8c02b..79386c3b 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "time" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" @@ -19,8 +20,14 @@ type OrgLimitsService interface { Update(ctx context.Context, body kernel.OrganizationLimitUpdateParams, opts ...option.RequestOption) (res *kernel.OrgLimits, err error) } +// OrgEntitlementsService defines the organization entitlements operation used by the CLI. +type OrgEntitlementsService interface { + Get(ctx context.Context, opts ...option.RequestOption) (res *kernel.OrgEntitlements, err error) +} + type OrgCmd struct { - limits OrgLimitsService + limits OrgLimitsService + entitlements OrgEntitlementsService } type OrgLimitsGetInput struct { @@ -32,6 +39,32 @@ type OrgLimitsSetInput struct { Output string } +type OrgEntitlementsInput struct { + Output string +} + +func (c OrgCmd) Entitlements(ctx context.Context, in OrgEntitlementsInput) error { + if err := validateJSONOutput(in.Output); err != nil { + return err + } + + entitlements, err := c.entitlements.Get(ctx) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + if entitlements == nil { + fmt.Println("null") + return nil + } + return util.PrintPrettyJSON(entitlements) + } + + renderOrgEntitlements(entitlements) + return nil +} + func (c OrgCmd) LimitsGet(ctx context.Context, in OrgLimitsGetInput) error { if err := validateJSONOutput(in.Output); err != nil { return err @@ -125,6 +158,56 @@ func orgLimitFieldPresent(field respjson.Field) bool { return field.Raw() != respjson.Omitted } +func renderOrgEntitlements(entitlements *kernel.OrgEntitlements) { + if entitlements == nil { + pterm.Info.Println("No organization entitlements found") + return + } + + status := entitlements.Plan.Status + if !entitlements.Plan.JSON.Status.Valid() { + status = "none" + } + trialEndsAt := "none" + if entitlements.Plan.JSON.TrialEndsAt.Valid() { + trialEndsAt = entitlements.Plan.TrialEndsAt.Format(time.RFC3339) + } + + features := entitlements.Features + limits := entitlements.Limits + rows := pterm.TableData{ + {"Category", "Entitlement", "Value"}, + {"Plan", "Contractual plan", entitlements.Plan.ID}, + {"Plan", "Effective plan", entitlements.Plan.EffectiveID}, + {"Plan", "Status", status}, + {"Plan", "Trialing", fmt.Sprintf("%t", entitlements.Plan.IsTrialing)}, + {"Plan", "Trial ends at", trialEndsAt}, + {"Feature", "Profiles", fmt.Sprintf("%t", features.Profiles.Enabled)}, + {"Feature", "File I/O", fmt.Sprintf("%t", features.FileIo.Enabled)}, + {"Feature", "Browser replays", fmt.Sprintf("%t", features.BrowserReplays.Enabled)}, + {"Feature", "Browser replay retention (days)", fmt.Sprintf("%d", features.BrowserReplays.RetentionDays)}, + {"Feature", "Browser extensions", fmt.Sprintf("%t", features.BrowserExtensions.Enabled)}, + {"Feature", "Max stored extensions", formatProjectLimitValue(features.BrowserExtensions.MaxStoredPerOrg, features.BrowserExtensions.JSON.MaxStoredPerOrg)}, + {"Feature", "Browser pools", fmt.Sprintf("%t", features.BrowserPools.Enabled)}, + {"Feature", "Managed auth", fmt.Sprintf("%t", features.ManagedAuth.Enabled)}, + {"Feature", "Max managed auth connections", formatProjectLimitValue(features.ManagedAuth.MaxConnections, features.ManagedAuth.JSON.MaxConnections)}, + {"Feature", "Health check minimum (seconds)", fmt.Sprintf("%d", features.ManagedAuth.HealthCheckIntervalMinSeconds)}, + {"Feature", "Health check default (seconds)", fmt.Sprintf("%d", features.ManagedAuth.HealthCheckIntervalDefaultSeconds)}, + {"Feature", "Health check maximum (seconds)", fmt.Sprintf("%d", features.ManagedAuth.HealthCheckIntervalMaxSeconds)}, + {"Feature", "Credentials", fmt.Sprintf("%t", features.Credentials.Enabled)}, + {"Feature", "Credential providers", fmt.Sprintf("%t", features.CredentialProviders.Enabled)}, + {"Feature", "Managed proxies", fmt.Sprintf("%t", features.ManagedProxies.Enabled)}, + {"Feature", "Custom proxies", fmt.Sprintf("%t", features.CustomProxies.Enabled)}, + {"Feature", "Proxy bypass hosts", fmt.Sprintf("%t", features.ProxyBypassHosts.Enabled)}, + {"Feature", "GPU", fmt.Sprintf("%t", features.GPU.Enabled)}, + {"Limit", "Max concurrent browsers", fmt.Sprintf("%d", limits.MaxConcurrentBrowsers)}, + {"Limit", "Max concurrent invocations", fmt.Sprintf("%d", limits.MaxConcurrentInvocations)}, + {"Limit", "Default max concurrent invocations per app", fmt.Sprintf("%d", limits.DefaultMaxConcurrentInvocationsPerApp)}, + } + + PrintTableNoPad(rows, true) +} + // --- Cobra wiring --- var orgCmd = &cobra.Command{ @@ -160,9 +243,20 @@ var orgLimitsSetCmd = &cobra.Command{ RunE: runOrgLimitsSet, } +var orgEntitlementsCmd = &cobra.Command{ + Use: "entitlements", + Short: "Get effective organization entitlements", + Long: "Show the authenticated organization's effective feature access and limits after applying its plan, trial, status, and organization-specific overrides. Unlimited values are shown as unlimited.", + Args: cobra.NoArgs, + RunE: runOrgEntitlements, +} + func getOrgHandler(cmd *cobra.Command) OrgCmd { client := getKernelClient(cmd) - return OrgCmd{limits: &client.Organization.Limits} + return OrgCmd{ + limits: &client.Organization.Limits, + entitlements: &client.Organization.Entitlements, + } } func runOrgLimitsGet(cmd *cobra.Command, args []string) error { @@ -184,12 +278,20 @@ func runOrgLimitsSet(cmd *cobra.Command, args []string) error { }) } +func runOrgEntitlements(cmd *cobra.Command, args []string) error { + c := getOrgHandler(cmd) + output, _ := cmd.Flags().GetString("output") + return c.Entitlements(cmd.Context(), OrgEntitlementsInput{Output: output}) +} + func init() { addJSONOutputFlag(orgLimitsGetCmd) orgLimitsSetCmd.Flags().Int64("default-project-max-concurrent-sessions", 0, "Default maximum concurrent browsers for projects without an explicit override (0 to remove the default)") addJSONOutputFlag(orgLimitsSetCmd) + addJSONOutputFlag(orgEntitlementsCmd) orgLimitsCmd.AddCommand(orgLimitsGetCmd) orgLimitsCmd.AddCommand(orgLimitsSetCmd) orgCmd.AddCommand(orgLimitsCmd) + orgCmd.AddCommand(orgEntitlementsCmd) } diff --git a/cmd/org_test.go b/cmd/org_test.go index 946800b0..b0ee5761 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "errors" "testing" @@ -16,6 +17,86 @@ type FakeOrgLimitsService struct { UpdateFunc func(ctx context.Context, body kernel.OrganizationLimitUpdateParams, opts ...option.RequestOption) (*kernel.OrgLimits, error) } +type FakeOrgEntitlementsService struct { + GetFunc func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) +} + +func (f *FakeOrgEntitlementsService) Get(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + if f.GetFunc != nil { + return f.GetFunc(ctx, opts...) + } + return nil, nil +} + +func testOrgEntitlements(t *testing.T) *kernel.OrgEntitlements { + t.Helper() + var entitlements kernel.OrgEntitlements + err := json.Unmarshal([]byte(`{ + "plan":{"id":"FREE","effective_id":"START_UP","status":null,"is_trialing":true,"trial_ends_at":null}, + "features":{ + "profiles":{"enabled":true}, + "file_io":{"enabled":true}, + "browser_replays":{"enabled":true,"retention_days":30}, + "browser_extensions":{"enabled":true,"max_stored_per_org":null}, + "browser_pools":{"enabled":true}, + "managed_auth":{"enabled":true,"max_connections":null,"health_check_interval_min_seconds":1200,"health_check_interval_default_seconds":3600,"health_check_interval_max_seconds":86400}, + "credentials":{"enabled":true}, + "credential_providers":{"enabled":true}, + "managed_proxies":{"enabled":true}, + "custom_proxies":{"enabled":true}, + "proxy_bypass_hosts":{"enabled":true}, + "gpu":{"enabled":false} + }, + "limits":{"max_concurrent_browsers":150,"max_concurrent_invocations":150,"default_max_concurrent_invocations_per_app":20} + }`), &entitlements) + assert.NoError(t, err) + return &entitlements +} + +func TestOrgEntitlements_RendersEffectiveValues(t *testing.T) { + buf := capturePtermOutput(t) + c := OrgCmd{entitlements: &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + return testOrgEntitlements(t), nil + }, + }} + + assert.NoError(t, c.Entitlements(context.Background(), OrgEntitlementsInput{})) + out := buf.String() + assert.Contains(t, out, "Contractual plan") + assert.Contains(t, out, "Effective plan") + assert.Contains(t, out, "START_UP") + assert.Contains(t, out, "Browser replay retention (days)") + assert.Contains(t, out, "Max managed auth connections") + assert.Contains(t, out, "unlimited") + assert.Contains(t, out, "Max concurrent browsers") +} + +func TestOrgEntitlements_JSONPreservesNullUnlimitedValues(t *testing.T) { + c := OrgCmd{entitlements: &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + return testOrgEntitlements(t), nil + }, + }} + + out := captureStdout(t, func() { + assert.NoError(t, c.Entitlements(context.Background(), OrgEntitlementsInput{Output: "json"})) + }) + assert.Contains(t, out, `"max_stored_per_org": null`) + assert.Contains(t, out, `"max_connections": null`) +} + +func TestOrgEntitlements_SurfacesAPIError(t *testing.T) { + capturePtermOutput(t) + c := OrgCmd{entitlements: &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + return nil, errors.New("boom") + }, + }} + + assert.Error(t, c.Entitlements(context.Background(), OrgEntitlementsInput{})) +} + func (f *FakeOrgLimitsService) Get(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgLimits, error) { if f.GetFunc != nil { return f.GetFunc(ctx, opts...) diff --git a/go.mod b/go.mod index ebe99635..502421b9 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.91.0 + github.com/kernel/kernel-go-sdk v0.92.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 7808eb39..04679b80 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.91.0 h1:/bJKFJQ8ZwAyl+r8P1sUW8NQYEjDekYZJ5R8Sml5bus= -github.com/kernel/kernel-go-sdk v0.91.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.0 h1:3EeoPahTcGEo97BCbwT50gu8QJnawfL166z12hc8Ucg= +github.com/kernel/kernel-go-sdk v0.92.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From c820e44426779d0ab719a9795227054ad5089fb0 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:45:38 -0400 Subject: [PATCH 2/7] Format trial end in local time Use the CLI's shared human-readable timestamp formatter for organization entitlement trial dates and cover the behavior with a focused regression test. --- cmd/org.go | 3 +-- cmd/org_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cmd/org.go b/cmd/org.go index 79386c3b..bad89008 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -3,7 +3,6 @@ package cmd import ( "context" "fmt" - "time" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" @@ -170,7 +169,7 @@ func renderOrgEntitlements(entitlements *kernel.OrgEntitlements) { } trialEndsAt := "none" if entitlements.Plan.JSON.TrialEndsAt.Valid() { - trialEndsAt = entitlements.Plan.TrialEndsAt.Format(time.RFC3339) + trialEndsAt = util.FormatLocal(entitlements.Plan.TrialEndsAt) } features := entitlements.Features diff --git a/cmd/org_test.go b/cmd/org_test.go index b0ee5761..b340bc96 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -6,6 +6,7 @@ import ( "errors" "testing" + "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" "github.com/kernel/kernel-go-sdk/packages/respjson" @@ -72,6 +73,21 @@ func TestOrgEntitlements_RendersEffectiveValues(t *testing.T) { assert.Contains(t, out, "Max concurrent browsers") } +func TestOrgEntitlements_RendersTrialEndInLocalTime(t *testing.T) { + var entitlements kernel.OrgEntitlements + err := json.Unmarshal([]byte(`{ + "plan":{"id":"FREE","effective_id":"START_UP","status":"ACTIVE","is_trialing":true,"trial_ends_at":"2030-01-02T03:04:05Z"}, + "features":{}, + "limits":{} + }`), &entitlements) + assert.NoError(t, err) + + buf := capturePtermOutput(t) + renderOrgEntitlements(&entitlements) + + assert.Contains(t, buf.String(), util.FormatLocal(entitlements.Plan.TrialEndsAt)) +} + func TestOrgEntitlements_JSONPreservesNullUnlimitedValues(t *testing.T) { c := OrgCmd{entitlements: &FakeOrgEntitlementsService{ GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { From 550d33c00ce9554bcec4243de8795a809301ae30 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:20:02 -0400 Subject: [PATCH 3/7] Document org entitlements command --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 96eed04e..3b2cef39 100644 --- a/README.md +++ b/README.md @@ -724,6 +724,8 @@ Automated authentication for web services. The `run` command orchestrates the fu ### Org +- `kernel org entitlements` - Show the organization's effective plan, feature access, and limits + - `--output json`, `-o json` - Output the raw entitlement response - `kernel org limits get` - Show the organization's concurrency limit and the default per-project cap applied to projects without an explicit override - `--output json`, `-o json` - Output raw JSON object - `kernel org limits set` - Set the default per-project concurrency cap applied to projects without an explicit override From dc984655b69310d340c0536432180dbeb241751e Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:01:24 -0400 Subject: [PATCH 4/7] Verify complete entitlement projection --- cmd/org.go | 8 +++--- cmd/org_test.go | 69 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/cmd/org.go b/cmd/org.go index bad89008..52fa43f7 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -163,6 +163,10 @@ func renderOrgEntitlements(entitlements *kernel.OrgEntitlements) { return } + PrintTableNoPad(orgEntitlementRows(entitlements), true) +} + +func orgEntitlementRows(entitlements *kernel.OrgEntitlements) pterm.TableData { status := entitlements.Plan.Status if !entitlements.Plan.JSON.Status.Valid() { status = "none" @@ -174,7 +178,7 @@ func renderOrgEntitlements(entitlements *kernel.OrgEntitlements) { features := entitlements.Features limits := entitlements.Limits - rows := pterm.TableData{ + return pterm.TableData{ {"Category", "Entitlement", "Value"}, {"Plan", "Contractual plan", entitlements.Plan.ID}, {"Plan", "Effective plan", entitlements.Plan.EffectiveID}, @@ -203,8 +207,6 @@ func renderOrgEntitlements(entitlements *kernel.OrgEntitlements) { {"Limit", "Max concurrent invocations", fmt.Sprintf("%d", limits.MaxConcurrentInvocations)}, {"Limit", "Default max concurrent invocations per app", fmt.Sprintf("%d", limits.DefaultMaxConcurrentInvocationsPerApp)}, } - - PrintTableNoPad(rows, true) } // --- Cobra wiring --- diff --git a/cmd/org_test.go b/cmd/org_test.go index b340bc96..ef25e645 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -10,6 +10,7 @@ import ( "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" "github.com/kernel/kernel-go-sdk/packages/respjson" + "github.com/pterm/pterm" "github.com/stretchr/testify/assert" ) @@ -29,7 +30,7 @@ func (f *FakeOrgEntitlementsService) Get(ctx context.Context, opts ...option.Req return nil, nil } -func testOrgEntitlements(t *testing.T) *kernel.OrgEntitlements { +func testOrgEntitlementsWithUnlimitedValues(t *testing.T) *kernel.OrgEntitlements { t.Helper() var entitlements kernel.OrgEntitlements err := json.Unmarshal([]byte(`{ @@ -54,23 +55,57 @@ func testOrgEntitlements(t *testing.T) *kernel.OrgEntitlements { return &entitlements } -func TestOrgEntitlements_RendersEffectiveValues(t *testing.T) { - buf := capturePtermOutput(t) - c := OrgCmd{entitlements: &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - return testOrgEntitlements(t), nil +func TestOrgEntitlementRows_CompleteProjection(t *testing.T) { + var entitlements kernel.OrgEntitlements + err := json.Unmarshal([]byte(`{ + "plan":{"id":"HOBBYIST","effective_id":"START_UP","status":"ACTIVE","is_trialing":true,"trial_ends_at":"2030-01-02T03:04:05Z"}, + "features":{ + "profiles":{"enabled":true}, + "file_io":{"enabled":false}, + "browser_replays":{"enabled":true,"retention_days":17}, + "browser_extensions":{"enabled":false,"max_stored_per_org":23}, + "browser_pools":{"enabled":true}, + "managed_auth":{"enabled":false,"max_connections":29,"health_check_interval_min_seconds":31,"health_check_interval_default_seconds":37,"health_check_interval_max_seconds":41}, + "credentials":{"enabled":true}, + "credential_providers":{"enabled":false}, + "managed_proxies":{"enabled":true}, + "custom_proxies":{"enabled":false}, + "proxy_bypass_hosts":{"enabled":true}, + "gpu":{"enabled":false} }, - }} + "limits":{"max_concurrent_browsers":43,"max_concurrent_invocations":47,"default_max_concurrent_invocations_per_app":53} + }`), &entitlements) + assert.NoError(t, err) - assert.NoError(t, c.Entitlements(context.Background(), OrgEntitlementsInput{})) - out := buf.String() - assert.Contains(t, out, "Contractual plan") - assert.Contains(t, out, "Effective plan") - assert.Contains(t, out, "START_UP") - assert.Contains(t, out, "Browser replay retention (days)") - assert.Contains(t, out, "Max managed auth connections") - assert.Contains(t, out, "unlimited") - assert.Contains(t, out, "Max concurrent browsers") + assert.Equal(t, pterm.TableData{ + {"Category", "Entitlement", "Value"}, + {"Plan", "Contractual plan", "HOBBYIST"}, + {"Plan", "Effective plan", "START_UP"}, + {"Plan", "Status", "ACTIVE"}, + {"Plan", "Trialing", "true"}, + {"Plan", "Trial ends at", util.FormatLocal(entitlements.Plan.TrialEndsAt)}, + {"Feature", "Profiles", "true"}, + {"Feature", "File I/O", "false"}, + {"Feature", "Browser replays", "true"}, + {"Feature", "Browser replay retention (days)", "17"}, + {"Feature", "Browser extensions", "false"}, + {"Feature", "Max stored extensions", "23"}, + {"Feature", "Browser pools", "true"}, + {"Feature", "Managed auth", "false"}, + {"Feature", "Max managed auth connections", "29"}, + {"Feature", "Health check minimum (seconds)", "31"}, + {"Feature", "Health check default (seconds)", "37"}, + {"Feature", "Health check maximum (seconds)", "41"}, + {"Feature", "Credentials", "true"}, + {"Feature", "Credential providers", "false"}, + {"Feature", "Managed proxies", "true"}, + {"Feature", "Custom proxies", "false"}, + {"Feature", "Proxy bypass hosts", "true"}, + {"Feature", "GPU", "false"}, + {"Limit", "Max concurrent browsers", "43"}, + {"Limit", "Max concurrent invocations", "47"}, + {"Limit", "Default max concurrent invocations per app", "53"}, + }, orgEntitlementRows(&entitlements)) } func TestOrgEntitlements_RendersTrialEndInLocalTime(t *testing.T) { @@ -91,7 +126,7 @@ func TestOrgEntitlements_RendersTrialEndInLocalTime(t *testing.T) { func TestOrgEntitlements_JSONPreservesNullUnlimitedValues(t *testing.T) { c := OrgCmd{entitlements: &FakeOrgEntitlementsService{ GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - return testOrgEntitlements(t), nil + return testOrgEntitlementsWithUnlimitedValues(t), nil }, }} From 444eae7967dc8bf8cb2970e199d7fe39cfa14855 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:08:40 -0400 Subject: [PATCH 5/7] Distinguish unknown entitlement values --- cmd/org.go | 40 ++++++++++++++++++++++++------- cmd/org_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/cmd/org.go b/cmd/org.go index 52fa43f7..e8ba0d52 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "github.com/kernel/cli/pkg/util" @@ -167,12 +168,11 @@ func renderOrgEntitlements(entitlements *kernel.OrgEntitlements) { } func orgEntitlementRows(entitlements *kernel.OrgEntitlements) pterm.TableData { - status := entitlements.Plan.Status - if !entitlements.Plan.JSON.Status.Valid() { - status = "none" - } - trialEndsAt := "none" - if entitlements.Plan.JSON.TrialEndsAt.Valid() { + status := formatNullableEntitlementString(entitlements.Plan.Status, entitlements.Plan.JSON.Status) + trialEndsAt := "unknown" + if entitlements.Plan.JSON.TrialEndsAt.Raw() == respjson.Null { + trialEndsAt = "none" + } else if entitlements.Plan.JSON.TrialEndsAt.Valid() { trialEndsAt = util.FormatLocal(entitlements.Plan.TrialEndsAt) } @@ -190,10 +190,10 @@ func orgEntitlementRows(entitlements *kernel.OrgEntitlements) pterm.TableData { {"Feature", "Browser replays", fmt.Sprintf("%t", features.BrowserReplays.Enabled)}, {"Feature", "Browser replay retention (days)", fmt.Sprintf("%d", features.BrowserReplays.RetentionDays)}, {"Feature", "Browser extensions", fmt.Sprintf("%t", features.BrowserExtensions.Enabled)}, - {"Feature", "Max stored extensions", formatProjectLimitValue(features.BrowserExtensions.MaxStoredPerOrg, features.BrowserExtensions.JSON.MaxStoredPerOrg)}, + {"Feature", "Max stored extensions", formatEntitlementLimitValue(features.BrowserExtensions.MaxStoredPerOrg, features.BrowserExtensions.JSON.MaxStoredPerOrg)}, {"Feature", "Browser pools", fmt.Sprintf("%t", features.BrowserPools.Enabled)}, {"Feature", "Managed auth", fmt.Sprintf("%t", features.ManagedAuth.Enabled)}, - {"Feature", "Max managed auth connections", formatProjectLimitValue(features.ManagedAuth.MaxConnections, features.ManagedAuth.JSON.MaxConnections)}, + {"Feature", "Max managed auth connections", formatEntitlementLimitValue(features.ManagedAuth.MaxConnections, features.ManagedAuth.JSON.MaxConnections)}, {"Feature", "Health check minimum (seconds)", fmt.Sprintf("%d", features.ManagedAuth.HealthCheckIntervalMinSeconds)}, {"Feature", "Health check default (seconds)", fmt.Sprintf("%d", features.ManagedAuth.HealthCheckIntervalDefaultSeconds)}, {"Feature", "Health check maximum (seconds)", fmt.Sprintf("%d", features.ManagedAuth.HealthCheckIntervalMaxSeconds)}, @@ -209,6 +209,30 @@ func orgEntitlementRows(entitlements *kernel.OrgEntitlements) pterm.TableData { } } +func formatNullableEntitlementString(value string, field respjson.Field) string { + if field.Raw() == respjson.Null { + return "none" + } + if !field.Valid() { + return "unknown" + } + var decoded string + if err := json.Unmarshal([]byte(field.Raw()), &decoded); err != nil { + return "unknown" + } + return value +} + +func formatEntitlementLimitValue(value int64, field respjson.Field) string { + if field.Raw() == respjson.Null { + return "unlimited" + } + if !field.Valid() { + return "unknown" + } + return fmt.Sprintf("%d", value) +} + // --- Cobra wiring --- var orgCmd = &cobra.Command{ diff --git a/cmd/org_test.go b/cmd/org_test.go index ef25e645..8268d989 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "testing" + "time" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" @@ -108,6 +109,68 @@ func TestOrgEntitlementRows_CompleteProjection(t *testing.T) { }, orgEntitlementRows(&entitlements)) } +func TestOrgEntitlementRows_NullableFieldStates(t *testing.T) { + populatedTrialEnd, err := time.Parse(time.RFC3339, "2031-02-03T04:05:06Z") + assert.NoError(t, err) + + tests := []struct { + name string + payload string + expectedStatus string + expectedTrialEnd string + expectedMaxStored string + expectedMaxAuthConns string + }{ + { + name: "populated", + payload: `{"plan":{"status":"ACTIVE","trial_ends_at":"2031-02-03T04:05:06Z"},"features":{"browser_extensions":{"max_stored_per_org":11},"managed_auth":{"max_connections":13}}}`, + expectedStatus: "ACTIVE", + expectedTrialEnd: util.FormatLocal(populatedTrialEnd), + expectedMaxStored: "11", + expectedMaxAuthConns: "13", + }, + { + name: "explicit null", + payload: `{"plan":{"status":null,"trial_ends_at":null},"features":{"browser_extensions":{"max_stored_per_org":null},"managed_auth":{"max_connections":null}}}`, + expectedStatus: "none", + expectedTrialEnd: "none", + expectedMaxStored: "unlimited", + expectedMaxAuthConns: "unlimited", + }, + { + name: "omitted", + payload: `{"plan":{},"features":{"browser_extensions":{},"managed_auth":{}}}`, + expectedStatus: "unknown", + expectedTrialEnd: "unknown", + expectedMaxStored: "unknown", + expectedMaxAuthConns: "unknown", + }, + { + name: "malformed", + payload: `{"plan":{"status":7,"trial_ends_at":"not-a-date"},"features":{"browser_extensions":{"max_stored_per_org":"many"},"managed_auth":{"max_connections":"many"}}}`, + expectedStatus: "unknown", + expectedTrialEnd: "unknown", + expectedMaxStored: "unknown", + expectedMaxAuthConns: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var entitlements kernel.OrgEntitlements + assert.NoError(t, json.Unmarshal([]byte(tt.payload), &entitlements)) + + rows := orgEntitlementRows(&entitlements) + assert.Equal(t, pterm.TableData{ + {"Plan", "Status", tt.expectedStatus}, + {"Plan", "Trial ends at", tt.expectedTrialEnd}, + {"Feature", "Max stored extensions", tt.expectedMaxStored}, + {"Feature", "Max managed auth connections", tt.expectedMaxAuthConns}, + }, pterm.TableData{rows[3], rows[5], rows[11], rows[14]}) + }) + } +} + func TestOrgEntitlements_RendersTrialEndInLocalTime(t *testing.T) { var entitlements kernel.OrgEntitlements err := json.Unmarshal([]byte(`{ From c4a13565f8679d2f35542591cfc1e5a0de3950ee Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:16:47 -0400 Subject: [PATCH 6/7] Verify entitlement boolean mappings --- cmd/org_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cmd/org_test.go b/cmd/org_test.go index 8268d989..30f2825a 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -109,6 +109,48 @@ func TestOrgEntitlementRows_CompleteProjection(t *testing.T) { }, orgEntitlementRows(&entitlements)) } +func TestOrgEntitlementRows_BooleanFieldProvenance(t *testing.T) { + booleanEntitlements := []string{"Trialing", "Profiles", "File I/O", "Browser replays", "Browser extensions", "Browser pools", "Managed auth", "Credentials", "Credential providers", "Managed proxies", "Custom proxies", "Proxy bypass hosts", "GPU"} + tests := []struct { + entitlement string + set func(*kernel.OrgEntitlements) + }{ + {"Trialing", func(e *kernel.OrgEntitlements) { e.Plan.IsTrialing = true }}, + {"Profiles", func(e *kernel.OrgEntitlements) { e.Features.Profiles.Enabled = true }}, + {"File I/O", func(e *kernel.OrgEntitlements) { e.Features.FileIo.Enabled = true }}, + {"Browser replays", func(e *kernel.OrgEntitlements) { e.Features.BrowserReplays.Enabled = true }}, + {"Browser extensions", func(e *kernel.OrgEntitlements) { e.Features.BrowserExtensions.Enabled = true }}, + {"Browser pools", func(e *kernel.OrgEntitlements) { e.Features.BrowserPools.Enabled = true }}, + {"Managed auth", func(e *kernel.OrgEntitlements) { e.Features.ManagedAuth.Enabled = true }}, + {"Credentials", func(e *kernel.OrgEntitlements) { e.Features.Credentials.Enabled = true }}, + {"Credential providers", func(e *kernel.OrgEntitlements) { e.Features.CredentialProviders.Enabled = true }}, + {"Managed proxies", func(e *kernel.OrgEntitlements) { e.Features.ManagedProxies.Enabled = true }}, + {"Custom proxies", func(e *kernel.OrgEntitlements) { e.Features.CustomProxies.Enabled = true }}, + {"Proxy bypass hosts", func(e *kernel.OrgEntitlements) { e.Features.ProxyBypassHosts.Enabled = true }}, + {"GPU", func(e *kernel.OrgEntitlements) { e.Features.GPU.Enabled = true }}, + } + + for _, tt := range tests { + t.Run(tt.entitlement, func(t *testing.T) { + var entitlements kernel.OrgEntitlements + tt.set(&entitlements) + rows := orgEntitlementRows(&entitlements) + values := make(map[string]string, len(rows)) + for _, row := range rows { + values[row[1]] = row[2] + } + + for _, entitlement := range booleanEntitlements { + expected := "false" + if entitlement == tt.entitlement { + expected = "true" + } + assert.Equal(t, expected, values[entitlement], entitlement) + } + }) + } +} + func TestOrgEntitlementRows_NullableFieldStates(t *testing.T) { populatedTrialEnd, err := time.Parse(time.RFC3339, "2031-02-03T04:05:06Z") assert.NoError(t, err) From 4961b765399465608c246189bb015c031643058a Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:19:52 -0400 Subject: [PATCH 7/7] Simplify entitlement projection test --- cmd/org_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/org_test.go b/cmd/org_test.go index 30f2825a..e55713fd 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -110,7 +110,6 @@ func TestOrgEntitlementRows_CompleteProjection(t *testing.T) { } func TestOrgEntitlementRows_BooleanFieldProvenance(t *testing.T) { - booleanEntitlements := []string{"Trialing", "Profiles", "File I/O", "Browser replays", "Browser extensions", "Browser pools", "Managed auth", "Credentials", "Credential providers", "Managed proxies", "Custom proxies", "Proxy bypass hosts", "GPU"} tests := []struct { entitlement string set func(*kernel.OrgEntitlements) @@ -140,12 +139,12 @@ func TestOrgEntitlementRows_BooleanFieldProvenance(t *testing.T) { values[row[1]] = row[2] } - for _, entitlement := range booleanEntitlements { + for _, candidate := range tests { expected := "false" - if entitlement == tt.entitlement { + if candidate.entitlement == tt.entitlement { expected = "true" } - assert.Equal(t, expected, values[entitlement], entitlement) + assert.Equal(t, expected, values[candidate.entitlement], candidate.entitlement) } }) }