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.