all repos — llm_aggregator @ e6f508220e5dbe8a9333b4ad603b5add4b13af7b

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