cli + daemon: move test seams off package globals onto injected structs

CLI: introduce internal/cli.deps which owns every RPC/SSH/host-command
seam the tree used to reach through mutable package vars. Command
builders, orchestrators, and the completion helpers become methods on
*deps. Tests construct their own deps per case, so fakes no longer leak
across cases and tests are free to run in parallel.

Daemon: move workspaceInspectRepoFunc + workspaceImportFunc onto the
Daemon struct (workspaceInspectRepo / workspaceImport), mirroring the
existing guestWaitForSSH / guestDial pattern. Workspace-prepare tests
drop t.Parallel() guards now that they no longer mutate process-wide
state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-19 19:03:55 -03:00
parent d38f580e00
commit c42fcbe012
No known key found for this signature in database
GPG key ID: 33112E6833C34679
19 changed files with 664 additions and 733 deletions

View file

@ -12,10 +12,11 @@ import (
)
// 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.
// 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,
@ -24,28 +25,19 @@ func stubCompletionSeams(
) {
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) {
d.daemonPing = 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) {
d.completionLister = 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) {
d.completionSessionLister = func(ctx context.Context, socketPath, vmIDOrName string) ([]string, error) {
if sessionErr != nil {
return nil, sessionErr
}
@ -89,9 +81,10 @@ func testCmdWithCtx() *cobra.Command {
}
func TestCompleteVMNamesHappyPath(t *testing.T) {
stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil, nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil, nil, nil)
got, directive := completeVMNames(testCmdWithCtx(), nil, "")
got, directive := d.completeVMNames(testCmdWithCtx(), nil, "")
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("directive = %d, want NoFileComp", directive)
}
@ -101,9 +94,10 @@ func TestCompleteVMNamesHappyPath(t *testing.T) {
}
func TestCompleteVMNamesDaemonDown(t *testing.T) {
stubCompletionSeams(t, errors.New("connection refused"), nil, nil, nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, errors.New("connection refused"), nil, nil, nil, nil)
got, directive := completeVMNames(testCmdWithCtx(), nil, "")
got, directive := d.completeVMNames(testCmdWithCtx(), nil, "")
if len(got) != 0 {
t.Errorf("daemon-down should return no suggestions, got %v", got)
}
@ -113,18 +107,20 @@ func TestCompleteVMNamesDaemonDown(t *testing.T) {
}
func TestCompleteVMNamesRPCError(t *testing.T) {
stubCompletionSeams(t, nil, nil, errors.New("rpc failed"), nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, nil, nil, errors.New("rpc failed"), nil, nil)
got, _ := completeVMNames(testCmdWithCtx(), nil, "")
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) {
stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil, nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "gamma"}}, nil, nil, nil)
got, _ := completeVMNames(testCmdWithCtx(), []string{"alpha"}, "")
got, _ := d.completeVMNames(testCmdWithCtx(), []string{"alpha"}, "")
want := []string{"beta", "gamma"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
@ -132,9 +128,10 @@ func TestCompleteVMNamesExcludesAlreadyEntered(t *testing.T) {
}
func TestCompleteVMNamesPrefixFilter(t *testing.T) {
stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha", "beta", "alphabet"}}, nil, nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha", "beta", "alphabet"}}, nil, nil, nil)
got, _ := completeVMNames(testCmdWithCtx(), nil, "alp")
got, _ := d.completeVMNames(testCmdWithCtx(), nil, "alp")
want := []string{"alpha", "alphabet"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
@ -142,49 +139,53 @@ func TestCompleteVMNamesPrefixFilter(t *testing.T) {
}
func TestCompleteVMNameOnlyAtPos0(t *testing.T) {
stubCompletionSeams(t, nil, map[string][]string{"vm.list": {"alpha"}}, nil, nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"vm.list": {"alpha"}}, nil, nil, nil)
atPos0, _ := completeVMNameOnlyAtPos0(testCmdWithCtx(), nil, "")
atPos0, _ := d.completeVMNameOnlyAtPos0(testCmdWithCtx(), nil, "")
if len(atPos0) != 1 || atPos0[0] != "alpha" {
t.Errorf("pos 0: got %v", atPos0)
}
atPos1, _ := completeVMNameOnlyAtPos0(testCmdWithCtx(), []string{"alpha"}, "")
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) {
stubCompletionSeams(t, nil, map[string][]string{"image.list": {"debian-bookworm", "alpine"}}, nil, nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"image.list": {"debian-bookworm", "alpine"}}, nil, nil, nil)
got, _ := completeImageNames(testCmdWithCtx(), nil, "")
got, _ := d.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)
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"kernel.list": {"generic-6.12"}}, nil, nil, nil)
got, _ := completeKernelNames(testCmdWithCtx(), 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) {
stubCompletionSeams(t, nil, map[string][]string{"image.list": {"alpine"}}, nil, nil, nil)
d := defaultDeps()
stubCompletionSeams(t, d, nil, map[string][]string{"image.list": {"alpine"}}, nil, nil, nil)
after, _ := completeImageNameOnlyAtPos0(testCmdWithCtx(), []string{"alpine"}, "")
after, _ := d.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,
d := defaultDeps()
stubCompletionSeams(t, d,
nil,
map[string][]string{"vm.list": {"devbox"}},
nil,
@ -193,34 +194,35 @@ func TestCompleteSessionNames(t *testing.T) {
)
// Position 0 → VMs.
vms, _ := completeSessionNames(testCmdWithCtx(), nil, "")
vms, _ := d.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"}, "")
sessions, _ := d.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")
filtered, _ := d.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"}, "")
past, _ := d.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)
d := defaultDeps()
stubCompletionSeams(t, d, errors.New("down"), nil, nil, nil, nil)
got, directive := completeSessionNames(testCmdWithCtx(), []string{"devbox"}, "")
got, directive := d.completeSessionNames(testCmdWithCtx(), []string{"devbox"}, "")
if len(got) != 0 {
t.Errorf("expected no suggestions when daemon down, got %v", got)
}