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

@ -14,22 +14,17 @@ import (
"github.com/spf13/cobra"
)
// stubPruneSeams installs fakes for vmListFunc and vmDeleteFunc, and
// restores originals on cleanup.
func stubPruneSeams(t *testing.T, vms []model.VMRecord, listErr error, deleteErr map[string]error) *[]string {
// stubPruneSeams installs list + delete fakes onto the caller's *deps
// and returns a pointer to a slice that records every ID passed to the
// delete fake.
func stubPruneSeams(t *testing.T, d *deps, vms []model.VMRecord, listErr error, deleteErr map[string]error) *[]string {
t.Helper()
origList := vmListFunc
origDelete := vmDeleteFunc
t.Cleanup(func() {
vmListFunc = origList
vmDeleteFunc = origDelete
})
var deleted []string
vmListFunc = func(ctx context.Context, socketPath string) (api.VMListResult, error) {
d.vmList = func(ctx context.Context, socketPath string) (api.VMListResult, error) {
return api.VMListResult{VMs: vms}, listErr
}
vmDeleteFunc = func(ctx context.Context, socketPath, idOrName string) error {
d.vmDelete = func(ctx context.Context, socketPath, idOrName string) error {
if err, ok := deleteErr[idOrName]; ok {
return err
}
@ -89,13 +84,14 @@ func TestPromptYesNoEOF(t *testing.T) {
}
func TestRunVMPruneNoVictims(t *testing.T) {
stubPruneSeams(t, []model.VMRecord{
d := defaultDeps()
stubPruneSeams(t, d, []model.VMRecord{
{ID: "id-1", Name: "running-vm", State: model.VMStateRunning},
}, nil, nil)
cmd, stdout, _ := newPruneTestCmd("")
if err := runVMPrune(cmd, "sock", false); err != nil {
t.Fatalf("runVMPrune: %v", err)
if err := d.runVMPrune(cmd, "sock", false); err != nil {
t.Fatalf("d.runVMPrune: %v", err)
}
if !strings.Contains(stdout.String(), "no non-running VMs") {
t.Errorf("expected no-op message, got %q", stdout.String())
@ -103,13 +99,14 @@ func TestRunVMPruneNoVictims(t *testing.T) {
}
func TestRunVMPruneAbortedByUser(t *testing.T) {
deleted := stubPruneSeams(t, []model.VMRecord{
d := defaultDeps()
deleted := stubPruneSeams(t, d, []model.VMRecord{
{ID: "id-1", Name: "stale", State: model.VMStateStopped},
}, nil, nil)
cmd, stdout, _ := newPruneTestCmd("n\n")
if err := runVMPrune(cmd, "sock", false); err != nil {
t.Fatalf("runVMPrune: %v", err)
if err := d.runVMPrune(cmd, "sock", false); err != nil {
t.Fatalf("d.runVMPrune: %v", err)
}
if !strings.Contains(stdout.String(), "aborted") {
t.Errorf("expected 'aborted' output, got %q", stdout.String())
@ -120,7 +117,8 @@ func TestRunVMPruneAbortedByUser(t *testing.T) {
}
func TestRunVMPruneConfirmedDeletesNonRunning(t *testing.T) {
deleted := stubPruneSeams(t, []model.VMRecord{
d := defaultDeps()
deleted := stubPruneSeams(t, d, []model.VMRecord{
{ID: "id-run", Name: "keeper", State: model.VMStateRunning},
{ID: "id-stop", Name: "stale", State: model.VMStateStopped},
{ID: "id-err", Name: "broken", State: model.VMStateError},
@ -128,8 +126,8 @@ func TestRunVMPruneConfirmedDeletesNonRunning(t *testing.T) {
}, nil, nil)
cmd, stdout, _ := newPruneTestCmd("y\n")
if err := runVMPrune(cmd, "sock", false); err != nil {
t.Fatalf("runVMPrune: %v", err)
if err := d.runVMPrune(cmd, "sock", false); err != nil {
t.Fatalf("d.runVMPrune: %v", err)
}
// Deleted must be exactly the three non-running IDs, in list order.
want := []string{"id-stop", "id-err", "id-created"}
@ -152,14 +150,15 @@ func TestRunVMPruneConfirmedDeletesNonRunning(t *testing.T) {
}
func TestRunVMPruneForceSkipsPrompt(t *testing.T) {
deleted := stubPruneSeams(t, []model.VMRecord{
d := defaultDeps()
deleted := stubPruneSeams(t, d, []model.VMRecord{
{ID: "id-1", Name: "stale", State: model.VMStateStopped},
}, nil, nil)
// Empty stdin + force=true: must not block on prompt.
cmd, stdout, _ := newPruneTestCmd("")
if err := runVMPrune(cmd, "sock", true); err != nil {
t.Fatalf("runVMPrune: %v", err)
if err := d.runVMPrune(cmd, "sock", true); err != nil {
t.Fatalf("d.runVMPrune: %v", err)
}
if len(*deleted) != 1 || (*deleted)[0] != "id-1" {
t.Errorf("deleted = %v, want [id-1]", *deleted)
@ -171,7 +170,8 @@ func TestRunVMPruneForceSkipsPrompt(t *testing.T) {
}
func TestRunVMPruneReportsPartialFailure(t *testing.T) {
stubPruneSeams(t,
d := defaultDeps()
stubPruneSeams(t, d,
[]model.VMRecord{
{ID: "id-a", Name: "a", State: model.VMStateStopped},
{ID: "id-b", Name: "b", State: model.VMStateStopped},
@ -181,7 +181,7 @@ func TestRunVMPruneReportsPartialFailure(t *testing.T) {
)
cmd, _, stderr := newPruneTestCmd("")
err := runVMPrune(cmd, "sock", true)
err := d.runVMPrune(cmd, "sock", true)
if err == nil {
t.Fatal("expected non-zero exit when any delete fails")
}
@ -194,10 +194,11 @@ func TestRunVMPruneReportsPartialFailure(t *testing.T) {
}
func TestRunVMPruneListErrorPropagates(t *testing.T) {
stubPruneSeams(t, nil, fmt.Errorf("rpc failed"), nil)
d := defaultDeps()
stubPruneSeams(t, d, nil, fmt.Errorf("rpc failed"), nil)
cmd, _, _ := newPruneTestCmd("")
err := runVMPrune(cmd, "sock", true)
err := d.runVMPrune(cmd, "sock", true)
if err == nil || !strings.Contains(err.Error(), "rpc failed") {
t.Fatalf("expected rpc error to propagate, got %v", err)
}