fix: avoid stdin buffering bug in installer prompts

Prompt() created a new bufio.NewReader(os.Stdin) on each call, which
buffered all piped input on the first call and left nothing for
subsequent prompts. This made cheat un-scriptable (e.g., piping answers
via printf). Fix by reading one byte at a time from os.Stdin directly.

Also adds an end-to-end integration test for the first-run experience
(regression test for #721, #771, #730) and bumps the Dockerfile to
Go 1.26.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christopher Allen Lane
2026-02-14 21:51:30 -05:00
parent 00ec2c130d
commit fd1465ee38
4 changed files with 205 additions and 12 deletions

View File

@@ -3,7 +3,6 @@
package installer
import (
"bufio"
"fmt"
"os"
"strings"
@@ -12,20 +11,34 @@ import (
// Prompt prompts the user for a answer
func Prompt(prompt string, def bool) (bool, error) {
// initialize a line reader
reader := bufio.NewReader(os.Stdin)
// display the prompt
fmt.Printf("%s: ", prompt)
// read the answer
ans, err := reader.ReadString('\n')
if err != nil {
return false, fmt.Errorf("failed to parse input: %v", err)
// read one byte at a time until newline to avoid buffering past the
// end of the current line, which would consume input intended for
// subsequent Prompt calls on the same stdin
var line []byte
buf := make([]byte, 1)
for {
n, err := os.Stdin.Read(buf)
if n > 0 {
if buf[0] == '\n' {
break
}
if buf[0] != '\r' {
line = append(line, buf[0])
}
}
if err != nil {
if len(line) > 0 {
break
}
return false, fmt.Errorf("failed to prompt: %v", err)
}
}
// normalize the answer
ans = strings.ToLower(strings.TrimSpace(ans))
ans := strings.ToLower(strings.TrimSpace(string(line)))
// return the appropriate response
switch ans {

View File

@@ -154,8 +154,8 @@ func TestPromptError(t *testing.T) {
if err == nil {
t.Error("expected error when reading from closed stdin, got nil")
}
if !strings.Contains(err.Error(), "failed to parse input") {
t.Errorf("expected 'failed to parse input' error, got: %v", err)
if !strings.Contains(err.Error(), "failed to prompt") {
t.Errorf("expected 'failed to prompt' error, got: %v", err)
}
}