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>
205 lines
5.8 KiB
Go
205 lines
5.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/model"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// 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()
|
|
|
|
var deleted []string
|
|
d.vmList = func(ctx context.Context, socketPath string) (api.VMListResult, error) {
|
|
return api.VMListResult{VMs: vms}, listErr
|
|
}
|
|
d.vmDelete = func(ctx context.Context, socketPath, idOrName string) error {
|
|
if err, ok := deleteErr[idOrName]; ok {
|
|
return err
|
|
}
|
|
deleted = append(deleted, idOrName)
|
|
return nil
|
|
}
|
|
return &deleted
|
|
}
|
|
|
|
func newPruneTestCmd(stdin string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
|
|
cmd := &cobra.Command{Use: "prune"}
|
|
cmd.SetContext(context.Background())
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
cmd.SetIn(strings.NewReader(stdin))
|
|
cmd.SetOut(stdout)
|
|
cmd.SetErr(stderr)
|
|
return cmd, stdout, stderr
|
|
}
|
|
|
|
func TestPromptYesNo(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"y\n": true,
|
|
"Y\n": true,
|
|
"yes\n": true,
|
|
"YES\n": true,
|
|
" y \n": true,
|
|
"n\n": false,
|
|
"no\n": false,
|
|
"\n": false,
|
|
"anything\n": false,
|
|
}
|
|
for input, want := range cases {
|
|
out := &bytes.Buffer{}
|
|
got, err := promptYesNo(strings.NewReader(input), out, "go? ")
|
|
if err != nil {
|
|
t.Errorf("input %q: error %v", input, err)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("input %q: got %v, want %v", input, got, want)
|
|
}
|
|
if !strings.Contains(out.String(), "go?") {
|
|
t.Errorf("input %q: prompt not written; got %q", input, out.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPromptYesNoEOF(t *testing.T) {
|
|
got, err := promptYesNo(strings.NewReader(""), &bytes.Buffer{}, "? ")
|
|
if err != nil {
|
|
t.Fatalf("EOF should not error: %v", err)
|
|
}
|
|
if got {
|
|
t.Fatal("EOF should be treated as no")
|
|
}
|
|
}
|
|
|
|
func TestRunVMPruneNoVictims(t *testing.T) {
|
|
d := defaultDeps()
|
|
stubPruneSeams(t, d, []model.VMRecord{
|
|
{ID: "id-1", Name: "running-vm", State: model.VMStateRunning},
|
|
}, nil, nil)
|
|
|
|
cmd, stdout, _ := newPruneTestCmd("")
|
|
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())
|
|
}
|
|
}
|
|
|
|
func TestRunVMPruneAbortedByUser(t *testing.T) {
|
|
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 := 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())
|
|
}
|
|
if len(*deleted) != 0 {
|
|
t.Errorf("should not have deleted anything, got %v", *deleted)
|
|
}
|
|
}
|
|
|
|
func TestRunVMPruneConfirmedDeletesNonRunning(t *testing.T) {
|
|
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},
|
|
{ID: "id-created", Name: "fresh", State: model.VMStateCreated},
|
|
}, nil, nil)
|
|
|
|
cmd, stdout, _ := newPruneTestCmd("y\n")
|
|
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"}
|
|
if len(*deleted) != len(want) {
|
|
t.Fatalf("deleted = %v, want %v", *deleted, want)
|
|
}
|
|
for i, id := range want {
|
|
if (*deleted)[i] != id {
|
|
t.Errorf("deleted[%d] = %q, want %q", i, (*deleted)[i], id)
|
|
}
|
|
}
|
|
for _, want := range []string{"stale", "broken", "fresh"} {
|
|
if !strings.Contains(stdout.String(), "deleted "+want) {
|
|
t.Errorf("output missing 'deleted %s':\n%s", want, stdout.String())
|
|
}
|
|
}
|
|
if strings.Contains(stdout.String(), "deleted keeper") {
|
|
t.Errorf("running VM should not be deleted:\n%s", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestRunVMPruneForceSkipsPrompt(t *testing.T) {
|
|
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 := 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)
|
|
}
|
|
// Prompt should not appear in output.
|
|
if strings.Contains(stdout.String(), "Delete these VMs?") {
|
|
t.Errorf("force=true should skip prompt:\n%s", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestRunVMPruneReportsPartialFailure(t *testing.T) {
|
|
d := defaultDeps()
|
|
stubPruneSeams(t, d,
|
|
[]model.VMRecord{
|
|
{ID: "id-a", Name: "a", State: model.VMStateStopped},
|
|
{ID: "id-b", Name: "b", State: model.VMStateStopped},
|
|
},
|
|
nil,
|
|
map[string]error{"id-a": errors.New("simulated")},
|
|
)
|
|
|
|
cmd, _, stderr := newPruneTestCmd("")
|
|
err := d.runVMPrune(cmd, "sock", true)
|
|
if err == nil {
|
|
t.Fatal("expected non-zero exit when any delete fails")
|
|
}
|
|
if !strings.Contains(err.Error(), "1 VM(s) failed") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(stderr.String(), "delete a:") {
|
|
t.Errorf("stderr missing failure log: %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunVMPruneListErrorPropagates(t *testing.T) {
|
|
d := defaultDeps()
|
|
stubPruneSeams(t, d, nil, fmt.Errorf("rpc failed"), nil)
|
|
|
|
cmd, _, _ := newPruneTestCmd("")
|
|
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)
|
|
}
|
|
}
|