Skip to content

Commit df5ae0d

Browse files
authored
cmdiotest: add black-box test suite for cmdio prompts (#5248)
## Summary Adds `libs/cmdio/cmdiotest`: a pipe-backed harness plus 41 baseline tests covering `cmdio.RunPrompt`, `Secret`, `RunSelect`, and `SelectOrdered`. Each test drives the entry point through a pair of `os.Pipe`s, captures the rendered screen via an in-process VT emulator, and pins both the visible output (golden snapshots) and the returned `(value, err)`. No pty required, so the suite runs on every supported OS without `term.OpenPTY` quirks. Background: #5231 sketched a similar baseline against promptui using a pty + vt10x. That PR is a point-in-time snapshot and will be closed; this PR is a fresh net-add against `main`, targeting the current bubbletea-backed implementation from #5232. The golden screen snapshots are byte-identical to those captured against promptui in #5231 — concrete evidence that the bubbletea rewrite preserved the visible behavior. Properties of the new suite: - All 41 tests use `t.Parallel()`; wall time is ~0.6s, no flakes across `-race` × 3 and sequential × 10. - `Term[T]` is generic over result type — one constructor per cmdio entry point, one `Result()` method. - Two small test-only hooks on `cmdIO` (`teaWindowSize`, `teaFPS`); both zero-valued and inert outside `NewTestIO`, so production code paths are unchanged. ## Test plan - [x] `go test ./libs/cmdio/...` passes - [x] `go test -race ./libs/cmdio/cmdiotest/...` passes This pull request and its description were written by Isaac.
1 parent 01cd7e0 commit df5ae0d

168 files changed

Lines changed: 3132 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

NOTICE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ charmbracelet/lipgloss - https://github.com/charmbracelet/lipgloss
139139
Copyright (c) 2021-2025 Charmbracelet, Inc
140140
License - https://github.com/charmbracelet/lipgloss/blob/master/LICENSE
141141

142+
charmbracelet/x/ansi - https://github.com/charmbracelet/x
143+
Copyright (c) 2023-2025 Charmbracelet, Inc
144+
License - https://github.com/charmbracelet/x/blob/main/ansi/LICENSE
145+
142146
Masterminds/semver - https://github.com/Masterminds/semver
143147
Copyright (C) 2014-2019, Matt Butcher and Matt Farina
144148
License - https://github.com/Masterminds/semver/blob/master/LICENSE.txt

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/charmbracelet/bubbletea v1.3.10 // MIT
1313
github.com/charmbracelet/huh v1.0.0 // MIT
1414
github.com/charmbracelet/lipgloss v1.1.0 // MIT
15+
github.com/charmbracelet/x/ansi v0.11.6 // MIT
1516
github.com/databricks/databricks-sdk-go v0.132.0 // Apache-2.0
1617
github.com/google/jsonschema-go v0.4.3 // MIT
1718
github.com/google/uuid v1.6.0 // BSD-3-Clause
@@ -53,7 +54,6 @@ require (
5354
github.com/catppuccin/go v0.3.0 // indirect
5455
github.com/cespare/xxhash/v2 v2.3.0 // indirect
5556
github.com/charmbracelet/colorprofile v0.4.1 // indirect
56-
github.com/charmbracelet/x/ansi v0.11.6 // indirect
5757
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
5858
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
5959
github.com/charmbracelet/x/term v0.2.2 // indirect
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPromptBaseline_AltKeyNoop pins that Alt-prefixed keys are silent
13+
// no-ops in [cmdio.RunPrompt]. Specifically, Alt+f (the readline binding
14+
// for "move forward by word") must neither move the cursor nor insert a
15+
// literal 'f' into the buffer. The same shape applies to Alt+b, Alt+d,
16+
// Alt+Backspace, and any other modified key the prompt model doesn't
17+
// handle; pinning Alt+f covers the class.
18+
func TestPromptBaseline_AltKeyNoop(t *testing.T) {
19+
t.Parallel()
20+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
21+
Label: "Workspace name",
22+
})
23+
tm.WaitFor("Workspace name")
24+
25+
// Type "hello" and move cursor two places left so it sits mid-word.
26+
// If Alt+f moved the cursor (or inserted), goldens 01 and 02 would
27+
// diverge.
28+
tm.Type("hello")
29+
tm.Type(termtest.KeyLeft)
30+
tm.Type(termtest.KeyLeft)
31+
tm.Golden("01-cursor-mid")
32+
33+
tm.Type("\x1bf")
34+
tm.Golden("02-after-alt-f")
35+
36+
tm.Type(termtest.KeyEnter)
37+
v, err := tm.Result()
38+
require.NoError(t, err, "raw output: %q", tm.Raw())
39+
// Final guard: the returned value must be exactly "hello". A literal
40+
// 'f' insertion would surface here even if the goldens above somehow
41+
// missed it.
42+
assert.Equal(t, "hello", v)
43+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPromptBaseline_CtrlC pins Ctrl+C cancellation for RunPrompt. Mirrors
13+
// the equivalent Secret test: error is returned, value is empty, snapshot
14+
// captures any "^C" that the terminal echoed.
15+
func TestPromptBaseline_CtrlC(t *testing.T) {
16+
t.Parallel()
17+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
18+
Label: "Workspace name",
19+
})
20+
tm.WaitFor("Workspace name")
21+
tm.Golden("01-empty")
22+
23+
tm.Type("partial input")
24+
tm.Golden("02-after-typing")
25+
26+
tm.Type(termtest.KeyCtrlC)
27+
28+
v, err := tm.Result()
29+
require.Error(t, err)
30+
assert.EqualError(t, err, "^C")
31+
assert.Empty(t, v)
32+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPromptBaseline_CtrlFCtrlB pins that Ctrl+F and Ctrl+B move the cursor
13+
// one character forward and backward in [cmdio.RunPrompt], the same as the
14+
// right and left arrow keys. The emacs-style bindings are de-facto aliases
15+
// for the arrow keys; this test pins that equivalence.
16+
func TestPromptBaseline_CtrlFCtrlB(t *testing.T) {
17+
t.Parallel()
18+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
19+
Label: "Workspace name",
20+
})
21+
tm.WaitFor("Workspace name")
22+
tm.Type("hello")
23+
tm.Golden("01-cursor-end")
24+
25+
tm.Type(termtest.KeyCtrlB)
26+
tm.Type(termtest.KeyCtrlB)
27+
tm.Golden("02-after-ctrl-b-twice")
28+
29+
tm.Type(termtest.KeyCtrlF)
30+
tm.Golden("03-after-ctrl-f")
31+
32+
tm.Type(termtest.KeyEnter)
33+
v, err := tm.Result()
34+
require.NoError(t, err, "raw output: %q", tm.Raw())
35+
assert.Equal(t, "hello", v)
36+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPromptBaseline_CtrlH pins that Ctrl+H deletes the character to the
13+
// left of the cursor in [cmdio.RunPrompt] — the same as the Backspace key.
14+
// Ctrl+H sends BS (0x08) and Backspace sends DEL (0x7f); the prompt model
15+
// handles both as backspace, making the control-character form a de-facto
16+
// alias. This test pins that equivalence so a future change can't silently
17+
// drop it.
18+
func TestPromptBaseline_CtrlH(t *testing.T) {
19+
t.Parallel()
20+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
21+
Label: "Workspace name",
22+
})
23+
tm.WaitFor("Workspace name")
24+
tm.Type("hello")
25+
tm.Golden("01-typed-hello")
26+
27+
tm.Type(termtest.KeyCtrlH)
28+
tm.Type(termtest.KeyCtrlH)
29+
tm.Golden("02-after-ctrl-h-twice")
30+
31+
tm.Type(termtest.KeyEnter)
32+
v, err := tm.Result()
33+
require.NoError(t, err, "raw output: %q", tm.Raw())
34+
assert.Equal(t, "hel", v)
35+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPromptBaseline_CtrlJ pins that Ctrl+J submits the prompt in
13+
// [cmdio.RunPrompt] — the same as the Enter (Return) key. Enter sends CR
14+
// (0x0d) and Ctrl+J sends LF (0x0a); the prompt model treats both as
15+
// submit. A future change that only reacts to CR would silently swallow
16+
// Ctrl+J; this test pins the parity.
17+
func TestPromptBaseline_CtrlJ(t *testing.T) {
18+
t.Parallel()
19+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
20+
Label: "Workspace name",
21+
})
22+
tm.WaitFor("Workspace name")
23+
tm.Type("hello")
24+
tm.Golden("01-typed-hello")
25+
26+
tm.Type(termtest.KeyCtrlJ)
27+
v, err := tm.Result()
28+
require.NoError(t, err, "raw output: %q", tm.Raw())
29+
assert.Equal(t, "hello", v)
30+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPromptBaseline_CursorEditing pins how RunPrompt responds to cursor
13+
// movement and line-editing keys: ←/→, Home/End, Backspace, Ctrl+W, Ctrl+U.
14+
// The prompt model handles ←/→ and Backspace; Home/End/Ctrl+W/Ctrl+U are
15+
// no-ops, so the goldens after them are intentionally identical to the
16+
// post-Backspace one. The Delete key (\x1b[3~) is *not* covered here
17+
// because it exits the prompt with EOF; that behavior is pinned separately
18+
// by TestPromptBaseline_DeleteKeyExits.
19+
func TestPromptBaseline_CursorEditing(t *testing.T) {
20+
t.Parallel()
21+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
22+
Label: "Workspace name",
23+
})
24+
tm.WaitFor("Workspace name")
25+
tm.Golden("01-empty")
26+
27+
tm.Type("hello world")
28+
tm.Golden("02-typed")
29+
30+
tm.Type(termtest.KeyHome)
31+
tm.Type("X")
32+
tm.Golden("03-insert-at-start")
33+
34+
tm.Type(termtest.KeyEnd)
35+
tm.Type("!")
36+
tm.Golden("04-insert-at-end")
37+
38+
tm.Type(termtest.KeyLeft)
39+
tm.Type(termtest.KeyLeft)
40+
tm.Type("Y")
41+
tm.Golden("05-insert-mid")
42+
43+
tm.Type(termtest.KeyBackspace)
44+
tm.Golden("06-after-backspace")
45+
46+
tm.Type(termtest.KeyCtrlW)
47+
tm.Golden("07-after-ctrl-w")
48+
49+
tm.Type(termtest.KeyCtrlU)
50+
tm.Golden("08-after-ctrl-u")
51+
52+
tm.Type(termtest.KeyEnter)
53+
54+
v, err := tm.Result()
55+
require.NoError(t, err, "raw output: %q", tm.Raw())
56+
// The goldens above show the visible buffer is "hello worldX!" when
57+
// Enter fires; that's what the prompt returns.
58+
assert.Equal(t, "hello worldX!", v, "snapshot:\n%s", tm.Snapshot())
59+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"io"
5+
"testing"
6+
7+
"github.com/databricks/cli/libs/cmdio"
8+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// TestPromptBaseline_DeleteKeyExits pins a surprising behavior of
14+
// [cmdio.RunPrompt]: pressing the Delete key (\x1b[3~) exits the prompt with
15+
// io.EOF, just like Ctrl+D would on an empty line — and discards any input
16+
// the user had already typed. The prompt model collapses both keys into the
17+
// same EOF path; see prompt.go for the rationale. Pinning the behavior here
18+
// makes sure a future change that splits the two keys is intentional.
19+
func TestPromptBaseline_DeleteKeyExits(t *testing.T) {
20+
t.Parallel()
21+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
22+
Label: "Workspace name",
23+
})
24+
tm.WaitFor("Workspace name")
25+
26+
// Type some content first to prove the buffer is non-empty from the user's
27+
// perspective. This is what makes the behavior surprising: the prompt
28+
// still exits even though the user has typed input.
29+
tm.Type("hello")
30+
tm.Type(termtest.KeyDelete)
31+
32+
v, err := tm.Result()
33+
require.Error(t, err, "raw output: %q", tm.Raw())
34+
assert.ErrorIs(t, err, io.EOF)
35+
assert.Empty(t, v, "Delete-as-EOF discards typed input")
36+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package cmdiotest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/cmdio/cmdiotest/termtest"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestPromptBaseline_HideEnteredFalse pins the default post-Enter rendering
13+
// of [cmdio.RunPrompt]: with HideEntered=false (the default), the entered
14+
// value is shown alongside the label after the prompt closes.
15+
func TestPromptBaseline_HideEnteredFalse(t *testing.T) {
16+
t.Parallel()
17+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
18+
Label: "Workspace name",
19+
HideEntered: false,
20+
})
21+
tm.WaitFor("Workspace name")
22+
tm.Type("hello")
23+
tm.Type(termtest.KeyEnter)
24+
25+
v, err := tm.Result()
26+
require.NoError(t, err, "raw output: %q", tm.Raw())
27+
assert.Equal(t, "hello", v, "snapshot:\n%s", tm.Snapshot())
28+
29+
tm.Golden("01-after-enter")
30+
}
31+
32+
// TestPromptBaseline_HideEnteredTrue pins that HideEntered=true clears the
33+
// prompt frame after the user submits, leaving no trace of the entered value
34+
// on screen. This is the path used by [cmdio.Secret].
35+
func TestPromptBaseline_HideEnteredTrue(t *testing.T) {
36+
t.Parallel()
37+
tm := termtest.NewPrompt(t, cmdio.PromptOptions{
38+
Label: "Workspace name",
39+
HideEntered: true,
40+
})
41+
tm.WaitFor("Workspace name")
42+
tm.Type("hello")
43+
tm.Type(termtest.KeyEnter)
44+
45+
v, err := tm.Result()
46+
require.NoError(t, err, "raw output: %q", tm.Raw())
47+
assert.Equal(t, "hello", v, "snapshot:\n%s", tm.Snapshot())
48+
49+
tm.Golden("01-after-enter")
50+
}

0 commit comments

Comments
 (0)