internal/cli/help_test.go (view raw)
1package cli
2
3import (
4 "os"
5 "strings"
6 "testing"
7
8 "codeberg.org/maxwelljensen/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.