1package modes
2
3import (
4 "context"
5 "strings"
6 "testing"
7
8 "github.com/patriceckhart/zot/packages/agent/tools"
9 "github.com/patriceckhart/zot/packages/core"
10 "github.com/patriceckhart/zot/packages/tui"
11)
12
13func TestConfiguredKeyCommandMatchesModifiers(t *testing.T) {
14 keymap := map[string]string{"Ctrl+Shift+G": "/git-tools:status"}
15 if got := configuredKeyCommand(keymap, tui.Key{Kind: tui.KeyRune, Rune: 'g', Ctrl: true, Shift: true}); got != "/git-tools:status" {
16 t.Fatalf("configuredKeyCommand = %q, want extension command", got)
17 }
18 if got := configuredKeyCommand(keymap, tui.Key{Kind: tui.KeyRune, Rune: 'G', Ctrl: true, Shift: true}); got != "/git-tools:status" {
19 t.Fatalf("shifted uppercase configuredKeyCommand = %q, want extension command", got)
20 }
21 if got := configuredKeyCommand(keymap, tui.Key{Kind: tui.KeyRune, Rune: 'g', Ctrl: true}); got != "/git-tools:status" {
22 t.Fatalf("legacy ctrl-letter configuredKeyCommand = %q, want extension command", got)
23 }
24 if got := configuredKeyCommand(keymap, tui.Key{Kind: tui.KeyRune, Rune: 'g', Shift: true}); got != "" {
25 t.Fatalf("unmodified key matched command %q", got)
26 }
27}
28
29func TestConfiguredKeyCommandPrefersExactChord(t *testing.T) {
30 keymap := map[string]string{
31 "ctrl+g": "/help",
32 "ctrl+shift+g": "/git-tools:status",
33 }
34 if got := configuredKeyCommand(keymap, tui.Key{Kind: tui.KeyRune, Rune: 'g', Ctrl: true}); got != "/help" {
35 t.Fatalf("plain ctrl+g = %q, want exact /help binding", got)
36 }
37 if got := configuredKeyCommand(keymap, tui.Key{Kind: tui.KeyRune, Rune: 'G', Ctrl: true, Shift: true}); got != "/git-tools:status" {
38 t.Fatalf("ctrl+shift+g = %q, want /git-tools:status", got)
39 }
40}
41
42func TestConfiguredKeyCommandMatchesDedicatedControlKeys(t *testing.T) {
43 if got := configuredKeyCommand(map[string]string{"ctrl+l": "/skill:review"}, tui.Key{Kind: tui.KeyCtrlL}); got != "/skill:review" {
44 t.Fatalf("configuredKeyCommand = %q, want skill command", got)
45 }
46 if got := configuredKeyCommand(map[string]string{"ctrl+h": "/help"}, tui.Key{Kind: tui.KeyRune, Rune: 'h', Ctrl: true}); got != "/help" {
47 t.Fatalf("enhanced ctrl+h binding = %q, want /help", got)
48 }
49}
50
51func TestParseConfiguredKeyRejectsMalformedAndSupportsSpecialKeys(t *testing.T) {
52 for _, bad := range []string{"ctrl+shift", "g", "enter", "space", "tab", "up", "backspace", "no-such-key", "ctrl+", "ctrl+a+b", ""} {
53 if _, ok := parseConfiguredKey(bad); ok {
54 t.Fatalf("parseConfiguredKey(%q) accepted an unbindable key", bad)
55 }
56 }
57 key, ok := parseConfiguredKey("alt+enter")
58 if !ok || key.kind != tui.KeyEnter || !key.alt {
59 t.Fatalf("parseConfiguredKey(alt+enter) = %#v, %v", key, ok)
60 }
61 if got := configuredKeyCommand(map[string]string{"shift+tab": "/help"}, tui.Key{Kind: tui.KeyShiftTab}); got != "/help" {
62 t.Fatalf("shift+tab binding = %q, want /help", got)
63 }
64 if got := configuredKeyCommand(map[string]string{"ctrl+space": "/help"}, tui.Key{Kind: tui.KeyRune, Rune: ' ', Ctrl: true}); got != "/help" {
65 t.Fatalf("ctrl+space binding = %q, want /help", got)
66 }
67}
68
69func TestCompileKeymapReportsInvalidAndReservedEntries(t *testing.T) {
70 bindings, issues := compileKeymap(map[string]string{
71 "ctrl+c": "/help",
72 "ctrl+d": "/help",
73 "esc": "/help",
74 "enter": "/help",
75 "ctrl+s": "settings",
76 "ctrl+x": "",
77 "alt+r": "/skill:code-review",
78 "ctrl+alt+c": "/help",
79 "Shift+Enter": "/help",
80 })
81 if len(issues) != 6 {
82 t.Fatalf("issues = %v, want 6 entries", issues)
83 }
84 for _, issue := range issues {
85 if !strings.HasPrefix(issue, "keymap: ") {
86 t.Fatalf("issue %q lacks keymap prefix", issue)
87 }
88 }
89 var names []string
90 for _, b := range bindings {
91 names = append(names, b.name)
92 }
93 if got, want := strings.Join(names, ","), "alt+r,ctrl+alt+c,shift+enter"; got != want {
94 t.Fatalf("compiled bindings = %q, want %q", got, want)
95 }
96 if got := lookupKeymap(bindings, tui.Key{Kind: tui.KeyCtrlC}); got != "" {
97 t.Fatalf("ctrl+c resolved to %q, want reserved", got)
98 }
99 if got := lookupKeymap(bindings, tui.Key{Kind: tui.KeyEnter, Shift: true}); got != "/help" {
100 t.Fatalf("shift+enter = %q, want /help", got)
101 }
102}
103
104func newKeymapInteractive(keymap map[string]string) *Interactive {
105 agent := core.NewAgent(nil, "test-model", "test system", nil)
106 return NewInteractive(InteractiveConfig{
107 Theme: tui.Dark,
108 Terminal: tui.NewProcTerm(),
109 Agent: agent,
110 Sandbox: tools.NewSandbox("."),
111 Keymap: keymap,
112 })
113}
114
115func TestInteractiveKeymapRunsSlashCommand(t *testing.T) {
116 i := newKeymapInteractive(map[string]string{"ctrl+s": "/help", "bogus": "/help"})
117 if len(i.reloadErrors) != 1 || !strings.Contains(i.reloadErrors[0], `"bogus"`) {
118 t.Fatalf("reloadErrors = %v, want one keymap diagnostic", i.reloadErrors)
119 }
120 i.ed.SetValue("draft")
121 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRune, Rune: 's', Ctrl: true})
122 if len(i.helpBlock) == 0 {
123 t.Fatal("ctrl+s did not run /help")
124 }
125 if got := i.ed.Value(); got != "" {
126 t.Fatalf("editor = %q, want cleared before running the bound command", got)
127 }
128}
129
130func TestInteractiveUnboundChordDoesNotType(t *testing.T) {
131 i := newKeymapInteractive(nil)
132 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRune, Rune: 's', Ctrl: true})
133 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRune, Rune: 'k', Super: true})
134 if got := i.ed.Value(); got != "" {
135 t.Fatalf("editor = %q, want unbound chords ignored", got)
136 }
137}
138
139func TestInteractiveKeymapInactiveWhileConfirmationFocused(t *testing.T) {
140 i := newKeymapInteractive(map[string]string{"ctrl+s": "/help"})
141 resp := enqueueConfirmation(i)
142 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRune, Rune: 's', Ctrl: true})
143 if len(i.helpBlock) != 0 {
144 t.Fatal("keymap fired while confirmation owned input")
145 }
146 select {
147 case decision := <-resp:
148 t.Fatalf("chord answered confirmation: %+v", decision)
149 default:
150 }
151}
152
153func TestInteractiveChordDoesNotLeakIntoDialogAsLetter(t *testing.T) {
154 i := newKeymapInteractive(map[string]string{"ctrl+r": "/help"})
155 i.sessionDialog.sessions = []core.SessionSummary{{Path: "a.jsonl", Title: "one", MessageCount: 1}}
156 i.sessionDialog.active = true
157 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRune, Rune: 'r', Ctrl: true})
158 if i.sessionDialog.renaming {
159 t.Fatal("ctrl+r started a rename as if it were a bare 'r'")
160 }
161 if len(i.helpBlock) != 0 {
162 t.Fatal("keymap fired while the session dialog owned input")
163 }
164 if !i.sessionDialog.Active() {
165 t.Fatal("session dialog closed unexpectedly")
166 }
167}