1package modes
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/bnema/zut/packages/tui"
11)
12
13func TestLoginDialogProviderPickerFiltersLikeModelPicker(t *testing.T) {
14 d := newLoginDialog()
15 d.step = loginStepProvider
16 d.method = "apikey"
17 d.status = map[string]string{}
18 for _, r := range "llamacpp" {
19 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: r})
20 }
21 options := d.providerOptions()
22 if len(options) != 1 || options[0] != "llama.cpp" {
23 t.Fatalf("options = %v", options)
24 }
25 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
26 if d.step != loginStepLlamaURL {
27 t.Fatalf("step = %v", d.step)
28 }
29}
30
31func TestLoginDialogProviderPickerShowsNoMatches(t *testing.T) {
32 d := newLoginDialog()
33 d.step = loginStepProvider
34 d.method = "apikey"
35 d.status = map[string]string{}
36 d.providerQuery = "not-a-provider"
37 if options := d.providerOptions(); len(options) != 0 {
38 t.Fatalf("options = %v", options)
39 }
40 d.HandleKey(tui.Key{Kind: tui.KeyPageDown})
41 if d.cursor != 0 {
42 t.Fatalf("cursor = %d", d.cursor)
43 }
44 if action := d.HandleKey(tui.Key{Kind: tui.KeyEnter}); action != (loginDialogAction{}) {
45 t.Fatalf("action = %+v", action)
46 }
47}
48
49func TestLoginDialogLlamaCPPValidatesURLAndAcceptsOptionalKey(t *testing.T) {
50 d := newLoginDialog()
51 d.step = loginStepLlamaURL
52 d.provider = "llama.cpp"
53 d.method = "apikey"
54 d.llamaEd = tui.NewEditor("")
55
56 d.llamaEd.SetValue("file:///tmp/router")
57 if action := d.HandleKey(tui.Key{Kind: tui.KeyEnter}); action.SaveLlama || d.step != loginStepLlamaURL {
58 t.Fatalf("invalid URL advanced dialog: action=%+v step=%v", action, d.step)
59 }
60 if !strings.Contains(d.message, "http or https") {
61 t.Fatalf("validation message = %q", d.message)
62 }
63
64 d.llamaEd.SetValue("http://127.0.0.1:8080/v1/")
65 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
66 if d.step != loginStepLlamaKey || d.llamaURL != "http://127.0.0.1:8080" {
67 t.Fatalf("step=%v URL=%q", d.step, d.llamaURL)
68 }
69 action := d.HandleKey(tui.Key{Kind: tui.KeyEnter})
70 if !action.SaveLlama || action.LlamaURL != "http://127.0.0.1:8080" || action.LlamaAPIKey != "" {
71 t.Fatalf("action = %+v", action)
72 }
73}
74
75func TestLoginDialogCursorPosMatchesPaddedInputRow(t *testing.T) {
76 d := newLoginDialog()
77 d.Open(t.TempDir())
78 d.method = "oauth"
79 d.provider = "anthropic"
80 d.ShowWaiting("https://example.com/oauth/authorize?code_challenge=abc&state=xyz")
81
82 // Floating panes render this dialog narrower than the main terminal, so
83 // CursorPos must use the identical width to retain wrapping alignment.
84 const paneWidth = 62
85 lines := padDialogFrame(d.Render(tui.Theme{}, paneWidth))
86 row, _ := d.CursorPos(paneWidth)
87 if row < 0 || row >= len(lines) {
88 t.Fatalf("CursorPos row = %d outside rendered lines %d", row, len(lines))
89 }
90 if got := stripANSIBytes(lines[row]); !strings.Contains(got, "▌") {
91 t.Fatalf("CursorPos row %d = %q; want editor input row", row, got)
92 }
93}
94
95func TestLoginDialogWaitingShowsCopyURLShortcut(t *testing.T) {
96 for _, provider := range []string{"openai-codex", "kimi", "xai", "github-copilot"} {
97 t.Run(provider, func(t *testing.T) {
98 d := newLoginDialog()
99 d.Open(t.TempDir())
100 d.method = "oauth"
101 d.provider = provider
102 d.ShowWaiting("https://example.com/oauth/authorize?state=xyz")
103
104 lines := strings.Join(d.Render(tui.Theme{}, 80), "\n")
105 if !strings.Contains(lines, "alt+c copies URL") {
106 t.Fatalf("login dialog does not advertise the copy shortcut: %q", lines)
107 }
108 })
109 }
110}
111
112func TestInteractiveAltCSendsLoginURLToTerminalClipboard(t *testing.T) {
113 t.Setenv("PATH", t.TempDir())
114 t.Setenv("WAYLAND_DISPLAY", "")
115 t.Setenv("DISPLAY", "")
116 t.Setenv("TERM_PROGRAM", "vev")
117
118 const loginURL = "https://example.com/oauth/authorize?state=xyz"
119 term := &alertTestTerminal{}
120 i := NewInteractive(InteractiveConfig{Terminal: term})
121 i.dialog.Open(t.TempDir())
122 i.dialog.method = "oauth"
123 i.dialog.provider = "openai-codex"
124 i.dialog.ShowWaiting(loginURL)
125 i.dialog.Render(tui.Theme{}, 80)
126
127 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRune, Rune: 'c', Alt: true})
128
129 const want = "\x1b]52;c;aHR0cHM6Ly9leGFtcGxlLmNvbS9vYXV0aC9hdXRob3JpemU/c3RhdGU9eHl6\a"
130 if got := term.String(); got != want {
131 t.Fatalf("terminal output = %q, want OSC 52 copy %q", got, want)
132 }
133 if got := i.dialog.codeEd.SubmitValue(); got != "" {
134 t.Fatalf("authorization code input = %q after alt+c, want unchanged", got)
135 }
136 i.mu.Lock()
137 defer i.mu.Unlock()
138 if i.statusOK != "sent login URL to terminal clipboard" || i.statusErr != "" {
139 t.Fatalf("status = (%q, %q), want successful terminal copy request", i.statusOK, i.statusErr)
140 }
141}
142
143func TestInteractiveAltCUsesTerminalClipboardBeforeSystemClipboard(t *testing.T) {
144 dir := t.TempDir()
145 state := filepath.Join(dir, "wl-copy-called")
146 if err := os.WriteFile(filepath.Join(dir, "wl-copy"), []byte("#!/bin/sh\n: > \"$CLIPBOARD_TEST_STATE\"\n"), 0o700); err != nil {
147 t.Fatal(err)
148 }
149 t.Setenv("PATH", dir)
150 t.Setenv("WAYLAND_DISPLAY", "wayland-test")
151 t.Setenv("DISPLAY", "")
152 t.Setenv("TERM_PROGRAM", "vev")
153 t.Setenv("CLIPBOARD_TEST_STATE", state)
154
155 const loginURL = "https://example.com/oauth/authorize?state=xyz"
156 term := &alertTestTerminal{}
157 i := NewInteractive(InteractiveConfig{Terminal: term})
158 i.dialog.Open(t.TempDir())
159 i.dialog.ShowWaiting(loginURL)
160
161 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRune, Rune: 'c', Alt: true})
162
163 const want = "\x1b]52;c;aHR0cHM6Ly9leGFtcGxlLmNvbS9vYXV0aC9hdXRob3JpemU/c3RhdGU9eHl6\a"
164 if got := term.String(); got != want {
165 t.Fatalf("terminal output = %q, want OSC 52 copy %q", got, want)
166 }
167 if _, err := os.Stat(state); !os.IsNotExist(err) {
168 t.Fatalf("system clipboard helper ran: %v", err)
169 }
170}