package cli import ( "context" "errors" "reflect" "testing" "banger/internal/api" "github.com/spf13/cobra" ) // stubCompletionSeams installs test doubles for the daemon ping + lister // seams and restores the originals on cleanup. Tests opt into the // sub-functions they actually need. func stubCompletionSeams( t *testing.T, pingErr error, names map[string][]string, listErr error, sessions map[string][]string, sessionErr error, ) { t.Helper() origPing := daemonPingFunc origLister := completionListerFunc origSessionLister := completionSessionListerFunc t.Cleanup(func() { daemonPingFunc = origPing completionListerFunc = origLister completionSessionListerFunc = origSessionLister }) daemonPingFunc = func(ctx context.Context, socketPath string) (api.PingResult, error) { if pingErr != nil { return api.PingResult{}, pingErr } return api.PingResult{}, nil } completionListerFunc = func(ctx context.Context, socketPath, method string) ([]string, error) { if listErr != nil { return nil, listErr } return names[method], nil } completionSessionListerFunc = func(ctx context.Context, socketPath, vmIDOrName string) ([]string, error) { if sessionErr != nil { return nil, sessionErr } return sessions[vmIDOrName], nil } } func TestFilterPrefix(t *testing.T) { cases := []struct { name string candidates []string exclude []string prefix string want []string }{ {"no filter", []string{"a", "b"}, nil, "", []string{"a", "b"}}, {"prefix match", []string{"apple", "banana", "apricot"}, nil, "ap", []string{"apple", "apricot"}}, {"exclude already entered", []string{"a", "b", "c"}, []string{"b"}, "", []string{"a", "c"}}, {"prefix + exclude", []string{"alpha", "avocado", "banana"}, []string{"alpha"}, "a", []string{"avocado"}}, {"exact case sensitive", []string{"VM", "vm"}, nil, "v", []string{"vm"}}, {"empty candidates", nil, nil, "any", nil}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := filterPrefix(tc.candidates, tc.exclude, tc.prefix) if !reflect.DeepEqual(got, tc.want) { // Allow nil == empty if len(got) == 0 && len(tc.want) == 0 { return } t.Errorf("got %v, want %v", got, tc.want) } }) } } func testCmdWithCtx() *cobra.Command { cmd := &cobra.Command{Use: "test"} cmd.SetContext(context.Background()) return cmd } func TestCompleteVMNamesHappyPath(t *testing.T) { stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil, nil, nil) got, directive := completeVMNames(testCmdWithCtx(), nil, "") if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %d, want NoFileComp", directive) } if !reflect.DeepEqual(got, []string{"alpha", "beta", "gamma"}) { t.Errorf("got %v", got) } } func TestCompleteVMNamesDaemonDown(t *testing.T) { stubCompletionSeams(t, errors.New("connection refused"), nil, nil, nil, nil) got, directive := completeVMNames(testCmdWithCtx(), nil, "") if len(got) != 0 { t.Errorf("daemon-down should return no suggestions, got %v", got) } if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %d, want NoFileComp", directive) } } func TestCompleteVMNamesRPCError(t *testing.T) { stubCompletionSeams(t, nil, nil, errors.New("rpc failed"), nil, nil) got, _ := completeVMNames(testCmdWithCtx(), nil, "") if len(got) != 0 { t.Errorf("rpc error should return no suggestions, got %v", got) } } func TestCompleteVMNamesExcludesAlreadyEntered(t *testing.T) { stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil, nil, nil) got, _ := completeVMNames(testCmdWithCtx(), []string{"alpha"}, "") want := []string{"beta", "gamma"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } func TestCompleteVMNamesPrefixFilter(t *testing.T) { stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha", "beta", "alphabet"}}, nil, nil, nil) got, _ := completeVMNames(testCmdWithCtx(), nil, "alp") want := []string{"alpha", "alphabet"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } func TestCompleteVMNameOnlyAtPos0(t *testing.T) { stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha"}}, nil, nil, nil) atPos0, _ := completeVMNameOnlyAtPos0(testCmdWithCtx(), nil, "") if len(atPos0) != 1 || atPos0[0] != "alpha" { t.Errorf("pos 0: got %v", atPos0) } atPos1, _ := completeVMNameOnlyAtPos0(testCmdWithCtx(), []string{"alpha"}, "") if len(atPos1) != 0 { t.Errorf("pos 1+ should be silent, got %v", atPos1) } } func TestCompleteImageNames(t *testing.T) { stubCompletionSeams(t, nil, map[string][]string{"image.list": {"debian-bookworm", "alpine"}}, nil, nil, nil) got, _ := completeImageNames(testCmdWithCtx(), nil, "") if !reflect.DeepEqual(got, []string{"debian-bookworm", "alpine"}) { t.Errorf("got %v", got) } } func TestCompleteKernelNames(t *testing.T) { stubCompletionSeams(t, nil, map[string][]string{"kernel.list": {"generic-6.12"}}, nil, nil, nil) got, _ := completeKernelNames(testCmdWithCtx(), nil, "") if len(got) != 1 || got[0] != "generic-6.12" { t.Errorf("got %v", got) } } func TestCompleteImageNameOnlyAtPos0SilentAfterFirst(t *testing.T) { stubCompletionSeams(t, nil, map[string][]string{"image.list": {"alpine"}}, nil, nil, nil) after, _ := completeImageNameOnlyAtPos0(testCmdWithCtx(), []string{"alpine"}, "") if len(after) != 0 { t.Errorf("expected silence at pos 1+, got %v", after) } } func TestCompleteSessionNames(t *testing.T) { stubCompletionSeams( t, nil, map[string][]string{"vm.list": {"devbox"}}, nil, map[string][]string{"devbox": {"planner", "worker"}}, nil, ) // Position 0 → VMs. vms, _ := completeSessionNames(testCmdWithCtx(), nil, "") if len(vms) != 1 || vms[0] != "devbox" { t.Errorf("pos 0: got %v", vms) } // Position 1 → sessions scoped to args[0]. sessions, _ := completeSessionNames(testCmdWithCtx(), []string{"devbox"}, "") if !reflect.DeepEqual(sessions, []string{"planner", "worker"}) { t.Errorf("pos 1: got %v", sessions) } // Position 1 with prefix filter. filtered, _ := completeSessionNames(testCmdWithCtx(), []string{"devbox"}, "wor") if len(filtered) != 1 || filtered[0] != "worker" { t.Errorf("pos 1 prefix: got %v", filtered) } // Position 2+ silent. past, _ := completeSessionNames(testCmdWithCtx(), []string{"devbox", "planner"}, "") if len(past) != 0 { t.Errorf("pos 2+: got %v", past) } } func TestCompleteSessionNamesDaemonDown(t *testing.T) { stubCompletionSeams(t, errors.New("down"), nil, nil, nil, nil) got, directive := completeSessionNames(testCmdWithCtx(), []string{"devbox"}, "") if len(got) != 0 { t.Errorf("expected no suggestions when daemon down, got %v", got) } if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %d, want NoFileComp", directive) } }