mydev/cmd/dash_render_test.go
Victor Abrell 47bb2d3a69 Use Gitea module path, add MIT license
Set module to git.abrell.se/victor/mydev so it installs from the
private Gitea host, and add an MIT LICENSE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:41:20 +02:00

276 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cmd
import (
"context"
"strings"
"testing"
"time"
"git.abrell.se/victor/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")
}
}