all repos — llm_aggregator @ e3e7be8461bb50a7fe674b4055286d34dddb0c5c

A CLI tool to aggregate RSS feeds and summarise them with LLMs

internal/cli/help_test.go (view raw)

  1package cli
  2
  3import (
  4	"os"
  5	"strings"
  6	"testing"
  7
  8	"llm_aggregator/internal/style"
  9)
 10
 11func TestShouldStyle(t *testing.T) {
 12	tests := []struct {
 13		name       string
 14		noColorVal string
 15		want       bool
 16	}{
 17		{"color enabled", "", true},
 18		{"color disabled via NO_COLOR", "1", false},
 19		{"color disabled via arbitrary value", "true", false},
 20	}
 21
 22	for _, tt := range tests {
 23		t.Run(tt.name, func(t *testing.T) {
 24			t.Setenv("NO_COLOR", tt.noColorVal)
 25
 26			got := shouldStyle()
 27			if got != tt.want {
 28				t.Errorf("shouldStyle() = %v, want %v", got, tt.want)
 29			}
 30		})
 31	}
 32}
 33
 34func TestRenderFlag(t *testing.T) {
 35	tests := []struct {
 36		name  string
 37		short string
 38		long  string
 39		want  string
 40	}{
 41		{
 42			name:  "with short and long flag",
 43			short: "-f",
 44			long:  "--feeds-file",
 45			want:  "-f, --feeds-file",
 46		},
 47		{
 48			name:  "long flag only",
 49			short: "",
 50			long:  "--version",
 51			want:  "--version",
 52		},
 53		{
 54			name:  "short and long with multiple dashes",
 55			short: "-n",
 56			long:  "--max-articles-per-feed",
 57			want:  "-n, --max-articles-per-feed",
 58		},
 59	}
 60
 61	for _, tt := range tests {
 62		t.Run(tt.name, func(t *testing.T) {
 63			got := renderFlag(tt.short, tt.long)
 64			if got != tt.want {
 65				t.Errorf("renderFlag(%q, %q) = %q, want %q", tt.short, tt.long, got, tt.want)
 66			}
 67		})
 68	}
 69}
 70
 71func TestGetOptionValue(t *testing.T) {
 72	tests := []struct {
 73		name string
 74		opt  HelpOption
 75		want string
 76	}{
 77		{
 78			name: "type with default",
 79			opt:  HelpOption{Type: "int", Default: "10"},
 80			want: "int (default: 10)",
 81		},
 82		{
 83			name: "type without default",
 84			opt:  HelpOption{Type: "string"},
 85			want: "string",
 86		},
 87		{
 88			name: "default without type",
 89			opt:  HelpOption{Default: "deepseek-chat"},
 90			want: "deepseek-chat",
 91		},
 92		{
 93			name: "neither type nor default",
 94			opt:  HelpOption{},
 95			want: "",
 96		},
 97		{
 98			name: "choice type with default",
 99			opt:  HelpOption{Type: "text|markdown|json", Default: "text"},
100			want: "text|markdown|json (default: text)",
101		},
102	}
103
104	for _, tt := range tests {
105		t.Run(tt.name, func(t *testing.T) {
106			got := getOptionValue(tt.opt)
107			if got != tt.want {
108				t.Errorf("getOptionValue() = %q, want %q", got, tt.want)
109			}
110		})
111	}
112}
113
114func TestMaxFlagWidth(t *testing.T) {
115	t.Setenv("NO_COLOR", "1")
116
117	sections := []HelpSection{
118		{
119			Title: "Required",
120			Options: []HelpOption{
121				{Short: "-f", Name: "--feeds-file"},
122				{Short: "-p", Name: "--prompt"},
123			},
124		},
125		{
126			Title: "Optional",
127			Options: []HelpOption{
128				{Name: "--max-articles-per-feed"},
129				{Name: "--max-total-articles"},
130			},
131		},
132	}
133
134	got := maxFlagWidth(sections)
135	// "--max-articles-per-feed" is 23 chars
136	if got != 23 {
137		t.Errorf("maxFlagWidth() = %d, want 24", got)
138	}
139}
140
141func TestFormatSection(t *testing.T) {
142	// Test with NO_COLOR to avoid lipgloss dependency in assertions
143	t.Setenv("NO_COLOR", "1")
144
145	section := HelpSection{
146		Title: "Test Section",
147		Options: []HelpOption{
148			{
149				Short:       "-f",
150				Name:        "--feeds-file",
151				Description: "Path to feeds file",
152				Default:     "feeds.txt",
153				Required:    true,
154			},
155			{
156				Short:       "-v",
157				Name:        "--verbose",
158				Description: "Enable verbose output",
159			},
160		},
161	}
162
163	output := formatSection(section, 16, 0)
164
165	if !strings.Contains(output, "Test Section") {
166		t.Error("formatSection missing section title")
167	}
168	if !strings.Contains(output, "-f, --feeds-file") {
169		t.Error("formatSection missing short+long flag")
170	}
171	if !strings.Contains(output, "feeds.txt") {
172		t.Error("formatSection missing default value")
173	}
174	if !strings.Contains(output, "[required]") {
175		t.Error("formatSection missing required marker")
176	}
177	if !strings.Contains(output, "Path to feeds file") {
178		t.Error("formatSection missing description")
179	}
180	if !strings.Contains(output, "--verbose") {
181		t.Error("formatSection missing long-only flag")
182	}
183}
184
185func TestFormatSectionWithStyling(t *testing.T) {
186	// Ensure NO_COLOR is not set for this test
187	_ = os.Unsetenv("NO_COLOR")
188
189	// Suppress the lipgloss warning during test
190	_ = style.NoColor
191
192	section := HelpSection{
193		Title: "Styled Section",
194		Options: []HelpOption{
195			{
196				Short:       "-o",
197				Name:        "--output",
198				Description: "Output format",
199				Type:        "text|json|markdown",
200				Default:     "text",
201			},
202		},
203	}
204
205	output := formatSection(section, 20, 0)
206
207	if !strings.Contains(output, "Styled Section") {
208		t.Error("formatSection missing section title")
209	}
210	if !strings.Contains(output, "text|json|markdown") {
211		t.Error("formatSection missing type with default")
212	}
213}
214
215func TestBuildStyledHelp(t *testing.T) {
216	// Test without color
217	t.Setenv("NO_COLOR", "1")
218
219	args := &Args{FeedsFile: "/tmp/feeds.txt", Prompt: "Summarise"}
220	output := BuildStyledHelp(args)
221
222	if !strings.Contains(output, "llm_aggregator") {
223		t.Error("BuildStyledHelp missing program name")
224	}
225	if !strings.Contains(output, "Required Arguments") {
226		t.Error("BuildStyledHelp missing Required Arguments section")
227	}
228	if !strings.Contains(output, "--feeds-file") {
229		t.Error("BuildStyledHelp missing feeds-file flag")
230	}
231	if !strings.Contains(output, "--prompt") {
232		t.Error("BuildStyledHelp missing prompt flag")
233	}
234	if !strings.Contains(output, "Examples:") {
235		t.Error("BuildStyledHelp missing Examples section")
236	}
237}
238
239func TestBuildStyledHelpWithColor(t *testing.T) {
240	_ = os.Unsetenv("NO_COLOR")
241
242	args := &Args{FeedsFile: "/tmp/feeds.txt", Prompt: "Summarise"}
243	output := BuildStyledHelp(args)
244
245	// Should contain all expected sections even with styling
246	if !strings.Contains(output, "Required Arguments") {
247		t.Error("BuildStyledHelp missing Required Arguments")
248	}
249	if !strings.Contains(output, "LLM API Options") {
250		t.Error("BuildStyledHelp missing LLM API Options")
251	}
252	if !strings.Contains(output, "Output Options") {
253		t.Error("BuildStyledHelp missing Output Options")
254	}
255}
256
257func TestWriteHelp(t *testing.T) {
258	args := &Args{FeedsFile: "/tmp/feeds.txt", Prompt: "Summarise"}
259
260	t.Setenv("NO_COLOR", "1")
261
262	tmpFile, err := os.CreateTemp(t.TempDir(), "help_test_*.txt")
263	if err != nil {
264		t.Fatalf("Failed to create temp file: %v", err)
265	}
266	defer func() { _ = os.Remove(tmpFile.Name()) }() //nolint:errcheck
267	_ = tmpFile.Close()                               //nolint:errcheck
268
269	w, err := os.OpenFile(tmpFile.Name(), os.O_WRONLY, 0644)
270	if err != nil {
271		t.Fatalf("Failed to open temp file for write: %v", err)
272	}
273	defer func() { _ = w.Close() }() //nolint:errcheck
274
275	WriteHelp(args, w)
276
277	content, err := os.ReadFile(tmpFile.Name())
278	if err != nil {
279		t.Fatalf("Failed to read temp file: %v", err)
280	}
281
282	if !strings.Contains(string(content), "llm_aggregator") {
283		t.Error("WriteHelp did not write expected content")
284	}
285}
286
287// Note: ParseArgs help/version branches (lines 86-95) call os.Exit
288// and cannot be tested without refactoring to use an interface or
289// callback for exit behavior.