summaryrefslogtreecommitdiff
path: root/internal/shell/shell_internal_test.go
blob: 16cda0a45a47be19bcd21b01db92504249946e1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// package shell (internal test) exercises unexported types that cannot be
// reached from the external shell_test package.
package shell

import (
	"strings"
	"testing"

	"github.com/ergochat/readline"
)

// TestPrefixCompleterDo exercises the Do method of prefixCompleter against a
// fixed candidate list.  No TTY is required because the method is pure logic.
func TestPrefixCompleterDo(t *testing.T) {
	candidates := []string{"add", "edit", "export", "ls", "search"}

	p := &prefixCompleter{
		fn: func(prefix string) []string {
			var out []string
			for _, c := range candidates {
				if strings.HasPrefix(c, prefix) {
					out = append(out, c)
				}
			}
			return out
		},
	}

	cases := []struct {
		name       string
		line       string // full line up to cursor
		wantSuffix []string
		wantLen    int
	}{
		{
			// Empty input: all candidates returned; each suffix is the full word + space.
			name:       "empty prefix",
			line:       "",
			wantSuffix: []string{"add ", "edit ", "export ", "ls ", "search "},
			wantLen:    0,
		},
		{
			// "e" prefix: edit, export.
			name:       "single char prefix",
			line:       "e",
			wantSuffix: []string{"dit ", "xport "},
			wantLen:    1,
		},
		{
			// "ex" prefix: only export.
			name:       "two char prefix",
			line:       "ex",
			wantSuffix: []string{"port "},
			wantLen:    2,
		},
		{
			// "z" prefix: no matches.
			name:       "no match",
			line:       "z",
			wantSuffix: nil,
			wantLen:    1,
		},
		{
			// Line has a space: prefix is the word after the last space.
			// "cat e" → prefix is "e", completions for "e".
			name:       "prefix after space",
			line:       "cat e",
			wantSuffix: []string{"dit ", "xport "},
			wantLen:    1,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			line := []rune(tc.line)
			pos := len(line)
			newLine, length := p.Do(line, pos)

			if length != tc.wantLen {
				t.Errorf("length = %d; want %d", length, tc.wantLen)
			}
			if len(newLine) != len(tc.wantSuffix) {
				t.Errorf("len(newLine) = %d; want %d (%v vs %v)",
					len(newLine), len(tc.wantSuffix), toStrings(newLine), tc.wantSuffix)
				return
			}
			// Build a set for order-independent comparison.
			got := make(map[string]bool, len(newLine))
			for _, r := range newLine {
				got[string(r)] = true
			}
			for _, want := range tc.wantSuffix {
				if !got[want] {
					t.Errorf("missing suffix %q in completions %v", want, toStrings(newLine))
				}
			}
		})
	}
}

// TestVIInputFilter verifies vi-style modal key translation used by shell and
// PIN prompts through FuncFilterInputRune.
func TestVIInputFilter(t *testing.T) {
	v := newVIInputFilter()

	// Insert mode passes regular typing through unchanged.
	if r, ok := v.filter('x'); !ok || r != 'x' {
		t.Fatalf("insert mode passthrough got (%q,%v), want ('x',true)", r, ok)
	}

	// Esc enters normal mode and is swallowed.
	if r, ok := v.filter(readline.CharEsc); ok || r != 0 {
		t.Fatalf("esc got (%q,%v), want (0,false)", r, ok)
	}

	// Normal-mode movement mappings.
	cases := []struct {
		in   rune
		want rune
	}{
		{'h', readline.CharBackward},
		{'j', readline.CharNext},
		{'k', readline.CharPrev},
		{'l', readline.CharForward},
		{'w', readline.MetaForward},
		{'b', readline.MetaBackward},
		{'0', readline.CharLineStart},
		{'$', readline.CharLineEnd},
	}
	for _, tc := range cases {
		if r, ok := v.filter(tc.in); !ok || r != tc.want {
			t.Fatalf("normal mapping %q got (%q,%v), want (%q,true)", tc.in, r, ok, tc.want)
		}
	}

	// "i" returns to insert mode and is swallowed.
	if r, ok := v.filter('i'); ok || r != 0 {
		t.Fatalf("i got (%q,%v), want (0,false)", r, ok)
	}
	if r, ok := v.filter('z'); !ok || r != 'z' {
		t.Fatalf("insert mode after i got (%q,%v), want ('z',true)", r, ok)
	}

	// Ctrl-] should also enter normal mode.
	if r, ok := v.filter(29); ok || r != 0 {
		t.Fatalf("ctrl-] got (%q,%v), want (0,false)", r, ok)
	}
	if r, ok := v.filter('h'); !ok || r != readline.CharBackward {
		t.Fatalf("normal mode after ctrl-] got (%q,%v), want (CharBackward,true)", r, ok)
	}
}

// toStrings converts a [][]rune to []string for readable error output.
func toStrings(runes [][]rune) []string {
	out := make([]string, len(runes))
	for i, r := range runes {
		out[i] = string(r)
	}
	return out
}