all repos — llm_aggregator @ 95ee75b064bd241201f71304ee65f4393292f081

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

internal/output/formatter_test.go (view raw)

  1package output
  2
  3import (
  4	"encoding/json"
  5	"strings"
  6	"testing"
  7	"time"
  8)
  9
 10func TestNewFormatter(t *testing.T) {
 11	tests := []struct {
 12		name      string
 13		format    string
 14		wantErr   bool
 15		wantNil   bool
 16	}{
 17		{"text format", "text", false, false},
 18		{"json format", "json", false, false},
 19		{"markdown format", "markdown", false, false},
 20		{"uppercase text", "TEXT", false, false},
 21		{"mixed case json", "Json", false, false},
 22		{"unsupported format", "xml", true, true},
 23		{"empty format", "", true, true},
 24		{"unknown format", "csv", true, true},
 25	}
 26
 27	for _, tt := range tests {
 28		t.Run(tt.name, func(t *testing.T) {
 29			formatter, err := NewFormatter(tt.format)
 30
 31			if tt.wantErr && err == nil {
 32				t.Error("Expected error, got nil")
 33			}
 34			if !tt.wantErr && err != nil {
 35				t.Errorf("Unexpected error: %v", err)
 36			}
 37			if tt.wantNil && formatter != nil {
 38				t.Error("Expected nil formatter")
 39			}
 40			if !tt.wantNil && formatter == nil {
 41				t.Error("Expected non-nil formatter")
 42			}
 43		})
 44	}
 45}
 46
 47func TestFormatDataJSON(t *testing.T) {
 48	formatter, err := NewFormatter("json")
 49	if err != nil {
 50		t.Fatalf("Failed to create formatter: %v", err)
 51	}
 52
 53	data := map[string]any{
 54		"title":          "Test Summary",
 55		"prompt":         "Summarise the news",
 56		"model":          "deepseek-chat",
 57		"articles_count": 5,
 58		"timestamp":      "2024-01-15T10:00:00Z",
 59		"summary":        "This is a test summary.",
 60	}
 61
 62	output, err := formatter.FormatData(data)
 63	if err != nil {
 64		t.Errorf("FormatData failed: %v", err)
 65	}
 66
 67	// Verify it's valid JSON
 68	var parsed map[string]any
 69	if err := json.Unmarshal([]byte(output), &parsed); err != nil {
 70		t.Errorf("Output is not valid JSON: %v", err)
 71	}
 72
 73	// Verify expected fields
 74	if parsed["title"] != "Test Summary" {
 75		t.Errorf("Expected title 'Test Summary', got %v", parsed["title"])
 76	}
 77	if parsed["summary"] != "This is a test summary." {
 78		t.Errorf("Expected summary 'This is a test summary.', got %v", parsed["summary"])
 79	}
 80}
 81
 82func TestFormatDataMarkdown(t *testing.T) {
 83	formatter, err := NewFormatter("markdown")
 84	if err != nil {
 85		t.Fatalf("Failed to create formatter: %v", err)
 86	}
 87
 88	data := map[string]any{
 89		"title":          "Markdown Test",
 90		"prompt":         "Test prompt",
 91		"model":          "gpt-4",
 92		"articles_count": 3,
 93		"summary":        "Markdown summary content.",
 94	}
 95
 96	output, err := formatter.FormatData(data)
 97	if err != nil {
 98		t.Errorf("FormatData failed: %v", err)
 99	}
100
101	// Check for markdown structure
102	if !strings.Contains(output, "# Markdown Test") {
103		t.Error("Expected title in markdown format")
104	}
105	if !strings.Contains(output, "## Metadata") {
106		t.Error("Expected metadata section")
107	}
108	if !strings.Contains(output, "## Summary") {
109		t.Error("Expected summary section")
110	}
111	if !strings.Contains(output, "Markdown summary content.") {
112		t.Error("Expected summary content")
113	}
114}
115
116func TestFormatDataText(t *testing.T) {
117	formatter, err := NewFormatter("text")
118	if err != nil {
119		t.Fatalf("Failed to create formatter: %v", err)
120	}
121
122	data := map[string]any{
123		"title":          "Text Output Test",
124		"prompt":         "Test prompt",
125		"model":          "claude-3",
126		"articles_count": 2,
127		"summary":        "Plain text summary.",
128	}
129
130	output, err := formatter.FormatData(data)
131	if err != nil {
132		t.Errorf("FormatData failed: %v", err)
133	}
134
135	// Check for text formatting elements
136	if !strings.Contains(output, "====") {
137		t.Error("Expected separator lines")
138	}
139	if !strings.Contains(output, "Text Output Test") {
140		t.Error("Expected title in output")
141	}
142	if !strings.Contains(output, "METADATA") {
143		t.Error("Expected metadata header")
144	}
145	if !strings.Contains(output, "SUMMARY") {
146		t.Error("Expected summary header")
147	}
148}
149
150func TestFormatDataWithArticles(t *testing.T) {
151	formatter, err := NewFormatter("text")
152	if err != nil {
153		t.Fatalf("Failed to create formatter: %v", err)
154	}
155
156	articles := []map[string]any{
157		{
158			"title":       "Article One",
159			"source_feed": "News Feed",
160			"author":      "John Doe",
161			"link":        "https://example.com/1",
162			"published":   time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC),
163		},
164		{
165			"title":       "Article Two",
166			"source_feed": "Tech Feed",
167			"author":      "Jane Smith",
168			"link":        "https://example.com/2",
169		},
170	}
171
172	data := map[string]any{
173		"title":          "With Articles",
174		"summary":        "Summary with articles",
175		"articles_count": 2,
176		"articles":       articles,
177	}
178
179	output, err := formatter.FormatData(data)
180	if err != nil {
181		t.Errorf("FormatData failed: %v", err)
182	}
183
184	// Check articles are included
185	if !strings.Contains(output, "Article One") {
186		t.Error("Expected Article One in output")
187	}
188	if !strings.Contains(output, "Article Two") {
189		t.Error("Expected Article Two in output")
190	}
191	if !strings.Contains(output, "News Feed") {
192		t.Error("Expected feed name in output")
193	}
194}
195
196func TestFormatDataMarkdownWithArticles(t *testing.T) {
197	formatter, err := NewFormatter("markdown")
198	if err != nil {
199		t.Fatalf("Failed to create formatter: %v", err)
200	}
201
202	articles := []map[string]any{
203		{
204			"title":       "MD Article",
205			"source_feed": "MD Feed",
206			"published":   time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC),
207			"link":        "https://example.com/md",
208		},
209	}
210
211	data := map[string]any{
212		"title":          "Markdown with Articles",
213		"summary":        "Test summary",
214		"articles_count": 1,
215		"articles":       articles,
216	}
217
218	output, err := formatter.FormatData(data)
219	if err != nil {
220		t.Errorf("FormatData failed: %v", err)
221	}
222
223	// Check markdown article format
224	if !strings.Contains(output, "### Article 1: MD Article") {
225		t.Error("Expected markdown article header")
226	}
227	if !strings.Contains(output, "**Source**: MD Feed") {
228		t.Error("Expected source field in markdown")
229	}
230	if !strings.Contains(output, "[https://example.com/md](https://example.com/md)") {
231		t.Error("Expected link in markdown format")
232	}
233}
234
235func TestFormatDataDefaultValues(t *testing.T) {
236	formatter, err := NewFormatter("text")
237	if err != nil {
238		t.Fatalf("Failed to create formatter: %v", err)
239	}
240
241	// Data with missing fields - should use defaults
242	data := map[string]any{}
243
244	output, err := formatter.FormatData(data)
245	if err != nil {
246		t.Errorf("FormatData failed: %v", err)
247	}
248
249	// Check default values are used
250	if !strings.Contains(output, "LLM Aggregator Summary") {
251		t.Error("Expected default title")
252	}
253	if !strings.Contains(output, "No summary available.") {
254		t.Error("Expected default summary message")
255	}
256}
257
258func TestGetStringHelper(t *testing.T) {
259	tests := []struct {
260		name         string
261		data         map[string]any
262		key          string
263		defaultValue string
264		want         string
265	}{
266		{"existing string", map[string]any{"key": "value"}, "key", "default", "value"},
267		{"missing key", map[string]any{}, "key", "default", "default"},
268		{"nil value", map[string]any{"key": nil}, "key", "default", "default"},
269		{"wrong type", map[string]any{"key": 123}, "key", "default", "default"},
270	}
271
272	for _, tt := range tests {
273		t.Run(tt.name, func(t *testing.T) {
274			got := getString(tt.data, tt.key, tt.defaultValue)
275			if got != tt.want {
276				t.Errorf("getString() = %q, want %q", got, tt.want)
277			}
278		})
279	}
280}
281
282func TestGetIntHelper(t *testing.T) {
283	tests := []struct {
284		name         string
285		data         map[string]any
286		key          string
287		defaultValue int
288		want         int
289	}{
290		{"existing int", map[string]any{"count": 42}, "count", 0, 42},
291		{"existing float64", map[string]any{"count": float64(42.5)}, "count", 0, 42},
292		{"missing key", map[string]any{}, "count", 10, 10},
293		{"nil value", map[string]any{"count": nil}, "count", 10, 10},
294		{"wrong type", map[string]any{"count": "string"}, "count", 10, 10},
295	}
296
297	for _, tt := range tests {
298		t.Run(tt.name, func(t *testing.T) {
299			got := getInt(tt.data, tt.key, tt.defaultValue)
300			if got != tt.want {
301				t.Errorf("getInt() = %d, want %d", got, tt.want)
302			}
303		})
304	}
305}
306
307func TestCenterText(t *testing.T) {
308	tests := []struct {
309		name   string
310		text   string
311		width  int
312		expect string
313	}{
314		{"short text", "Hi", 10, "    Hi    "},  // 3 left, 2 right (floored)
315		{"exact width", "Hello", 5, "Hello"},
316		{"longer than width", "Hello World", 5, "Hello World"},
317		{"even padding", "Test", 12, "    Test    "},  // 4 left, 3 right (floored)
318	}
319
320	for _, tt := range tests {
321		t.Run(tt.name, func(t *testing.T) {
322			got := centerText(tt.text, tt.width)
323			if got != tt.expect {
324				t.Errorf("centerText(%q, %d) = %q, want %q", tt.text, tt.width, got, tt.expect)
325			}
326		})
327	}
328}
329
330func TestJSONIndentation(t *testing.T) {
331	formatter, err := NewFormatter("json")
332	if err != nil {
333		t.Fatalf("Failed to create formatter: %v", err)
334	}
335
336	data := map[string]any{
337		"nested": map[string]any{
338			"key1": "value1",
339			"key2": 42,
340		},
341		"array": []int{1, 2, 3},
342	}
343
344	output, err := formatter.FormatData(data)
345	if err != nil {
346		t.Errorf("FormatData failed: %v", err)
347	}
348
349	// Check for proper indentation (two spaces)
350	lines := strings.Split(output, "\n")
351	if len(lines) < 3 {
352		t.Error("Expected multiple lines in formatted JSON")
353	}
354	if !strings.HasPrefix(lines[1], "  ") {
355		t.Error("Expected two-space indentation")
356	}
357}