• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

umputun / ralphex / 21343370876

26 Jan 2026 01:33AM UTC coverage: 78.401% (+0.2%) from 78.234%
21343370876

Pull #22

github

umputun
test: add tests for plan mode functions

improve coverage for plan creation mode:
- cmd/ralphex: add tests for setupRunnerLogger, getCurrentBranch,
  setupGitForExecution, handlePostExecution, validateFlags,
  printStartupInfo, findRecentPlan
- pkg/processor: add direct test for buildPlanPrompt

coverage improved from 37.6% to 50.4% for cmd/ralphex
Pull Request #22: Add interactive plan creation mode (`--plan` flag)

255 of 441 new or added lines in 11 files covered. (57.82%)

3 existing lines in 1 file now uncovered.

3245 of 4139 relevant lines covered (78.4%)

66.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.52
/pkg/input/input.go
1
// Package input provides terminal input collection for interactive plan creation.
2
package input
3

4
import (
5
        "bufio"
6
        "context"
7
        "errors"
8
        "fmt"
9
        "io"
10
        "os"
11
        "os/exec"
12
        "strconv"
13
        "strings"
14
)
15

16
//go:generate moq -out mocks/collector.go -pkg mocks -skip-ensure -fmt goimports . Collector
17

18
// Collector provides interactive input collection for plan creation.
19
type Collector interface {
20
        // AskQuestion presents a question with options and returns the selected answer.
21
        // Returns the selected option text or error if selection fails.
22
        AskQuestion(ctx context.Context, question string, options []string) (string, error)
23
}
24

25
// TerminalCollector implements Collector using fzf (if available) or numbered selection fallback.
26
type TerminalCollector struct {
27
        stdin  io.Reader // for testing, nil uses os.Stdin
28
        stdout io.Writer // for testing, nil uses os.Stdout
29
}
30

31
// NewTerminalCollector creates a new TerminalCollector with default stdin/stdout.
32
func NewTerminalCollector() *TerminalCollector {
3✔
33
        return &TerminalCollector{}
3✔
34
}
3✔
35

36
// AskQuestion presents options using fzf if available, otherwise falls back to numbered selection.
37
func (c *TerminalCollector) AskQuestion(ctx context.Context, question string, options []string) (string, error) {
2✔
38
        if len(options) == 0 {
4✔
39
                return "", errors.New("no options provided")
2✔
40
        }
2✔
41

42
        // try fzf first
NEW
43
        if hasFzf() {
×
NEW
44
                return c.selectWithFzf(ctx, question, options)
×
NEW
45
        }
×
46

47
        // fallback to numbered selection
NEW
48
        return c.selectWithNumbers(question, options)
×
49
}
50

51
// hasFzf checks if fzf is available in PATH.
NEW
52
func hasFzf() bool {
×
NEW
53
        _, err := exec.LookPath("fzf")
×
NEW
54
        return err == nil
×
NEW
55
}
×
56

57
// selectWithFzf uses fzf for interactive selection.
NEW
58
func (c *TerminalCollector) selectWithFzf(ctx context.Context, question string, options []string) (string, error) {
×
NEW
59
        input := strings.Join(options, "\n")
×
NEW
60

×
NEW
61
        cmd := exec.CommandContext(ctx, "fzf", "--prompt", question+": ", "--height", "10", "--layout=reverse") //nolint:gosec // fzf is a trusted external tool, question is user-provided prompt text
×
NEW
62
        cmd.Stdin = strings.NewReader(input)
×
NEW
63
        cmd.Stderr = os.Stderr
×
NEW
64

×
NEW
65
        output, err := cmd.Output()
×
NEW
66
        if err != nil {
×
NEW
67
                // fzf returns exit code 130 when user presses Escape
×
NEW
68
                var exitErr *exec.ExitError
×
NEW
69
                if errors.As(err, &exitErr) && exitErr.ExitCode() == 130 {
×
NEW
70
                        return "", errors.New("selection canceled")
×
NEW
71
                }
×
NEW
72
                return "", fmt.Errorf("fzf selection failed: %w", err)
×
73
        }
74

NEW
75
        selected := strings.TrimSpace(string(output))
×
NEW
76
        if selected == "" {
×
NEW
77
                return "", errors.New("no selection made")
×
NEW
78
        }
×
79

NEW
80
        return selected, nil
×
81
}
82

83
// selectWithNumbers presents numbered options for selection via stdin.
84
func (c *TerminalCollector) selectWithNumbers(question string, options []string) (string, error) {
12✔
85
        stdout := c.stdout
12✔
86
        if stdout == nil {
12✔
NEW
87
                stdout = os.Stdout
×
NEW
88
        }
×
89
        stdin := c.stdin
12✔
90
        if stdin == nil {
12✔
NEW
91
                stdin = os.Stdin
×
NEW
92
        }
×
93

94
        // print question and options
95
        _, _ = fmt.Fprintln(stdout)
12✔
96
        _, _ = fmt.Fprintln(stdout, question)
12✔
97
        for i, opt := range options {
39✔
98
                _, _ = fmt.Fprintf(stdout, "  %d) %s\n", i+1, opt)
27✔
99
        }
27✔
100
        _, _ = fmt.Fprintf(stdout, "Enter number (1-%d): ", len(options))
12✔
101

12✔
102
        // read selection
12✔
103
        reader := bufio.NewReader(stdin)
12✔
104
        line, err := reader.ReadString('\n')
12✔
105
        if err != nil {
13✔
106
                return "", fmt.Errorf("read input: %w", err)
1✔
107
        }
1✔
108

109
        // parse selection
110
        line = strings.TrimSpace(line)
11✔
111
        num, err := strconv.Atoi(line)
11✔
112
        if err != nil {
13✔
113
                return "", fmt.Errorf("invalid number: %s", line)
2✔
114
        }
2✔
115

116
        if num < 1 || num > len(options) {
12✔
117
                return "", fmt.Errorf("selection out of range: %d (must be 1-%d)", num, len(options))
3✔
118
        }
3✔
119

120
        return options[num-1], nil
6✔
121
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc