package cmd import ( "context" "strings" "testing" "time" "mydev/internal/checks" tea "github.com/charmbracelet/bubbletea" ) func TestDashViewRenders(t *testing.T) { m := newDash([]string{"/tmp"}, "/x/awsmfa", "/x/docker-compose.yml", 10*time.Second) m.auth = checks.AuthStatus{Authed: true, Account: "123", Detail: "account 123 · expires 21:25"} m.docker = dockerMsg{list: []checks.Container{{Name: "db", State: "running", Status: "Up 1h"}}} m.drift = driftMsg{stale: 2, total: 13} m.repos = repoMsg{list: []checks.Repo{{Name: "legacy", Branch: "main", Dirty: true}}} out := m.View() for _, want := range []string{"MyDev", "AWS AUTH", "DOCKER", "2/13 stale", "REPOS", "legacy"} { if !strings.Contains(out, want) { t.Errorf("View() missing %q\n---\n%s", want, out) } } // Default footer shows the key legend. if !strings.Contains(out, "[R]estart") { t.Errorf("footer missing key legend\n---\n%s", out) } // With a pending action, the footer becomes a confirm prompt instead. m.pending = &pendingAction{label: "dev restart", args: []string{"dev", "restart"}} confirm := m.View() if !strings.Contains(confirm, "Run 'dev restart'?") || !strings.Contains(confirm, "[y] yes") { t.Errorf("confirm prompt not rendered\n---\n%s", confirm) } } func names(items []suggestion) string { var b strings.Builder for _, it := range items { b.WriteString(it.name + " ") } return b.String() } func TestCompletions(t *testing.T) { // Empty input → root commands, excluding dash/help/completion. label, items, leaf := completions("") if leaf { t.Fatal("root should not be a leaf") } got := names(items) for _, want := range []string{"dev", "db", "data", "port"} { if !strings.Contains(got, want) { t.Errorf("root completions missing %q (got: %s)", want, got) } } // Hidden at root: status + bare auth (dashboard-covered), and dash/help/completion. for _, no := range []string{"status", "auth", "dash", "help", "completion"} { if strings.Contains(got, no) { t.Errorf("root completions should not include %q (got: %s)", no, got) } } // But `auth login` is still reachable by typing `auth `. if !strings.Contains(names(mustItems(t, "auth ")), "login") { t.Error("auth login should still be reachable") } // Prefix filter. if got := names(mustItems(t, "d")); !strings.Contains(got, "dev") || !strings.Contains(got, "docker") || strings.Contains(got, "auth") { t.Errorf("prefix 'd' filter wrong: %s", got) } // Descend into a subcommand group. if got := names(mustItems(t, "dev ")); !strings.Contains(got, "start") || !strings.Contains(got, "restart") || !strings.Contains(got, "status") { t.Errorf("'dev ' subcommands wrong: %s", got) } // Leaf command shows usage. _, items, leaf = completions("port ") if !leaf || len(items) == 0 || !strings.Contains(items[0].name, "port") { t.Errorf("'port ' should be a leaf with usage, got leaf=%v items=%v", leaf, items) } _ = label } func mustItems(t *testing.T, in string) []suggestion { t.Helper() _, items, _ := completions(in) return items } func TestInteractiveClassification(t *testing.T) { cases := []struct { args []string want bool }{ {[]string{"auth", "login"}, true}, {[]string{"dev", "start"}, true}, {[]string{"dev", "restart"}, true}, {[]string{"dev", "stop"}, false}, // quick, capture output {[]string{"dev", "status"}, false}, // read-only {[]string{"data", "table", "x"}, true}, // may prompt / stream {[]string{"data", "snapshot"}, true}, {[]string{"db", "grant", "1234"}, false}, // captured → result pane {[]string{"port", "3306"}, false}, {[]string{"auth"}, false}, } for _, c := range cases { if got := interactive(c.args); got != c.want { t.Errorf("interactive(%v) = %v, want %v", c.args, got, c.want) } } } func TestDashboardCovered(t *testing.T) { cases := []struct { args []string want bool }{ {[]string{"status"}, true}, {[]string{"auth"}, true}, // bare auth = status {[]string{"auth", "login"}, false}, // login is an action {[]string{"docker"}, false}, // kept (more detail than panel) {[]string{"dev", "status"}, false}, // kept (per-service detail) {[]string{"port", "3306"}, false}, } for _, c := range cases { if got := dashboardCovered(c.args); got != c.want { t.Errorf("dashboardCovered(%v) = %v, want %v", c.args, got, c.want) } } } func TestPaletteRefusesCoveredCommand(t *testing.T) { m := newDash(nil, "/x/awsmfa", "", 10*time.Second) um, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{':'}}) m = um.(dashModel) // Type "status" char by char. for _, r := range "status" { um, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}}) m = um.(dashModel) } um, _ = m.Update(tea.KeyMsg{Type: tea.KeyEnter}) m = um.(dashModel) if !m.cmdActive { t.Error("palette should stay open after refusing a covered command") } if m.running != "" || m.result != nil { t.Error("covered command should not run") } if !strings.Contains(m.View(), "already shown on the dashboard") { t.Errorf("expected refusal note\n---\n%s", m.View()) } } func TestLaunchSetsRunningForCaptured(t *testing.T) { m := newDash(nil, "/x/awsmfa", "", 10*time.Second) m2, cmd := m.launch([]string{"status"}) if m2.running != "status" { t.Errorf("captured launch should set running=status, got %q", m2.running) } if cmd == nil { t.Error("launch returned nil command") } // Interactive launch should NOT set running (it suspends the TUI). m3, _ := m.launch([]string{"auth", "login"}) if m3.running != "" { t.Errorf("interactive launch should not set running, got %q", m3.running) } } func TestWrapTextBreaksLongTokens(t *testing.T) { // A generated password has no spaces — must still be hard-wrapped. pw := "temp password: " + strings.Repeat("Xy9$", 30) // ~135 chars, mostly one token const w = 40 out := wrapText(pw, w) for _, line := range strings.Split(out, "\n") { if len(line) > w { t.Errorf("line exceeds width %d: %q (len %d)", w, line, len(line)) } } // Content is preserved (ignoring inserted newlines). if strings.ReplaceAll(out, "\n", "") != strings.ReplaceAll(pw, " ", " ") && !strings.Contains(strings.ReplaceAll(out, "\n", ""), strings.Repeat("Xy9$", 30)) { t.Errorf("wrap lost content:\n%s", out) } } func TestRunningBlocksLaunchAndCancels(t *testing.T) { m := newDash(nil, "/x/awsmfa", "", 10*time.Second) m, _ = m.launch([]string{"status"}) if m.running != "status" || m.cancel == nil { t.Fatalf("captured launch should set running+cancel, got running=%q cancel=%v", m.running, m.cancel != nil) } // Footer shows the running state with a cancel hint. if !strings.Contains(m.View(), "[c]ancel") { t.Errorf("footer missing cancel hint\n---\n%s", m.View()) } // ":" must NOT open the palette while a command runs. um, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{':'}}) m = um.(dashModel) if m.cmdActive { t.Error("palette opened while a command was running") } // "c" cancels. um, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}}) m = um.(dashModel) if !m.cancelled { t.Error("'c' did not mark the command cancelled") } // The result message clears the running state and returns to the menu — // a cancelled command shows NO result pane. um, _ = m.Update(cmdResultMsg{title: "status", body: "partial", err: context.Canceled}) m = um.(dashModel) if m.running != "" || m.cancel != nil { t.Error("running state not cleared after result") } if m.result != nil { t.Errorf("cancelled command should not open a result pane, got %+v", m.result) } if m.cancelled { t.Error("cancelled flag not reset") } } func TestResultPaneRenders(t *testing.T) { m := newDash(nil, "/x/awsmfa", "", 10*time.Second) sized, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 24}) m = sized.(dashModel) updated, _ := m.Update(cmdResultMsg{title: "db list", body: "tenant-a\ntenant-b", err: nil}) m = updated.(dashModel) if m.result == nil { t.Fatal("cmdResultMsg did not open the result pane") } out := m.View() if !strings.Contains(out, "mydev db list") || !strings.Contains(out, "tenant-a") { t.Errorf("result pane missing title/body\n---\n%s", out) } // esc closes it. updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyEsc}) if updated.(dashModel).result != nil { t.Error("esc did not close the result pane") } } func TestDashCommandPalette(t *testing.T) { m := newDash([]string{"/tmp"}, "/x/awsmfa", "/x/docker-compose.yml", 10*time.Second) // ":" opens the palette. updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{':'}}) m = updated.(dashModel) if !m.cmdActive { t.Fatal("':' did not open the command palette") } if !strings.Contains(m.View(), "mydev ❯") { t.Errorf("palette prompt not shown\n---\n%s", m.View()) } // esc closes it without running anything. updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyEsc}) m = updated.(dashModel) if m.cmdActive { t.Error("esc did not close the palette") } }