From c48bc1a7d1f4adc9dca524ef99435291b35c24b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Dost=C3=A1l?= Date: Tue, 28 Apr 2026 19:50:35 +0200 Subject: [PATCH 1/3] Poll TTY echo mode instead of sleeping in password tests Replace the fixed-duration sleep with a polling loop that checks the actual TTY echo flag before sending password input. This eliminates the race condition where huh has not yet disabled echo mode, which caused flaky test failures in slow environments. Follow-up to #13304. --- internal/prompter/accessible_prompter_test.go | 33 ++++++++++++++----- internal/prompter/echo_test_darwin.go | 5 +++ internal/prompter/echo_test_linux.go | 5 +++ 3 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 internal/prompter/echo_test_darwin.go create mode 100644 internal/prompter/echo_test_linux.go diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index b67156bdf..8c4d8ce92 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -5,6 +5,7 @@ package prompter_test import ( "fmt" "io" + "os" "slices" "strings" "testing" @@ -17,6 +18,7 @@ import ( "github.com/hinshun/vt10x" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" ) // The following tests are broadly testing the accessible prompter, and NOT asserting @@ -34,8 +36,6 @@ import ( // but doesn't mandate that prompts always look exactly the same. func TestAccessiblePrompter(t *testing.T) { - beforePasswordSendTimeout := 100 * time.Millisecond - t.Run("Select", func(t *testing.T) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) @@ -505,8 +505,8 @@ func TestAccessiblePrompter(t *testing.T) { _, err := console.ExpectString("Enter password") require.NoError(t, err) - // Wait to ensure huh has time to set the echo mode - time.Sleep(beforePasswordSendTimeout) + // Wait until huh has disabled echo mode on the TTY + waitForEchoDisabled(t, console.Tty(), 5*time.Second) // Enter a number _, err = console.SendLine(dummyPassword) @@ -596,8 +596,8 @@ func TestAccessiblePrompter(t *testing.T) { _, err := console.ExpectString("Paste your authentication token:") require.NoError(t, err) - // Wait to ensure huh has time to set the echo mode - time.Sleep(beforePasswordSendTimeout) + // Wait until huh has disabled echo mode on the TTY + waitForEchoDisabled(t, console.Tty(), 5*time.Second) // Enter some dummy auth token _, err = console.SendLine(dummyAuthToken) @@ -641,8 +641,8 @@ func TestAccessiblePrompter(t *testing.T) { _, err = console.ExpectString("Paste your authentication token:") require.NoError(t, err) - // Wait to ensure huh has time to set the echo mode - time.Sleep(beforePasswordSendTimeout) + // Wait until huh has disabled echo mode on the TTY + waitForEchoDisabled(t, console.Tty(), 5*time.Second) // Now enter some dummy auth token to return control back to the test _, err = console.SendLine(dummyAuthTokenForAfterFailure) @@ -956,3 +956,20 @@ func testCloser(t *testing.T, closer io.Closer) { t.Errorf("Close failed: %s", err) } } + +// waitForEchoDisabled polls the TTY until echo mode is disabled or the +// timeout is reached. This is used in password and auth token tests to +// ensure that huh has configured the terminal before we send input. +func waitForEchoDisabled(t *testing.T, tty *os.File, timeout time.Duration) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + termios, err := unix.IoctlGetTermios(int(tty.Fd()), ioctlGetTermios) + require.NoError(t, err) + if termios.Lflag&unix.ECHO == 0 { + return + } + time.Sleep(time.Millisecond) + } + t.Fatal("timed out waiting for echo mode to be disabled") +} diff --git a/internal/prompter/echo_test_darwin.go b/internal/prompter/echo_test_darwin.go new file mode 100644 index 000000000..7019fa4df --- /dev/null +++ b/internal/prompter/echo_test_darwin.go @@ -0,0 +1,5 @@ +package prompter_test + +import "golang.org/x/sys/unix" + +const ioctlGetTermios = unix.TIOCGETA diff --git a/internal/prompter/echo_test_linux.go b/internal/prompter/echo_test_linux.go new file mode 100644 index 000000000..a75077301 --- /dev/null +++ b/internal/prompter/echo_test_linux.go @@ -0,0 +1,5 @@ +package prompter_test + +import "golang.org/x/sys/unix" + +const ioctlGetTermios = unix.TCGETS From 9c4184de6f8c208a11e4329b90fa9844efd728e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Dost=C3=A1l?= Date: Tue, 28 Apr 2026 19:57:35 +0200 Subject: [PATCH 2/3] Address review feedback on echo mode polling - Rename echo_test_{linux,darwin}.go to echo_{linux,darwin}_test.go so they are only compiled during tests - Narrow build tag from !windows to linux || darwin to avoid compile failures on other Unix platforms - Return error from waitForEchoDisabled instead of calling t.Fatal, since the function is called from goroutines where FailNow would only terminate the calling goroutine --- internal/prompter/accessible_prompter_test.go | 19 ++++++++++--------- ...cho_test_darwin.go => echo_darwin_test.go} | 0 ...{echo_test_linux.go => echo_linux_test.go} | 0 3 files changed, 10 insertions(+), 9 deletions(-) rename internal/prompter/{echo_test_darwin.go => echo_darwin_test.go} (100%) rename internal/prompter/{echo_test_linux.go => echo_linux_test.go} (100%) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 8c4d8ce92..ee6eba3a9 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build linux || darwin package prompter_test @@ -506,7 +506,7 @@ func TestAccessiblePrompter(t *testing.T) { require.NoError(t, err) // Wait until huh has disabled echo mode on the TTY - waitForEchoDisabled(t, console.Tty(), 5*time.Second) + require.NoError(t, waitForEchoDisabled(console.Tty(), 5*time.Second)) // Enter a number _, err = console.SendLine(dummyPassword) @@ -597,7 +597,7 @@ func TestAccessiblePrompter(t *testing.T) { require.NoError(t, err) // Wait until huh has disabled echo mode on the TTY - waitForEchoDisabled(t, console.Tty(), 5*time.Second) + require.NoError(t, waitForEchoDisabled(console.Tty(), 5*time.Second)) // Enter some dummy auth token _, err = console.SendLine(dummyAuthToken) @@ -642,7 +642,7 @@ func TestAccessiblePrompter(t *testing.T) { require.NoError(t, err) // Wait until huh has disabled echo mode on the TTY - waitForEchoDisabled(t, console.Tty(), 5*time.Second) + require.NoError(t, waitForEchoDisabled(console.Tty(), 5*time.Second)) // Now enter some dummy auth token to return control back to the test _, err = console.SendLine(dummyAuthTokenForAfterFailure) @@ -960,16 +960,17 @@ func testCloser(t *testing.T, closer io.Closer) { // waitForEchoDisabled polls the TTY until echo mode is disabled or the // timeout is reached. This is used in password and auth token tests to // ensure that huh has configured the terminal before we send input. -func waitForEchoDisabled(t *testing.T, tty *os.File, timeout time.Duration) { - t.Helper() +func waitForEchoDisabled(tty *os.File, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { termios, err := unix.IoctlGetTermios(int(tty.Fd()), ioctlGetTermios) - require.NoError(t, err) + if err != nil { + return fmt.Errorf("getting terminal attributes: %w", err) + } if termios.Lflag&unix.ECHO == 0 { - return + return nil } time.Sleep(time.Millisecond) } - t.Fatal("timed out waiting for echo mode to be disabled") + return fmt.Errorf("timed out waiting for echo mode to be disabled") } diff --git a/internal/prompter/echo_test_darwin.go b/internal/prompter/echo_darwin_test.go similarity index 100% rename from internal/prompter/echo_test_darwin.go rename to internal/prompter/echo_darwin_test.go diff --git a/internal/prompter/echo_test_linux.go b/internal/prompter/echo_linux_test.go similarity index 100% rename from internal/prompter/echo_test_linux.go rename to internal/prompter/echo_linux_test.go From a44721d233be9a2f6f0b5ee5c4f71274acb8d296 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 7 May 2026 20:20:09 +0200 Subject: [PATCH 3/3] Add explicit build tags to platform-specific echo test files The Go toolchain infers constraints from _darwin/_linux filename suffixes, but explicit //go:build tags make the constraint visible without relying on filename conventions, consistent with modern Go style. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/prompter/echo_darwin_test.go | 2 ++ internal/prompter/echo_linux_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/internal/prompter/echo_darwin_test.go b/internal/prompter/echo_darwin_test.go index 7019fa4df..2cb3130d9 100644 --- a/internal/prompter/echo_darwin_test.go +++ b/internal/prompter/echo_darwin_test.go @@ -1,3 +1,5 @@ +//go:build darwin + package prompter_test import "golang.org/x/sys/unix" diff --git a/internal/prompter/echo_linux_test.go b/internal/prompter/echo_linux_test.go index a75077301..ad63bd1d5 100644 --- a/internal/prompter/echo_linux_test.go +++ b/internal/prompter/echo_linux_test.go @@ -1,3 +1,5 @@ +//go:build linux + package prompter_test import "golang.org/x/sys/unix"