From ac70f92787df383c1bd671f546aab72973e6e3a0 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 7 May 2026 11:03:10 +0200 Subject: [PATCH 1/6] Add selftest tui scenarios for every cmdio prompt entry point Each scenario is a one-shot subcommand under `databricks selftest tui` that exercises a single cmdio helper with the simplest meaningful inputs; flags layer in customization. Used for manual visual confirmation across terminals and to eyeball the visual diff when prompt rendering changes. Coverage: ask, ask-yes-no, ask-select, prompt (--default, --mask, --validate), secret, select (--n), select-ordered (--n, --long, --filter), run-select (--rich, --conditional), spinner (--elapsed), colors. Fixture data is drawn from the public Databricks docs so the demo looks like content a user would actually encounter. Co-authored-by: Isaac --- cmd/selftest/tui/ask.go | 61 ++++++++++++ cmd/selftest/tui/colors.go | 25 +++++ cmd/selftest/tui/fixtures.go | 175 +++++++++++++++++++++++++++++++++++ cmd/selftest/tui/prompt.go | 54 +++++++++++ cmd/selftest/tui/secret.go | 24 +++++ cmd/selftest/tui/select.go | 152 ++++++++++++++++++++++++++++++ cmd/selftest/tui/spinner.go | 32 +++---- cmd/selftest/tui/tui.go | 14 ++- 8 files changed, 518 insertions(+), 19 deletions(-) create mode 100644 cmd/selftest/tui/ask.go create mode 100644 cmd/selftest/tui/colors.go create mode 100644 cmd/selftest/tui/fixtures.go create mode 100644 cmd/selftest/tui/prompt.go create mode 100644 cmd/selftest/tui/secret.go create mode 100644 cmd/selftest/tui/select.go diff --git a/cmd/selftest/tui/ask.go b/cmd/selftest/tui/ask.go new file mode 100644 index 0000000000..bd443b80e4 --- /dev/null +++ b/cmd/selftest/tui/ask.go @@ -0,0 +1,61 @@ +package tui + +import ( + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" +) + +func newAskCmd() *cobra.Command { + var defaultVal string + cmd := &cobra.Command{ + Use: "ask", + Short: "cmdio.Ask (single-line text input with optional default)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + ans, err := cmdio.Ask(ctx, "Enter a value", defaultVal) + if err != nil { + return err + } + cmdio.LogString(ctx, "Entered: "+ans) + return nil + }, + } + cmd.Flags().StringVar(&defaultVal, "default", "", "default returned if the user just presses Enter") + return cmd +} + +func newAskYesOrNoCmd() *cobra.Command { + return &cobra.Command{ + Use: "ask-yes-no", + Short: "cmdio.AskYesOrNo (yes/no question)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + ans, err := cmdio.AskYesOrNo(ctx, "Continue") + if err != nil { + return err + } + if ans { + cmdio.LogString(ctx, "Answer: yes") + } else { + cmdio.LogString(ctx, "Answer: no") + } + return nil + }, + } +} + +func newAskSelectCmd() *cobra.Command { + return &cobra.Command{ + Use: "ask-select", + Short: "cmdio.AskSelect (pick one of a fixed list of strings)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + ans, err := cmdio.AskSelect(ctx, "Choose a deployment strategy?", deploymentChoices) + if err != nil { + return err + } + cmdio.LogString(ctx, "Selected: "+ans) + return nil + }, + } +} diff --git a/cmd/selftest/tui/colors.go b/cmd/selftest/tui/colors.go new file mode 100644 index 0000000000..9afc6fd693 --- /dev/null +++ b/cmd/selftest/tui/colors.go @@ -0,0 +1,25 @@ +package tui + +import ( + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" +) + +func newColorsCmd() *cobra.Command { + return &cobra.Command{ + Use: "colors", + Short: "Print colored text to verify color support", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + swatch := "the quick brown fox jumps over the lazy dog" + cmdio.LogString(ctx, "red: "+cmdio.Red(ctx, swatch)) + cmdio.LogString(ctx, "green: "+cmdio.Green(ctx, swatch)) + cmdio.LogString(ctx, "yellow: "+cmdio.Yellow(ctx, swatch)) + cmdio.LogString(ctx, "blue: "+cmdio.Blue(ctx, swatch)) + cmdio.LogString(ctx, "cyan: "+cmdio.Cyan(ctx, swatch)) + cmdio.LogString(ctx, "hiblack: "+cmdio.HiBlack(ctx, swatch)) + cmdio.LogString(ctx, "hiblue: "+cmdio.HiBlue(ctx, swatch)) + return nil + }, + } +} diff --git a/cmd/selftest/tui/fixtures.go b/cmd/selftest/tui/fixtures.go new file mode 100644 index 0000000000..4f33389189 --- /dev/null +++ b/cmd/selftest/tui/fixtures.go @@ -0,0 +1,175 @@ +package tui + +import ( + "fmt" + "time" + + "github.com/databricks/cli/libs/cmdio" +) + +// deploymentChoices feeds the AskSelect scenario; the entries vary in +// length so the rendering is tested across short, medium, and wrap-width +// options. +var deploymentChoices = []string{ + "Deploy bundle with Terraform", + "Deploy bundle with the direct engine", + "Validate bundle configuration", + "Generate deployment artifacts only", + "Run pre-deploy checks", + "Skip deploy and tail logs", + "Cancel", + "Show me the full plan output before deciding", +} + +type spinnerMessage struct { + text string + duration time.Duration +} + +var spinnerMessages = []spinnerMessage{ + {"Initializing...", time.Second}, + {"Loading configuration", time.Second}, + {"Connecting to workspace", time.Second}, + {"Processing files", time.Second}, + {"Finalizing", time.Second}, +} + +// databricksFeatures is a stable list of Databricks product / feature names +// used as fixture data for the prompt scenarios. Drawn from the public docs +// (https://docs.databricks.com) so the demo data looks like something a user +// would actually encounter. +var databricksFeatures = []string{ + "unity-catalog", + "delta-lake", + "delta-sharing", + "photon", + "mlflow", + "mosaic-ai", + "genie", + "lakeflow-connect", + "lakeflow-jobs", + "vector-search", + "model-serving", + "feature-store", + "databricks-sql", + "ai-playground", + "foundation-models", + "lakehouse-monitoring", + "liquid-clustering", + "predictive-optimization", + "governed-tags", + "lakeflow-designer", +} + +// buildItems uses zero-padded ids so the alphabetical Select scenario has +// a stable sort order. +func buildItems(n int) []cmdio.Tuple { + n = min(n, len(databricksFeatures)) + items := make([]cmdio.Tuple, 0, n) + for i := range n { + items = append(items, cmdio.Tuple{ + Name: databricksFeatures[i], + Id: fmt.Sprintf("id-%02d", i+1), + }) + } + return items +} + +// buildFilterItems returns 15 items where 5 share the substring "lake", so +// progressive typing narrows the list, and a non-matching substring ("xyz") +// hits the "No results" path. +func buildFilterItems() []cmdio.Tuple { + names := []string{ + "lakehouse-monitoring", + "lakeflow-connect", + "lakeflow-jobs", + "delta-lake", + "lakebase-postgres", + "unity-catalog", + "mosaic-ai", + "vector-search", + "model-serving", + "feature-store", + "ai-playground", + "genie-spaces", + "mlflow-tracking", + "liquid-clustering", + "predictive-optimization", + } + items := make([]cmdio.Tuple, 0, len(names)) + for i, name := range names { + items = append(items, cmdio.Tuple{ + Name: name, + Id: fmt.Sprintf("id-%02d", i+1), + }) + } + return items +} + +// buildLongItems uses fully-qualified workspace URLs as ids so that the +// rendered field overflows a typical terminal width. +func buildLongItems() []cmdio.Tuple { + hosts := []string{ + "https://adb-1234567890123456.78.azuredatabricks.net/?o=1234567890123456", + "https://adb-2345678901234567.89.azuredatabricks.net/?o=2345678901234567", + "https://acme-prod.cloud.databricks.com/?o=3456789012345678", + "https://acme-staging.cloud.databricks.com/?o=4567890123456789", + "https://acme-dev.cloud.databricks.com/?o=5678901234567890", + "https://1234567890123456.7.gcp.databricks.com/?o=6789012345678901", + "https://2345678901234567.8.gcp.databricks.com/?o=7890123456789012", + "https://field-eng-east.cloud.databricks.com/?o=8901234567890123", + } + items := make([]cmdio.Tuple, 0, len(hosts)) + for i, host := range hosts { + items = append(items, cmdio.Tuple{ + Name: fmt.Sprintf("workspace-%02d", i+1), + Id: host, + }) + } + return items +} + +// clusterItem mirrors the shape used by libs/databrickscfg/cfgpickers/clusters.go, +// where Active/Inactive templates reference Name, State, Runtime, and Id. +type clusterItem struct { + Name string + State string + Runtime string + Id string +} + +func buildClusterItems() []clusterItem { + return []clusterItem{ + {Name: "shared-autoscaling-prod", State: "RUNNING", Runtime: "DBR 14.3 LTS", Id: "0123-456789-abcdef01"}, + {Name: "ml-gpu-experiments", State: "TERMINATED", Runtime: "DBR 15.0 ML", Id: "0123-456789-abcdef02"}, + {Name: "job-compute-bronze-etl", State: "RUNNING", Runtime: "DBR 13.3 LTS", Id: "0123-456789-abcdef03"}, + {Name: "interactive-analytics", State: "PENDING", Runtime: "DBR 14.3", Id: "0123-456789-abcdef04"}, + {Name: "photon-streaming-realtime", State: "RUNNING", Runtime: "DBR 14.3 Photon", Id: "0123-456789-abcdef05"}, + {Name: "single-node-dev", State: "TERMINATED", Runtime: "DBR 14.3 LTS", Id: "0123-456789-abcdef06"}, + {Name: "all-purpose-shared", State: "RUNNING", Runtime: "DBR 15.0", Id: "0123-456789-abcdef07"}, + {Name: "legacy-data-eng", State: "TERMINATED", Runtime: "DBR 12.2 LTS", Id: "0123-456789-abcdef08"}, + } +} + +// profileItem mirrors the profile picker in cmd/auth/token.go: regular items +// have a Host, the trailing meta items do not (so the {{if .Host}} branch fires). +type profileItem struct { + Name string + Host string +} + +// buildProfileItems returns 6 profile-shaped items across AWS / Azure / GCP +// hosts plus the two trailing meta-rows ("Create a new profile", "Enter a +// host URL manually") used by the real profile picker. +func buildProfileItems() []profileItem { + return []profileItem{ + {Name: "DEFAULT", Host: "https://acme.cloud.databricks.com"}, + {Name: "production", Host: "https://acme-prod.cloud.databricks.com"}, + {Name: "staging", Host: "https://acme-stg.cloud.databricks.com"}, + {Name: "field-eng", Host: "https://field-eng.cloud.databricks.com"}, + {Name: "azure-personal", Host: "https://adb-1234567890123456.78.azuredatabricks.net"}, + {Name: "gcp-sandbox", Host: "https://1234567890123456.7.gcp.databricks.com"}, + {Name: "Create a new profile"}, + {Name: "Enter a host URL manually"}, + } +} diff --git a/cmd/selftest/tui/prompt.go b/cmd/selftest/tui/prompt.go new file mode 100644 index 0000000000..d1110a7957 --- /dev/null +++ b/cmd/selftest/tui/prompt.go @@ -0,0 +1,54 @@ +package tui + +import ( + "errors" + "fmt" + "strings" + + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" +) + +func newPromptCmd() *cobra.Command { + var ( + defaultVal string + mask bool + validate bool + ) + cmd := &cobra.Command{ + Use: "prompt", + Short: "cmdio.RunPrompt (single-line text input)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + opts := cmdio.PromptOptions{ + Label: "Enter a value", + Default: defaultVal, + } + if mask { + opts.Mask = '*' + } + if validate { + opts.Validate = func(input string) error { + if !strings.Contains(input, "://") { + return errors.New("value must contain '://'") + } + return nil + } + } + value, err := cmdio.RunPrompt(ctx, opts) + if err != nil { + return err + } + if mask { + cmdio.LogString(ctx, fmt.Sprintf("Entered %d characters", len(value))) + return nil + } + cmdio.LogString(ctx, "Entered: "+value) + return nil + }, + } + cmd.Flags().StringVar(&defaultVal, "default", "", "pre-fill the input with this value") + cmd.Flags().BoolVar(&mask, "mask", false, "echo input as '*'") + cmd.Flags().BoolVar(&validate, "validate", false, "require '://' in input") + return cmd +} diff --git a/cmd/selftest/tui/secret.go b/cmd/selftest/tui/secret.go new file mode 100644 index 0000000000..e02b90c6e2 --- /dev/null +++ b/cmd/selftest/tui/secret.go @@ -0,0 +1,24 @@ +package tui + +import ( + "fmt" + + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" +) + +func newSecretCmd() *cobra.Command { + return &cobra.Command{ + Use: "secret", + Short: "cmdio.Secret (masked password input)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + value, err := cmdio.Secret(ctx, "Personal access token") + if err != nil { + return err + } + cmdio.LogString(ctx, fmt.Sprintf("Entered %d characters", len(value))) + return nil + }, + } +} diff --git a/cmd/selftest/tui/select.go b/cmd/selftest/tui/select.go new file mode 100644 index 0000000000..11a94e9678 --- /dev/null +++ b/cmd/selftest/tui/select.go @@ -0,0 +1,152 @@ +package tui + +import ( + "context" + "strings" + + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" +) + +func newSelectCmd() *cobra.Command { + var n int + cmd := &cobra.Command{ + Use: "select", + Short: "cmdio.Select (map; sorted alphabetically by name)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + tuples := buildItems(n) + items := make(map[string]string, len(tuples)) + for _, t := range tuples { + items[t.Name] = t.Id + } + id, err := cmdio.Select(ctx, items, "Pick an item") + if err != nil { + return err + } + cmdio.LogString(ctx, "Selected: "+id) + return nil + }, + } + cmd.Flags().IntVar(&n, "n", 5, "number of items") + return cmd +} + +func newSelectOrderedCmd() *cobra.Command { + var ( + n int + long bool + filter bool + ) + cmd := &cobra.Command{ + Use: "select-ordered", + Short: "cmdio.SelectOrdered ([]Tuple; preserves insertion order)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + var items []cmdio.Tuple + switch { + case filter: + items = buildFilterItems() + case long: + items = buildLongItems() + default: + items = buildItems(n) + } + id, err := cmdio.SelectOrdered(ctx, items, "Pick an item") + if err != nil { + return err + } + cmdio.LogString(ctx, "Selected: "+id) + return nil + }, + } + cmd.Flags().IntVar(&n, "n", 5, "number of items (ignored with --long or --filter)") + cmd.Flags().BoolVar(&long, "long", false, "use 8 items with 60+ char ids that overflow the terminal") + cmd.Flags().BoolVar(&filter, "filter", false, "use 15 items with overlapping substrings (try typing 'al' or 'xyz')") + cmd.MarkFlagsMutuallyExclusive("long", "filter") + return cmd +} + +func newRunSelectCmd() *cobra.Command { + var ( + rich bool + conditional bool + ) + cmd := &cobra.Command{ + Use: "run-select", + Short: "cmdio.RunSelect (custom SelectOptions)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + switch { + case rich: + return runSelectRich(ctx) + case conditional: + return runSelectProfile(ctx) + default: + return runSelectPlain(ctx) + } + }, + } + cmd.Flags().BoolVar(&rich, "rich", false, "use cluster-style rich Active/Inactive templates (bold + faint)") + cmd.Flags().BoolVar(&conditional, "conditional", false, "use profile-style {{if .Host}} template branches and trailing meta-rows") + cmd.MarkFlagsMutuallyExclusive("rich", "conditional") + return cmd +} + +// runSelectPlain exercises RunSelect with the minimum required options. +func runSelectPlain(ctx context.Context) error { + items := buildItems(5) + i, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ + Label: "Pick an item", + Items: items, + }) + if err != nil { + return err + } + cmdio.LogString(ctx, "Selected: "+items[i].Id) + return nil +} + +func runSelectRich(ctx context.Context) error { + items := buildClusterItems() + i, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ + Label: "Choose a cluster", + Items: items, + Searcher: func(input string, idx int) bool { + return strings.Contains(strings.ToLower(items[idx].Name), strings.ToLower(input)) + }, + StartInSearchMode: true, + LabelTemplate: `{{ . | faint }}`, + Active: `{{.Name | bold}} ({{.State}} Runtime {{.Runtime}}) ({{.Id | faint}})`, + Inactive: `{{.Name}} ({{.State}} Runtime {{.Runtime}})`, + Selected: `{{ "Selected cluster" | faint }}: {{ .Name | bold }} ({{ .Id | faint }})`, + }) + if err != nil { + return err + } + cmdio.LogString(ctx, "Selected: "+items[i].Name+" ("+items[i].Id+")") + return nil +} + +func runSelectProfile(ctx context.Context) error { + items := buildProfileItems() + i, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ + Label: "Select a profile", + Items: items, + StartInSearchMode: true, + Searcher: func(input string, idx int) bool { + input = strings.ToLower(input) + return strings.Contains(strings.ToLower(items[idx].Name), input) || + strings.Contains(strings.ToLower(items[idx].Host), input) + }, + LabelTemplate: `{{ . | faint }}`, + Active: `{{.Name | bold}}{{if .Host}} ({{.Host | faint}}){{end}}`, + Inactive: `{{.Name}}{{if .Host}} ({{.Host}}){{end}}`, + Selected: `{{ "Using profile" | faint }}: {{ .Name | bold }}`, + }) + if err != nil { + return err + } + cmdio.LogString(ctx, "Selected: "+items[i].Name) + return nil +} diff --git a/cmd/selftest/tui/spinner.go b/cmd/selftest/tui/spinner.go index a53591f315..799ce0585d 100644 --- a/cmd/selftest/tui/spinner.go +++ b/cmd/selftest/tui/spinner.go @@ -7,35 +7,31 @@ import ( "github.com/spf13/cobra" ) -func newSpinner() *cobra.Command { - return &cobra.Command{ +func newSpinnerCmd() *cobra.Command { + var elapsed bool + cmd := &cobra.Command{ Use: "spinner", - Short: "Test the cmdio spinner component", - Run: func(cmd *cobra.Command, args []string) { + Short: "cmdio.NewSpinner (progress indicator)", + RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - sp := cmdio.NewSpinner(ctx) - - // Test various status messages - messages := []struct { - text string - duration time.Duration - }{ - {"Initializing...", time.Second}, - {"Loading configuration", time.Second}, - {"Connecting to workspace", time.Second}, - {"Processing files", time.Second}, - {"Finalizing", time.Second}, + var opts []cmdio.SpinnerOption + if elapsed { + opts = append(opts, cmdio.WithElapsedTime()) } + sp := cmdio.NewSpinner(ctx, opts...) - for _, msg := range messages { + for _, msg := range spinnerMessages { sp.Update(msg.text) time.Sleep(msg.duration) } sp.Close() - cmdio.LogString(ctx, "✓ Spinner test complete") + cmdio.LogString(ctx, "Spinner test complete") + return nil }, } + cmd.Flags().BoolVar(&elapsed, "elapsed", false, "show an MM:SS elapsed-time prefix on the spinner") + return cmd } diff --git a/cmd/selftest/tui/tui.go b/cmd/selftest/tui/tui.go index 5a3f9aad1b..7317e279be 100644 --- a/cmd/selftest/tui/tui.go +++ b/cmd/selftest/tui/tui.go @@ -8,6 +8,18 @@ func New() *cobra.Command { Short: "Test terminal UI components (spinners, prompts, etc.)", } - cmd.AddCommand(newSpinner()) + cmd.AddCommand( + newAskCmd(), + newAskSelectCmd(), + newAskYesOrNoCmd(), + newColorsCmd(), + newPromptCmd(), + newRunSelectCmd(), + newSecretCmd(), + newSelectCmd(), + newSelectOrderedCmd(), + newSpinnerCmd(), + ) + return cmd } From 670c0d2e31b60dcd8c6d492ea70629fec81039ad Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 7 May 2026 14:50:10 +0200 Subject: [PATCH 2/6] Address review feedback on selftest tui - Validate --n > 0 in cobra PreRunE (both `select` and `select-ordered`). Previously a negative --n flowed into make([]Tuple, 0, n) and panicked. - Mirror the production cluster picker more faithfully in --rich: expose State/Access/Runtime as methods on clusterItem so the templates exercise text/template's method-resolution path, and have State() return a pre-colored string via cmdio.Green/Red/Blue (matching the renderedState cache on libs/databrickscfg/cfgpickers/clusters.go). Co-authored-by: Isaac --- cmd/selftest/tui/fixtures.go | 44 ++++++++++++++++++++++++------------ cmd/selftest/tui/select.go | 21 +++++++++++++---- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/cmd/selftest/tui/fixtures.go b/cmd/selftest/tui/fixtures.go index 4f33389189..7fd233e607 100644 --- a/cmd/selftest/tui/fixtures.go +++ b/cmd/selftest/tui/fixtures.go @@ -1,6 +1,7 @@ package tui import ( + "context" "fmt" "time" @@ -129,25 +130,38 @@ func buildLongItems() []cmdio.Tuple { return items } -// clusterItem mirrors the shape used by libs/databrickscfg/cfgpickers/clusters.go, -// where Active/Inactive templates reference Name, State, Runtime, and Id. +// clusterItem mirrors libs/databrickscfg/cfgpickers/clusters.go's +// compatibleCluster: State, Access, and Runtime are exposed as methods so +// the Active/Inactive templates exercise text/template's method-resolution +// path, and State returns a pre-rendered colored string (matching the +// renderedState cache in production) so the demo also exercises ANSI codes +// emitted from inside a template. type clusterItem struct { - Name string - State string - Runtime string - Id string + Name string + Id string + + access string + runtimeName string + renderedState string } -func buildClusterItems() []clusterItem { +func (c clusterItem) Access() string { return c.access } +func (c clusterItem) Runtime() string { return c.runtimeName } +func (c clusterItem) State() string { return c.renderedState } + +func buildClusterItems(ctx context.Context) []clusterItem { + green := func(s string) string { return cmdio.Green(ctx, s) } + red := func(s string) string { return cmdio.Red(ctx, s) } + blue := func(s string) string { return cmdio.Blue(ctx, s) } return []clusterItem{ - {Name: "shared-autoscaling-prod", State: "RUNNING", Runtime: "DBR 14.3 LTS", Id: "0123-456789-abcdef01"}, - {Name: "ml-gpu-experiments", State: "TERMINATED", Runtime: "DBR 15.0 ML", Id: "0123-456789-abcdef02"}, - {Name: "job-compute-bronze-etl", State: "RUNNING", Runtime: "DBR 13.3 LTS", Id: "0123-456789-abcdef03"}, - {Name: "interactive-analytics", State: "PENDING", Runtime: "DBR 14.3", Id: "0123-456789-abcdef04"}, - {Name: "photon-streaming-realtime", State: "RUNNING", Runtime: "DBR 14.3 Photon", Id: "0123-456789-abcdef05"}, - {Name: "single-node-dev", State: "TERMINATED", Runtime: "DBR 14.3 LTS", Id: "0123-456789-abcdef06"}, - {Name: "all-purpose-shared", State: "RUNNING", Runtime: "DBR 15.0", Id: "0123-456789-abcdef07"}, - {Name: "legacy-data-eng", State: "TERMINATED", Runtime: "DBR 12.2 LTS", Id: "0123-456789-abcdef08"}, + {Name: "shared-autoscaling-prod", Id: "0123-456789-abcdef01", access: "Shared", runtimeName: "DBR 14.3 LTS", renderedState: green("RUNNING")}, + {Name: "ml-gpu-experiments", Id: "0123-456789-abcdef02", access: "Assigned", runtimeName: "DBR 15.0 ML", renderedState: red("TERMINATED")}, + {Name: "job-compute-bronze-etl", Id: "0123-456789-abcdef03", access: "Shared", runtimeName: "DBR 13.3 LTS", renderedState: green("RUNNING")}, + {Name: "interactive-analytics", Id: "0123-456789-abcdef04", access: "Assigned", runtimeName: "DBR 14.3", renderedState: blue("PENDING")}, + {Name: "photon-streaming-realtime", Id: "0123-456789-abcdef05", access: "Shared", runtimeName: "DBR 14.3 Photon", renderedState: green("RUNNING")}, + {Name: "single-node-dev", Id: "0123-456789-abcdef06", access: "Assigned", runtimeName: "DBR 14.3 LTS", renderedState: red("TERMINATED")}, + {Name: "all-purpose-shared", Id: "0123-456789-abcdef07", access: "Shared", runtimeName: "DBR 15.0", renderedState: green("RUNNING")}, + {Name: "legacy-data-eng", Id: "0123-456789-abcdef08", access: "Assigned", runtimeName: "DBR 12.2 LTS", renderedState: red("TERMINATED")}, } } diff --git a/cmd/selftest/tui/select.go b/cmd/selftest/tui/select.go index 11a94e9678..6a597ac27b 100644 --- a/cmd/selftest/tui/select.go +++ b/cmd/selftest/tui/select.go @@ -2,17 +2,28 @@ package tui import ( "context" + "fmt" "strings" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) +func validatePositive(n int) error { + if n < 1 { + return fmt.Errorf("--n must be at least 1, got %d", n) + } + return nil +} + func newSelectCmd() *cobra.Command { var n int cmd := &cobra.Command{ Use: "select", Short: "cmdio.Select (map; sorted alphabetically by name)", + PreRunE: func(cmd *cobra.Command, args []string) error { + return validatePositive(n) + }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() tuples := buildItems(n) @@ -41,6 +52,9 @@ func newSelectOrderedCmd() *cobra.Command { cmd := &cobra.Command{ Use: "select-ordered", Short: "cmdio.SelectOrdered ([]Tuple; preserves insertion order)", + PreRunE: func(cmd *cobra.Command, args []string) error { + return validatePositive(n) + }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() var items []cmdio.Tuple @@ -93,7 +107,6 @@ func newRunSelectCmd() *cobra.Command { return cmd } -// runSelectPlain exercises RunSelect with the minimum required options. func runSelectPlain(ctx context.Context) error { items := buildItems(5) i, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ @@ -108,7 +121,7 @@ func runSelectPlain(ctx context.Context) error { } func runSelectRich(ctx context.Context) error { - items := buildClusterItems() + items := buildClusterItems(ctx) i, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ Label: "Choose a cluster", Items: items, @@ -117,8 +130,8 @@ func runSelectRich(ctx context.Context) error { }, StartInSearchMode: true, LabelTemplate: `{{ . | faint }}`, - Active: `{{.Name | bold}} ({{.State}} Runtime {{.Runtime}}) ({{.Id | faint}})`, - Inactive: `{{.Name}} ({{.State}} Runtime {{.Runtime}})`, + Active: `{{.Name | bold}} ({{.State}} {{.Access}} Runtime {{.Runtime}}) ({{.Id | faint}})`, + Inactive: `{{.Name}} ({{.State}} {{.Access}} Runtime {{.Runtime}})`, Selected: `{{ "Selected cluster" | faint }}: {{ .Name | bold }} ({{ .Id | faint }})`, }) if err != nil { From e2b1d6399dddda182c9916bb4c82f6d619c919f2 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 8 May 2026 12:13:54 +0200 Subject: [PATCH 3/6] Drop ask-select selftest scenario cmdio.AskSelect is being removed in #5219 (its only caller migrated to RunSelect with the new HideHelp option), so this scenario would no longer compile against main once that lands. Co-authored-by: Isaac --- cmd/selftest/tui/ask.go | 15 --------------- cmd/selftest/tui/fixtures.go | 14 -------------- cmd/selftest/tui/tui.go | 1 - 3 files changed, 30 deletions(-) diff --git a/cmd/selftest/tui/ask.go b/cmd/selftest/tui/ask.go index bd443b80e4..87da44af77 100644 --- a/cmd/selftest/tui/ask.go +++ b/cmd/selftest/tui/ask.go @@ -44,18 +44,3 @@ func newAskYesOrNoCmd() *cobra.Command { } } -func newAskSelectCmd() *cobra.Command { - return &cobra.Command{ - Use: "ask-select", - Short: "cmdio.AskSelect (pick one of a fixed list of strings)", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - ans, err := cmdio.AskSelect(ctx, "Choose a deployment strategy?", deploymentChoices) - if err != nil { - return err - } - cmdio.LogString(ctx, "Selected: "+ans) - return nil - }, - } -} diff --git a/cmd/selftest/tui/fixtures.go b/cmd/selftest/tui/fixtures.go index 7fd233e607..01cd2873ab 100644 --- a/cmd/selftest/tui/fixtures.go +++ b/cmd/selftest/tui/fixtures.go @@ -8,20 +8,6 @@ import ( "github.com/databricks/cli/libs/cmdio" ) -// deploymentChoices feeds the AskSelect scenario; the entries vary in -// length so the rendering is tested across short, medium, and wrap-width -// options. -var deploymentChoices = []string{ - "Deploy bundle with Terraform", - "Deploy bundle with the direct engine", - "Validate bundle configuration", - "Generate deployment artifacts only", - "Run pre-deploy checks", - "Skip deploy and tail logs", - "Cancel", - "Show me the full plan output before deciding", -} - type spinnerMessage struct { text string duration time.Duration diff --git a/cmd/selftest/tui/tui.go b/cmd/selftest/tui/tui.go index 7317e279be..f1b807fc93 100644 --- a/cmd/selftest/tui/tui.go +++ b/cmd/selftest/tui/tui.go @@ -10,7 +10,6 @@ func New() *cobra.Command { cmd.AddCommand( newAskCmd(), - newAskSelectCmd(), newAskYesOrNoCmd(), newColorsCmd(), newPromptCmd(), From 7898a6950b7a237615b6e036624b79a329905953 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 8 May 2026 12:19:20 +0200 Subject: [PATCH 4/6] selftest tui: skip --n validation when --long or --filter is set The flag help on `select-ordered` already documents that `--n` is ignored when `--long` or `--filter` selects its own fixture, but the PreRunE validator was rejecting `--long --n=0` anyway. Skip the check in those modes. Co-authored-by: Isaac --- cmd/selftest/tui/select.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/selftest/tui/select.go b/cmd/selftest/tui/select.go index 6a597ac27b..d7d862a1be 100644 --- a/cmd/selftest/tui/select.go +++ b/cmd/selftest/tui/select.go @@ -53,6 +53,9 @@ func newSelectOrderedCmd() *cobra.Command { Use: "select-ordered", Short: "cmdio.SelectOrdered ([]Tuple; preserves insertion order)", PreRunE: func(cmd *cobra.Command, args []string) error { + if long || filter { + return nil + } return validatePositive(n) }, RunE: func(cmd *cobra.Command, args []string) error { From 2815e68829bcd6ada5743356cd683a9ef15c2fc6 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 8 May 2026 12:21:31 +0200 Subject: [PATCH 5/6] selftest tui: gofmt Drop the trailing blank line left after removing newAskSelectCmd. Co-authored-by: Isaac --- cmd/selftest/tui/ask.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/selftest/tui/ask.go b/cmd/selftest/tui/ask.go index 87da44af77..1d64eecde6 100644 --- a/cmd/selftest/tui/ask.go +++ b/cmd/selftest/tui/ask.go @@ -43,4 +43,3 @@ func newAskYesOrNoCmd() *cobra.Command { }, } } - From 031c1fe9c56f90c0c9f718350d2445e3fa38e4dc Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 8 May 2026 14:05:28 +0200 Subject: [PATCH 6/6] selftest tui: fold secret.go into prompt.go cmdio.Secret is a tiny wrapper over the same prompt machinery; keeping a dedicated file for one 13-line function added more navigation overhead than it saved. Co-authored-by: Isaac --- cmd/selftest/tui/prompt.go | 16 ++++++++++++++++ cmd/selftest/tui/secret.go | 24 ------------------------ 2 files changed, 16 insertions(+), 24 deletions(-) delete mode 100644 cmd/selftest/tui/secret.go diff --git a/cmd/selftest/tui/prompt.go b/cmd/selftest/tui/prompt.go index d1110a7957..ed663116d0 100644 --- a/cmd/selftest/tui/prompt.go +++ b/cmd/selftest/tui/prompt.go @@ -52,3 +52,19 @@ func newPromptCmd() *cobra.Command { cmd.Flags().BoolVar(&validate, "validate", false, "require '://' in input") return cmd } + +func newSecretCmd() *cobra.Command { + return &cobra.Command{ + Use: "secret", + Short: "cmdio.Secret (masked password input)", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + value, err := cmdio.Secret(ctx, "Personal access token") + if err != nil { + return err + } + cmdio.LogString(ctx, fmt.Sprintf("Entered %d characters", len(value))) + return nil + }, + } +} diff --git a/cmd/selftest/tui/secret.go b/cmd/selftest/tui/secret.go deleted file mode 100644 index e02b90c6e2..0000000000 --- a/cmd/selftest/tui/secret.go +++ /dev/null @@ -1,24 +0,0 @@ -package tui - -import ( - "fmt" - - "github.com/databricks/cli/libs/cmdio" - "github.com/spf13/cobra" -) - -func newSecretCmd() *cobra.Command { - return &cobra.Command{ - Use: "secret", - Short: "cmdio.Secret (masked password input)", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - value, err := cmdio.Secret(ctx, "Personal access token") - if err != nil { - return err - } - cmdio.LogString(ctx, fmt.Sprintf("Entered %d characters", len(value))) - return nil - }, - } -}