banger/internal/cli/formatters_test.go
Thales Maciel 2b6437d1b4
remove vm session feature
Cuts the daemon-managed guest-session machinery (start/list/show/
logs/stop/kill/attach/send). The feature shipped aimed at agent-
orchestration workflows (programmatic stdin piping into a long-lived
guest process) that aren't driving any concrete user today, and the
~2.3K LOC of daemon surface area — attach bridge, FIFO keepalive,
controller registry, sessionstream framing, SQLite persistence — was
locking in an API we'd have to keep through v0.1.0.

Anything session-flavoured that people actually need today can be
done with `vm ssh + tmux` or `vm run -- cmd`.

Deleted:
- internal/cli/commands_vm_session.go
- internal/daemon/{guest_sessions,session_lifecycle,session_attach,session_stream,session_controller}.go
- internal/daemon/session/ (guest-session helpers package)
- internal/sessionstream/ (framing package)
- internal/daemon/guest_sessions_test.go
- internal/store/guest_session_test.go
- GuestSession* types from internal/{api,model}
- Store UpsertGuestSession/GetGuestSession/ListGuestSessionsByVM/DeleteGuestSession + scanner helpers
- guest.session.* RPC dispatch entries
- 5 CLI session tests, 2 completion tests, 2 printer tests

Extracted:
- ShellQuote + FormatStepError lifted to internal/daemon/workspace/util.go
  (only non-session consumer); workspace package now self-contained
- internal/daemon/guest_ssh.go keeps guestSSHClient + dialGuest +
  waitForGuestSSH — still used by workspace prepare/export
- internal/daemon/fake_firecracker_test.go preserves the test helper
  that used to live in guest_sessions_test.go

Store schema: CREATE TABLE guest_sessions and its column migrations
removed. Existing dev DBs keep an orphan table (harmless, pre-v0.1.0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:47:58 -03:00

287 lines
7.4 KiB
Go

package cli
import (
"bytes"
"errors"
"fmt"
"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 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 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) {
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")
}
}