Replace the shell-only user workflow with `banger` and `bangerd`: Cobra commands, XDG/SQLite-backed state, managed VM and image lifecycle, and a Bubble Tea TUI for browsing and operating VMs.\n\nKeep Firecracker orchestration behind the daemon so VM specs become persistent objects, and add repo entrypoints for building, installing, and documenting the new flow while still delegating rootfs customization to the existing shell tooling.\n\nHarden the control plane around real usage by reclaiming Firecracker API sockets for the user, restarting stale daemons after rebuilds, and returning the correct `vm.create` payload so the CLI and TUI creation flow work reliably.\n\nValidation: `go test ./...`, `make build`, and a host-side smoke test with `./banger vm create --name codex-smoke`.
129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"banger/internal/model"
|
|
)
|
|
|
|
func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
|
|
cmd := NewBangerCommand()
|
|
names := []string{}
|
|
for _, sub := range cmd.Commands() {
|
|
names = append(names, sub.Name())
|
|
}
|
|
want := []string{"daemon", "image", "tui", "vm"}
|
|
if !reflect.DeepEqual(names, want) {
|
|
t.Fatalf("subcommands = %v, want %v", names, want)
|
|
}
|
|
}
|
|
|
|
func TestVMCreateFlagsExist(t *testing.T) {
|
|
root := NewBangerCommand()
|
|
vm, _, err := root.Find([]string{"vm"})
|
|
if err != nil {
|
|
t.Fatalf("find vm: %v", err)
|
|
}
|
|
create, _, err := vm.Find([]string{"create"})
|
|
if err != nil {
|
|
t.Fatalf("find create: %v", err)
|
|
}
|
|
for _, flagName := range []string{"name", "image", "vcpu", "memory", "system-overlay-size", "disk-size", "nat", "no-start"} {
|
|
if create.Flags().Lookup(flagName) == nil {
|
|
t.Fatalf("missing flag %q", flagName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVMSetParamsFromFlags(t *testing.T) {
|
|
params, err := vmSetParamsFromFlags("devbox", 4, 2048, "16G", true, false)
|
|
if err != nil {
|
|
t.Fatalf("vmSetParamsFromFlags: %v", err)
|
|
}
|
|
if params.IDOrName != "devbox" || params.VCPUCount == nil || *params.VCPUCount != 4 {
|
|
t.Fatalf("unexpected params: %+v", params)
|
|
}
|
|
if params.MemoryMiB == nil || *params.MemoryMiB != 2048 {
|
|
t.Fatalf("unexpected memory: %+v", params)
|
|
}
|
|
if params.WorkDiskSize != "16G" {
|
|
t.Fatalf("unexpected disk size: %+v", params)
|
|
}
|
|
if params.NATEnabled == nil || !*params.NATEnabled {
|
|
t.Fatalf("unexpected nat value: %+v", params)
|
|
}
|
|
}
|
|
|
|
func TestVMSetParamsFromFlagsConflict(t *testing.T) {
|
|
if _, err := vmSetParamsFromFlags("devbox", -1, -1, "", true, true); err == nil {
|
|
t.Fatal("expected nat conflict error")
|
|
}
|
|
}
|
|
|
|
func TestSSHCommandArgs(t *testing.T) {
|
|
args, err := sshCommandArgs(model.DaemonConfig{RepoRoot: "/repo"}, "172.16.0.2", []string{"--", "uname", "-a"})
|
|
if err != nil {
|
|
t.Fatalf("sshCommandArgs: %v", err)
|
|
}
|
|
want := []string{
|
|
"-i", "/repo/id_ed25519",
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"root@172.16.0.2",
|
|
"--", "uname", "-a",
|
|
}
|
|
if !reflect.DeepEqual(args, want) {
|
|
t.Fatalf("args = %v, want %v", args, want)
|
|
}
|
|
}
|
|
|
|
func TestNewBangerdCommandRejectsArgs(t *testing.T) {
|
|
cmd := NewBangerdCommand()
|
|
cmd.SetArgs([]string{"extra"})
|
|
if err := cmd.Execute(); err == nil {
|
|
t.Fatal("expected extra args to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestDaemonOutdated(t *testing.T) {
|
|
dir := t.TempDir()
|
|
current := filepath.Join(dir, "bangerd-current")
|
|
same := filepath.Join(dir, "bangerd-same")
|
|
stale := filepath.Join(dir, "bangerd-stale")
|
|
if err := os.WriteFile(current, []byte("current"), 0o755); err != nil {
|
|
t.Fatalf("write current: %v", err)
|
|
}
|
|
if err := os.Link(current, same); err != nil {
|
|
t.Fatalf("hard link: %v", err)
|
|
}
|
|
if err := os.WriteFile(stale, []byte("stale"), 0o755); err != nil {
|
|
t.Fatalf("write stale: %v", err)
|
|
}
|
|
|
|
origBangerdPath := bangerdPathFunc
|
|
origDaemonExePath := daemonExePath
|
|
t.Cleanup(func() {
|
|
bangerdPathFunc = origBangerdPath
|
|
daemonExePath = origDaemonExePath
|
|
})
|
|
|
|
bangerdPathFunc = func() (string, error) {
|
|
return current, nil
|
|
}
|
|
daemonExePath = func(pid int) string {
|
|
if pid == 1 {
|
|
return same
|
|
}
|
|
return stale
|
|
}
|
|
|
|
if daemonOutdated(1) {
|
|
t.Fatal("expected matching daemon executable to be current")
|
|
}
|
|
if !daemonOutdated(2) {
|
|
t.Fatal("expected replaced daemon executable to be outdated")
|
|
}
|
|
}
|