package commandkit import ( "encoding/json" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" ) func TestCompletionConsumerInShells(t *testing.T) { if os.Getenv("COMMANDKIT_COMPLETION_SHELLS") != "1" { t.Skip("set COMMANDKIT_COMPLETION_SHELLS=1 to run shell completion checks") } dir := t.TempDir() app := filepath.Join(dir, "probe") build := exec.Command("go", "build", "-o", app, "./testdata/completion") if output, err := build.CombinedOutput(); err != nil { t.Fatalf("build completion fixture: %v\n%s", err, output) } if err := os.Mkdir(filepath.Join(dir, "partial dir"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "partial file"), nil, 0o644); err != nil { t.Fatal(err) } tests := []struct { name, line string want []string directory bool }{ {"canonical command", "probe ru", []string{"run"}, false}, {"entered alias", "probe shortcut --format=ya", []string{"shortcut", "--format=yaml"}, false}, {"choice", "probe run --format=te", []string{"run", "--format=text"}, false}, {"no file fallback", `probe run --format=./partial\ f`, []string{"run", "--format=./partial f"}, false}, {"separated file", `probe run --config ./partial\ f`, []string{"run", "--config", "./partial file"}, false}, {"attached file", `probe run --config=./partial\ f`, []string{"run", "--config=./partial file"}, false}, {"short attached file", `probe run -c./partial\ f`, []string{"run", "-c./partial file"}, false}, {"short cluster equals file", `probe run -sc=./partial\ f`, []string{"run", "-sc=./partial file"}, false}, {"separated directory", "probe run --directory ./partial", []string{"run", "--directory", "./partial dir"}, true}, {"attached directory", "probe run --directory=./partial", []string{"run", "--directory=./partial dir"}, true}, {"empty operand", "probe run ", []string{"run", "alpha"}, false}, {"leading dash", "probe run --for", []string{"run", "--format"}, false}, } for _, shell := range []string{"bash", "zsh", "fish"} { generator := exec.Command(app, "completion", shell) script, err := generator.CombinedOutput() if err != nil { t.Fatalf("generate %s script: %v\n%s", shell, err, script) } scriptPath := filepath.Join(dir, shell+".completion") if err := os.WriteFile(scriptPath, script, 0o600); err != nil { t.Fatal(err) } for _, tc := range tests { t.Run(shell+"/"+tc.name, func(t *testing.T) { log := filepath.Join(dir, "log") if err := os.WriteFile(log, nil, 0o600); err != nil { t.Fatal(err) } terminal := exec.Command("python3", "internal/cobra/testdata/completion_terminal.py", shell, scriptPath, tc.line) terminal.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "HOME="+dir, "TERM=xterm", "PROBE_LOG="+log, "PROBE_DIR="+dir) if output, err := terminal.CombinedOutput(); err != nil { t.Fatalf("shell completion: %v\n%s", err, output) } content, err := os.ReadFile(log) if err != nil { t.Fatal(err) } var got []string if err := json.Unmarshal(content, &got); err != nil { t.Fatalf("fixture output %q: %v", content, err) } if tc.directory && len(got) > 0 { got[len(got)-1] = strings.TrimSuffix(got[len(got)-1], "/") } if !reflect.DeepEqual(got, tc.want) { t.Fatalf("edited arguments=%q, want %q", got, tc.want) } }) } t.Run(shell+"/descriptions", func(t *testing.T) { terminal := exec.Command("python3", "internal/cobra/testdata/completion_terminal.py", shell, scriptPath, "probe run --format=") terminal.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "HOME="+dir, "TERM=xterm", "PROBE_LOG="+filepath.Join(dir, "log"), "PROBE_DIR="+dir, "PROBE_LIST=1") output, err := terminal.CombinedOutput() if err != nil || !strings.Contains(string(output), "Readable text") || !strings.Contains(string(output), "YAML output") { t.Fatalf("completion descriptions: %v\n%s", err, output) } }) } }