banger/internal/cli/completion_test.go
Thales Maciel 2b6437d1b4
remove vm session feature
Cuts the daemon-managed guest-session machinery (start/list/show/
logs/stop/kill/attach/send). The feature shipped aimed at agent-
orchestration workflows (programmatic stdin piping into a long-lived
guest process) that aren't driving any concrete user today, and the
~2.3K LOC of daemon surface area — attach bridge, FIFO keepalive,
controller registry, sessionstream framing, SQLite persistence — was
locking in an API we'd have to keep through v0.1.0.

Anything session-flavoured that people actually need today can be
done with `vm ssh + tmux` or `vm run -- cmd`.

Deleted:
- internal/cli/commands_vm_session.go
- internal/daemon/{guest_sessions,session_lifecycle,session_attach,session_stream,session_controller}.go
- internal/daemon/session/ (guest-session helpers package)
- internal/sessionstream/ (framing package)
- internal/daemon/guest_sessions_test.go
- internal/store/guest_session_test.go
- GuestSession* types from internal/{api,model}
- Store UpsertGuestSession/GetGuestSession/ListGuestSessionsByVM/DeleteGuestSession + scanner helpers
- guest.session.* RPC dispatch entries
- 5 CLI session tests, 2 completion tests, 2 printer tests

Extracted:
- ShellQuote + FormatStepError lifted to internal/daemon/workspace/util.go
  (only non-session consumer); workspace package now self-contained
- internal/daemon/guest_ssh.go keeps guestSSHClient + dialGuest +
  waitForGuestSSH — still used by workspace prepare/export
- internal/daemon/fake_firecracker_test.go preserves the test helper
  that used to live in guest_sessions_test.go

Store schema: CREATE TABLE guest_sessions and its column migrations
removed. Existing dev DBs keep an orphan table (harmless, pre-v0.1.0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:47:58 -03:00

175 lines
5.1 KiB
Go

package cli
import (
"context"
"errors"
"reflect"
"testing"
"banger/internal/api"
"github.com/spf13/cobra"
)
// stubCompletionSeams installs test doubles for the daemon ping + lister
// seams on the caller's *deps. Tests opt into the sub-functions they
// actually need.
func stubCompletionSeams(
t *testing.T,
d *deps,
pingErr error,
names map[string][]string,
listErr error) {
t.Helper()
d.daemonPing = func(ctx context.Context, socketPath string) (api.PingResult, error) {
if pingErr != nil {
return api.PingResult{}, pingErr
}
return api.PingResult{}, nil
}
d.completionLister = func(ctx context.Context, socketPath, method string) ([]string, error) {
if listErr != nil {
return nil, listErr
}
return names[method], 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) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil)
got, directive := d.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) {
d := defaultDeps()
stubCompletionSeams(t, d, errors.New("connection refused"), nil, nil)
got, directive := d.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) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, nil, errors.New("rpc failed"))
got, _ := d.completeVMNames(testCmdWithCtx(), nil, "")
if len(got) != 0 {
t.Errorf("rpc error should return no suggestions, got %v", got)
}
}
func TestCompleteVMNamesExcludesAlreadyEntered(t *testing.T) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil)
got, _ := d.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) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "alphabet"}}, nil)
got, _ := d.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) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha"}}, nil)
atPos0, _ := d.completeVMNameOnlyAtPos0(testCmdWithCtx(), nil, "")
if len(atPos0) != 1 || atPos0[0] != "alpha" {
t.Errorf("pos 0: got %v", atPos0)
}
atPos1, _ := d.completeVMNameOnlyAtPos0(testCmdWithCtx(), []string{"alpha"}, "")
if len(atPos1) != 0 {
t.Errorf("pos 1+ should be silent, got %v", atPos1)
}
}
func TestCompleteImageNames(t *testing.T) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"image.list": {"debian-bookworm", "alpine"}}, nil)
got, _ := d.completeImageNames(testCmdWithCtx(), nil, "")
if !reflect.DeepEqual(got, []string{"debian-bookworm", "alpine"}) {
t.Errorf("got %v", got)
}
}
func TestCompleteKernelNames(t *testing.T) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"kernel.list": {"generic-6.12"}}, nil)
got, _ := d.completeKernelNames(testCmdWithCtx(), nil, "")
if len(got) != 1 || got[0] != "generic-6.12" {
t.Errorf("got %v", got)
}
}
func TestCompleteImageNameOnlyAtPos0SilentAfterFirst(t *testing.T) {
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"image.list": {"alpine"}}, nil)
after, _ := d.completeImageNameOnlyAtPos0(testCmdWithCtx(), []string{"alpine"}, "")
if len(after) != 0 {
t.Errorf("expected silence at pos 1+, got %v", after)
}
}