completions_test.go

  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}