Make TUI startup render immediately
The TUI felt hung because banger tui blocked on ensureDaemon before Bubble Tea started, then treated the initial vm and image fetch as one combined loading gate. Start the program first, move daemon bootstrap into staged TUI commands, render the full layout with inline loading placeholders, and split vm list and image list startup so VMs can appear before images finish loading. Also record per-stage timings so the slow step is visible in the status line instead of hidden behind a generic loading state. Verified with go test ./internal/cli, go test ./..., and make build.
This commit is contained in:
parent
8ba920eda6
commit
3a92362829
2 changed files with 467 additions and 54 deletions
|
|
@ -1,9 +1,15 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func TestCreateVMFormSubmit(t *testing.T) {
|
||||
|
|
@ -87,3 +93,143 @@ func TestResolveSelectedID(t *testing.T) {
|
|||
t.Fatalf("resolveSelectedID empty = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTUICommandStartsProgramWithoutEnsuringDaemon(t *testing.T) {
|
||||
origEnsure := tuiEnsureDaemonFunc
|
||||
origRunner := tuiProgramRunner
|
||||
origTerminal := tuiIsTerminal
|
||||
t.Cleanup(func() {
|
||||
tuiEnsureDaemonFunc = origEnsure
|
||||
tuiProgramRunner = origRunner
|
||||
tuiIsTerminal = origTerminal
|
||||
})
|
||||
|
||||
ensureCalled := false
|
||||
tuiEnsureDaemonFunc = func(ctx context.Context) (paths.Layout, model.DaemonConfig, error) {
|
||||
ensureCalled = true
|
||||
return paths.Layout{}, model.DaemonConfig{}, nil
|
||||
}
|
||||
tuiProgramRunner = func(model tuiModel) error {
|
||||
if ensureCalled {
|
||||
t.Fatal("ensureDaemon should not run before the TUI starts")
|
||||
}
|
||||
if !model.daemonPending || !model.loading {
|
||||
t.Fatalf("startup model = %+v, want pending daemon startup", model)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
tuiIsTerminal = func(fd uintptr) bool { return true }
|
||||
|
||||
cmd := NewBangerCommand()
|
||||
cmd.SetArgs([]string{"tui"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if ensureCalled {
|
||||
t.Fatal("ensureDaemon should not have been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIViewRendersLayoutImmediately(t *testing.T) {
|
||||
m := newTUIModel(paths.Layout{}, model.DaemonConfig{})
|
||||
view := m.View()
|
||||
if strings.Contains(view, "Loading...") {
|
||||
t.Fatalf("view = %q, want full layout instead of one-line loading", view)
|
||||
}
|
||||
if !strings.Contains(view, "Starting daemon") {
|
||||
t.Fatalf("view = %q, want startup placeholder", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIVMLoadCanCompleteBeforeImages(t *testing.T) {
|
||||
now := time.Date(2026, time.March, 18, 12, 0, 0, 0, time.UTC)
|
||||
initial := newTUIModel(paths.Layout{}, model.DaemonConfig{})
|
||||
|
||||
updated, _ := initial.Update(daemonReadyMsg{
|
||||
generation: initial.loadGeneration,
|
||||
layout: paths.Layout{SocketPath: "/tmp/bangerd.sock"},
|
||||
cfg: model.DaemonConfig{DefaultImageName: "default"},
|
||||
duration: 2400 * time.Millisecond,
|
||||
})
|
||||
m := updated.(tuiModel)
|
||||
if !m.daemonReady || !m.vmListPending || !m.imagePending {
|
||||
t.Fatalf("model after daemonReady = %+v, want pending vm/image loads", m)
|
||||
}
|
||||
|
||||
vm := model.VMRecord{
|
||||
ID: "vm-1",
|
||||
Name: "devbox",
|
||||
State: model.VMStateRunning,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
LastTouchedAt: now,
|
||||
Spec: model.VMSpec{
|
||||
VCPUCount: 2,
|
||||
MemoryMiB: 1024,
|
||||
WorkDiskSizeBytes: 16 * 1024 * 1024 * 1024,
|
||||
},
|
||||
Runtime: model.VMRuntime{
|
||||
GuestIP: "172.16.0.2",
|
||||
DNSName: "devbox.vm",
|
||||
},
|
||||
}
|
||||
updated, _ = m.Update(vmListLoadedMsg{
|
||||
generation: m.loadGeneration,
|
||||
vms: []model.VMRecord{vm},
|
||||
duration: 20 * time.Millisecond,
|
||||
})
|
||||
m = updated.(tuiModel)
|
||||
if len(m.vms) != 1 || m.selectedID != vm.ID {
|
||||
t.Fatalf("model after vmListLoaded = %+v, want selected vm", m)
|
||||
}
|
||||
if !m.imagePending {
|
||||
t.Fatalf("image load should still be pending: %+v", m)
|
||||
}
|
||||
if strings.Contains(m.View(), "No VMs") {
|
||||
t.Fatalf("view should render the loaded VM while images are pending: %q", m.View())
|
||||
}
|
||||
if !strings.Contains(m.View(), "devbox") {
|
||||
t.Fatalf("view = %q, want loaded VM name", m.View())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUICreateBlockedWhileImagesLoad(t *testing.T) {
|
||||
m := newTUIModel(paths.Layout{}, model.DaemonConfig{})
|
||||
m.daemonPending = false
|
||||
m.daemonReady = true
|
||||
m.imagePending = true
|
||||
m.loading = true
|
||||
|
||||
updated, _ := m.updateBrowse(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}})
|
||||
if updated.mode != tuiModeBrowse {
|
||||
t.Fatalf("mode = %v, want browse", updated.mode)
|
||||
}
|
||||
if updated.statusText != "Images are still loading" {
|
||||
t.Fatalf("status = %q, want image loading warning", updated.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIStatusIncludesStageDurationsAfterInitialLoad(t *testing.T) {
|
||||
initial := newTUIModel(paths.Layout{}, model.DaemonConfig{})
|
||||
updated, _ := initial.Update(daemonReadyMsg{
|
||||
generation: initial.loadGeneration,
|
||||
layout: paths.Layout{SocketPath: "/tmp/bangerd.sock"},
|
||||
duration: 2400 * time.Millisecond,
|
||||
})
|
||||
m := updated.(tuiModel)
|
||||
updated, _ = m.Update(vmListLoadedMsg{
|
||||
generation: m.loadGeneration,
|
||||
vms: []model.VMRecord{},
|
||||
duration: 20 * time.Millisecond,
|
||||
})
|
||||
m = updated.(tuiModel)
|
||||
updated, _ = m.Update(imageListLoadedMsg{
|
||||
generation: m.loadGeneration,
|
||||
images: []model.Image{{Name: "default"}},
|
||||
duration: 15 * time.Millisecond,
|
||||
})
|
||||
m = updated.(tuiModel)
|
||||
if !strings.Contains(m.statusText, "daemon 2.4s") || !strings.Contains(m.statusText, "vm list 20ms") || !strings.Contains(m.statusText, "image list 15ms") {
|
||||
t.Fatalf("statusText = %q, want stage timings", m.statusText)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue