all repos — llm_aggregator @ 715e67108f4989876184fa73e7dcadecc63d59af

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