diff --git a/cmd/wfctl/plugin_install.go b/cmd/wfctl/plugin_install.go index d1c173eb..e68d5fef 100644 --- a/cmd/wfctl/plugin_install.go +++ b/cmd/wfctl/plugin_install.go @@ -56,8 +56,17 @@ func runPluginSearch(args []string) error { fmt.Println("No plugins found.") return nil } - fmt.Printf("%-20s %-10s %-12s %-14s %-12s %s\n", "NAME", "VERSION", "TIER", "STATUS", "SOURCE", "DESCRIPTION") - fmt.Printf("%-20s %-10s %-12s %-14s %-12s %s\n", "----", "-------", "----", "------", "------", "-----------") + fmt.Print(formatPluginSearchResults(plugins)) + return nil +} + +// formatPluginSearchResults renders the wfctl plugin search table as a string. +// Extracted from runPluginSearch so unit tests can exercise the formatter +// without capturing stdout. +func formatPluginSearchResults(plugins []PluginSearchResult) string { + var b strings.Builder + fmt.Fprintf(&b, "%-20s %-10s %-12s %-14s %-12s %s\n", "NAME", "VERSION", "TIER", "STATUS", "SOURCE", "DESCRIPTION") + fmt.Fprintf(&b, "%-20s %-10s %-12s %-14s %-12s %s\n", "----", "-------", "----", "------", "------", "-----------") for _, p := range plugins { desc := p.Description if len(desc) > 50 { @@ -67,9 +76,9 @@ func runPluginSearch(args []string) error { if status == "" { status = "-" } - fmt.Printf("%-20s %-10s %-12s %-14s %-12s %s\n", p.Name, p.Version, p.Tier, status, p.Source, desc) + fmt.Fprintf(&b, "%-20s %-10s %-12s %-14s %-12s %s\n", p.Name, p.Version, p.Tier, status, p.Source, desc) } - return nil + return b.String() } func runPluginInstall(args []string) error { diff --git a/cmd/wfctl/plugin_search_format_test.go b/cmd/wfctl/plugin_search_format_test.go new file mode 100644 index 00000000..5c7ff086 --- /dev/null +++ b/cmd/wfctl/plugin_search_format_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "strings" + "testing" +) + +func TestFormatPluginSearchResults_HeaderColumns(t *testing.T) { + out := formatPluginSearchResults(nil) + for _, want := range []string{"NAME", "VERSION", "TIER", "STATUS", "SOURCE", "DESCRIPTION"} { + if !strings.Contains(out, want) { + t.Errorf("header missing %q in output:\n%s", want, out) + } + } +} + +func TestFormatPluginSearchResults_StatusFallback(t *testing.T) { + rows := []PluginSearchResult{ + {PluginSummary: PluginSummary{Name: "a", Version: "v1", Tier: "core", Status: "", Description: "no status"}, Source: "src"}, + {PluginSummary: PluginSummary{Name: "b", Version: "v2", Tier: "community", Status: "verified", Description: "with status"}, Source: "src"}, + } + out := formatPluginSearchResults(rows) + if !strings.Contains(out, " - ") { + t.Errorf("expected '-' fallback for empty Status, got:\n%s", out) + } + if !strings.Contains(out, "verified") { + t.Errorf("expected 'verified' in output, got:\n%s", out) + } +} + +func TestFormatPluginSearchResults_DescriptionTruncation(t *testing.T) { + long := strings.Repeat("x", 80) + rows := []PluginSearchResult{{ + PluginSummary: PluginSummary{Name: "p", Version: "v", Tier: "core", Status: "verified", Description: long}, + Source: "src", + }} + out := formatPluginSearchResults(rows) + if !strings.Contains(out, "...") { + t.Errorf("expected truncated description ending with '...', got:\n%s", out) + } + if strings.Contains(out, long) { + t.Errorf("expected description to be truncated, but full string present:\n%s", out) + } +} + +func TestFormatPluginSearchResults_OrderAndFields(t *testing.T) { + rows := []PluginSearchResult{ + {PluginSummary: PluginSummary{Name: "first-plugin", Version: "1.0.0", Tier: "core", Status: "verified", Description: "first"}, Source: "main"}, + {PluginSummary: PluginSummary{Name: "second-plugin", Version: "2.0.0", Tier: "community", Status: "experimental", Description: "second"}, Source: "custom"}, + } + out := formatPluginSearchResults(rows) + firstIdx := strings.Index(out, "first-plugin") + secondIdx := strings.Index(out, "second-plugin") + if firstIdx < 0 || secondIdx < 0 { + t.Fatalf("expected both rows in output, got:\n%s", out) + } + if firstIdx >= secondIdx { + t.Errorf("expected first-plugin before second-plugin, got firstIdx=%d secondIdx=%d", firstIdx, secondIdx) + } + for _, want := range []string{"first-plugin", "1.0.0", "core", "verified", "main", "first", + "second-plugin", "2.0.0", "community", "experimental", "custom", "second"} { + if !strings.Contains(out, want) { + t.Errorf("expected %q in output, missing. Output:\n%s", want, out) + } + } +}