package cli import ( "bytes" "errors" "fmt" "reflect" "strings" "testing" "banger/internal/api" "banger/internal/model" "github.com/spf13/cobra" ) func TestHumanSize(t *testing.T) { cases := []struct { bytes int64 want string }{ {-1, "-"}, {0, "-"}, {1, "1B"}, {1023, "1023B"}, {1024, "1.0KiB"}, {2048, "2.0KiB"}, {1024 * 1024, "1.0MiB"}, {5 * 1024 * 1024, "5.0MiB"}, {1024 * 1024 * 1024, "1.0GiB"}, {3 * 1024 * 1024 * 1024, "3.0GiB"}, } for _, tc := range cases { if got := humanSize(tc.bytes); got != tc.want { t.Errorf("humanSize(%d) = %q, want %q", tc.bytes, got, tc.want) } } } func TestDashIfEmpty(t *testing.T) { cases := map[string]string{ "": "-", " ": "-", "\t\n": "-", "value": "value", " hello ": " hello ", } for in, want := range cases { if got := dashIfEmpty(in); got != want { t.Errorf("dashIfEmpty(%q) = %q, want %q", in, got, want) } } } func TestParseKeyValuePairs(t *testing.T) { t.Run("nil when empty", func(t *testing.T) { got, err := parseKeyValuePairs(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != nil { t.Fatalf("got %v, want nil", got) } }) t.Run("parses entries", func(t *testing.T) { got, err := parseKeyValuePairs([]string{"a=1", " b = two", "c=x=y"}) if err != nil { t.Fatalf("unexpected error: %v", err) } want := map[string]string{"a": "1", "b": " two", "c": "x=y"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v", got, want) } }) t.Run("rejects malformed entries", func(t *testing.T) { for _, bad := range []string{"noequals", "=noKey", " =v"} { if _, err := parseKeyValuePairs([]string{bad}); err == nil { t.Errorf("expected error for %q", bad) } } }) } func TestExitCodeErrorError(t *testing.T) { e := ExitCodeError{Code: 42} got := e.Error() if !strings.Contains(got, "42") { t.Fatalf("error %q missing code", got) } var target ExitCodeError if !errors.As(error(e), &target) { t.Fatal("errors.As failed to match ExitCodeError") } if target.Code != 42 { t.Fatalf("target.Code = %d, want 42", target.Code) } } func TestShortID(t *testing.T) { cases := map[string]string{ "": "", "abc": "abc", "0123456789ab": "0123456789ab", "0123456789abcd": "0123456789ab", "0123456789abcdefghij": "0123456789ab", } for in, want := range cases { if got := shortID(in); got != want { t.Errorf("shortID(%q) = %q, want %q", in, got, want) } } } func TestImageNameIndex(t *testing.T) { images := []model.Image{ {ID: "id-a", Name: "alpha"}, {ID: "id-b", Name: "beta"}, } idx := imageNameIndex(images) if len(idx) != 2 { t.Fatalf("len = %d, want 2", len(idx)) } if idx["id-a"] != "alpha" || idx["id-b"] != "beta" { t.Fatalf("unexpected index %v", idx) } empty := imageNameIndex(nil) if empty == nil || len(empty) != 0 { t.Fatalf("expected empty non-nil map, got %v", empty) } } func TestHelpNoArgs(t *testing.T) { called := false cmd := &cobra.Command{ Use: "x", RunE: func(cmd *cobra.Command, args []string) error { called = true return nil }, } cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{}) if err := helpNoArgs(cmd, nil); err != nil { t.Fatalf("helpNoArgs(nil): %v", err) } if called { t.Fatal("helpNoArgs should not invoke Run") } if err := helpNoArgs(cmd, []string{"bogus"}); err == nil { t.Fatal("expected error for unexpected args") } } func TestArgsValidators(t *testing.T) { cmd := &cobra.Command{Use: "x"} exact := exactArgsUsage(2, "need exactly two") if err := exact(cmd, []string{"a", "b"}); err != nil { t.Fatalf("exact(2 args): %v", err) } if err := exact(cmd, []string{"a"}); err == nil { t.Fatal("expected error for 1 arg with exactArgsUsage(2)") } minArgs := minArgsUsage(1, "need at least one") if err := minArgs(cmd, []string{"a"}); err != nil { t.Fatalf("min(1 arg): %v", err) } if err := minArgs(cmd, nil); err == nil { t.Fatal("expected error for 0 args with minArgsUsage(1)") } maxArgs := maxArgsUsage(1, "at most one") if err := maxArgs(cmd, []string{"a"}); err != nil { t.Fatalf("max(1 arg): %v", err) } if err := maxArgs(cmd, []string{"a", "b"}); err == nil { t.Fatal("expected error for 2 args with maxArgsUsage(1)") } noArgs := noArgsUsage("none allowed") if err := noArgs(cmd, nil); err != nil { t.Fatalf("no args: %v", err) } if err := noArgs(cmd, []string{"a"}); err == nil { t.Fatal("expected error for args with noArgsUsage") } } func TestPrintKernelListTable(t *testing.T) { var buf bytes.Buffer entries := []api.KernelEntry{ {Name: "generic-6.12", Distro: "debian", Arch: "x86_64", KernelVersion: "6.12", ImportedAt: "2026-01-01"}, {Name: "bare"}, } if err := printKernelListTable(&buf, entries); err != nil { t.Fatalf("printKernelListTable: %v", err) } got := buf.String() for _, want := range []string{"NAME", "DISTRO", "generic-6.12", "bare"} { if !strings.Contains(got, want) { t.Errorf("output missing %q:\n%s", want, got) } } // Empty fields render as "-". if !strings.Contains(got, "-") { t.Errorf("expected dash for empty fields, got:\n%s", got) } } func TestPrintKernelCatalogTable(t *testing.T) { var buf bytes.Buffer entries := []api.KernelCatalogEntry{ {Name: "generic-6.12", Arch: "x86_64", KernelVersion: "6.12", SizeBytes: 2 * 1024 * 1024, Pulled: true}, {Name: "new-kernel", SizeBytes: 0, Pulled: false}, } if err := printKernelCatalogTable(&buf, entries); err != nil { t.Fatalf("printKernelCatalogTable: %v", err) } got := buf.String() for _, want := range []string{"generic-6.12", "pulled", "available", "new-kernel"} { if !strings.Contains(got, want) { t.Errorf("output missing %q:\n%s", want, got) } } if !strings.Contains(got, "2.0MiB") { t.Errorf("expected humanSize(2 MiB), got:\n%s", got) } } func TestPrintGuestSessionTable(t *testing.T) { var buf bytes.Buffer sessions := []model.GuestSession{ {ID: "abcdef0123456789", Name: "planner", Status: "running", Command: "pi", CWD: "/root/repo", Attachable: true}, {ID: "short", Name: "once", Status: "exited", Command: "true", CWD: "/tmp", Attachable: false}, } if err := printGuestSessionTable(&buf, sessions); err != nil { t.Fatalf("printGuestSessionTable: %v", err) } got := buf.String() for _, want := range []string{"ID", "NAME", "planner", "once", "yes", "no", "pi"} { if !strings.Contains(got, want) { t.Errorf("output missing %q:\n%s", want, got) } } } func TestPrintGuestSessionSummary(t *testing.T) { var buf bytes.Buffer session := model.GuestSession{ ID: "id1", Name: "s", Status: "exited", Command: "true", CWD: "/root", } if err := printGuestSessionSummary(&buf, session); err != nil { t.Fatalf("printGuestSessionSummary: %v", err) } got := buf.String() fields := strings.Split(strings.TrimRight(got, "\n"), "\t") if len(fields) != 5 { t.Fatalf("expected 5 tab-separated fields, got %d: %q", len(fields), got) } } func TestPrintJSON(t *testing.T) { var buf bytes.Buffer if err := printJSON(&buf, map[string]int{"a": 1, "b": 2}); err != nil { t.Fatalf("printJSON: %v", err) } got := buf.String() if !strings.Contains(got, `"a": 1`) || !strings.Contains(got, `"b": 2`) { t.Errorf("unexpected JSON output:\n%s", got) } if !strings.HasSuffix(got, "\n") { t.Error("printJSON should terminate with newline") } } func TestPrintJSONUnmarshalableValue(t *testing.T) { var buf bytes.Buffer // Channels are not JSON-marshalable. err := printJSON(&buf, make(chan int)) if err == nil { t.Fatal("expected error for unmarshalable value") } } func TestPrintVMSummary(t *testing.T) { var buf bytes.Buffer vm := model.VMRecord{ ID: "0123456789abcdef", Name: "demo", State: model.VMStateRunning, } vm.Runtime.GuestIP = "172.16.0.5" vm.Runtime.DNSName = "demo.vm" vm.Spec.WorkDiskSizeBytes = 0 if err := printVMSummary(&buf, vm); err != nil { t.Fatalf("printVMSummary: %v", err) } got := buf.String() for _, want := range []string{"0123456789ab", "demo", "172.16.0.5", "demo.vm"} { if !strings.Contains(got, want) { t.Errorf("summary missing %q:\n%s", want, got) } } } func TestPrintImageSummary(t *testing.T) { var buf bytes.Buffer img := model.Image{ID: "img-id", Name: "debian-bookworm", Managed: true, RootfsPath: "/var/rootfs.ext4"} if err := printImageSummary(&buf, img); err != nil { t.Fatalf("printImageSummary: %v", err) } got := buf.String() for _, want := range []string{"debian-bookworm", "true", "/var/rootfs.ext4"} { if !strings.Contains(got, want) { t.Errorf("summary missing %q:\n%s", want, got) } } } func TestVMImageLabel(t *testing.T) { names := map[string]string{"img-1": "debian"} if got := vmImageLabel("img-1", names); got != "debian" { t.Errorf("got %q, want debian", got) } if got := vmImageLabel("img-2", names); got != "img-2" { t.Errorf("fallback: got %q, want img-2", got) } } // failWriter lets us exercise io-error branches of the printers. type failWriter struct{} func (failWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("boom") } func TestPrintersPropagateWriteErrors(t *testing.T) { sessions := []model.GuestSession{{ID: "id", Name: "n"}} if err := printGuestSessionTable(failWriter{}, sessions); err == nil { t.Error("expected write error from printGuestSessionTable") } kernels := []api.KernelEntry{{Name: "k"}} if err := printKernelListTable(failWriter{}, kernels); err == nil { t.Error("expected write error from printKernelListTable") } catalog := []api.KernelCatalogEntry{{Name: "k"}} if err := printKernelCatalogTable(failWriter{}, catalog); err == nil { t.Error("expected write error from printKernelCatalogTable") } }