1package cmd
2
3import (
4 "os"
5 "path/filepath"
6 "reflect"
7 "sort"
8 "testing"
9
10 "github.com/spf13/cobra"
11)
12
13// setupCompletionConfig writes a small TOML fixture and points PIKA_CONFIG_FILE
14// at it, so config.Presets() returns deterministic results.
15func setupCompletionConfig(t *testing.T) {
16 t.Helper()
17
18 dir := t.TempDir()
19 cfg := filepath.Join(dir, "config.toml")
20 err := os.WriteFile(cfg, []byte(`
21[global]
22verbose = true
23
24[home]
25repo = "/repos/home"
26
27["home@cloud"]
28repo = "/repos/cloud"
29
30["@nas"]
31repo = "/repos/nas"
32`), 0o600)
33 if err != nil {
34 t.Fatalf("writing fixture config: %v", err)
35 }
36 t.Setenv("PIKA_CONFIG_FILE", cfg)
37 t.Setenv("HOME", dir)
38}
39
40func TestCompleteArgsNoArgs(t *testing.T) {
41 setupCompletionConfig(t)
42
43 completions, directive := completeArgs(nil, nil, "")
44 if directive != cobra.ShellCompDirectiveNoFileComp {
45 t.Fatalf("expected NoFileComp directive, got %v", directive)
46 }
47
48 // Should contain both presets and commands.
49 has := make(map[string]bool)
50 for _, c := range completions {
51 has[c] = true
52 }
53
54 for _, want := range []string{"home", "home@cloud", "@nas", "backup", "restore", "snapshots"} {
55 if !has[want] {
56 t.Errorf("missing expected completion %q in %v", want, completions)
57 }
58 }
59 // "global" must not appear.
60 if has["global"] {
61 t.Errorf("completions should not include 'global', got %v", completions)
62 }
63}
64
65func TestCompleteArgsAfterPreset(t *testing.T) {
66 setupCompletionConfig(t)
67
68 completions, directive := completeArgs(nil, []string{"home"}, "")
69 if directive != cobra.ShellCompDirectiveNoFileComp {
70 t.Fatalf("expected NoFileComp, got %v", directive)
71 }
72 sort.Strings(completions)
73 if !reflect.DeepEqual(completions, knownCommands) {
74 t.Fatalf("after preset, expected commands %v, got %v", knownCommands, completions)
75 }
76}
77
78func TestCompleteArgsAfterCommand(t *testing.T) {
79 setupCompletionConfig(t)
80
81 completions, directive := completeArgs(nil, []string{"backup"}, "")
82 if directive != cobra.ShellCompDirectiveNoFileComp {
83 t.Fatalf("expected NoFileComp, got %v", directive)
84 }
85 if len(completions) != 0 {
86 t.Fatalf("after command, expected no completions, got %v", completions)
87 }
88}
89
90func TestCompleteArgsAfterPresetAndCommand(t *testing.T) {
91 setupCompletionConfig(t)
92
93 completions, directive := completeArgs(nil, []string{"home", "backup"}, "")
94 if directive != cobra.ShellCompDirectiveNoFileComp {
95 t.Fatalf("expected NoFileComp, got %v", directive)
96 }
97 if len(completions) != 0 {
98 t.Fatalf("after preset+command, expected no completions, got %v", completions)
99 }
100}
101
102func TestCompleteArgsFlagPrefix(t *testing.T) {
103 t.Parallel()
104
105 // No config needed — flag completions are static.
106 completions, directive := completeArgs(nil, nil, "--")
107 if directive != cobra.ShellCompDirectiveNoFileComp {
108 t.Fatalf("expected NoFileComp, got %v", directive)
109 }
110 sort.Strings(completions)
111 want := []string{"--config", "--dry-run", "--help"}
112 if !reflect.DeepEqual(completions, want) {
113 t.Fatalf("flag completions mismatch: got %v, want %v", completions, want)
114 }
115}
116
117func TestCompleteArgsSkipsFlags(t *testing.T) {
118 setupCompletionConfig(t)
119
120 // Simulate: pika --config ./pika.toml <tab>
121 // The flag and its value should not count as positionals, so we
122 // should still be at 0 positionals → presets + commands.
123 completions, directive := completeArgs(nil, []string{"--config", "./pika.toml"}, "")
124 if directive != cobra.ShellCompDirectiveNoFileComp {
125 t.Fatalf("expected NoFileComp, got %v", directive)
126 }
127
128 has := make(map[string]bool)
129 for _, c := range completions {
130 has[c] = true
131 }
132 if !has["backup"] {
133 t.Errorf("expected commands after flag+value, got %v", completions)
134 }
135 if !has["home"] {
136 t.Errorf("expected presets after flag+value, got %v", completions)
137 }
138}
139
140func TestCountPositionals(t *testing.T) {
141 t.Parallel()
142
143 tests := []struct {
144 name string
145 args []string
146 want int
147 }{
148 {name: "empty", args: nil, want: 0},
149 {name: "one positional", args: []string{"backup"}, want: 1},
150 {name: "two positionals", args: []string{"home", "backup"}, want: 2},
151 {name: "flag with value", args: []string{"--repo", "/repo"}, want: 0},
152 {name: "flag=value", args: []string{"--repo=/repo"}, want: 0},
153 {name: "mixed", args: []string{"--repo", "/repo", "home", "backup"}, want: 2},
154 {name: "double dash", args: []string{"home", "--", "a", "b"}, want: 3},
155 }
156 for _, tt := range tests {
157 t.Run(tt.name, func(t *testing.T) {
158 t.Parallel()
159 got := countPositionals(tt.args)
160 if got != tt.want {
161 t.Fatalf("countPositionals(%#v) = %d, want %d", tt.args, got, tt.want)
162 }
163 })
164 }
165}
166
167func TestIsKnownCommand(t *testing.T) {
168 t.Parallel()
169
170 tests := []struct {
171 name string
172 args []string
173 want bool
174 }{
175 {name: "empty", args: nil, want: false},
176 {name: "command first", args: []string{"backup"}, want: true},
177 {name: "preset first", args: []string{"home"}, want: false},
178 {name: "flag then command", args: []string{"--repo", "/repo", "backup"}, want: true},
179 {name: "flag then preset", args: []string{"--repo", "/repo", "home"}, want: false},
180 }
181 for _, tt := range tests {
182 t.Run(tt.name, func(t *testing.T) {
183 t.Parallel()
184 got := isKnownCommand(tt.args)
185 if got != tt.want {
186 t.Fatalf("isKnownCommand(%#v) = %v, want %v", tt.args, got, tt.want)
187 }
188 })
189 }
190}