all repos — llm_aggregator @ 97ccb8da8015d57585d37a5d27e94ada7f766922

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

docs/TESTING.md (view raw)

  1# Testing guide
  2
  3This document explains the testing strategy and test coverage for the LLM
  4Aggregator project.
  5
  6## Running tests
  7
  8```bash
  9# Run all tests
 10go test ./...
 11
 12# Run tests with verbose output
 13go test ./... -v
 14
 15# Run tests for a specific package
 16go test ./internal/aggregator/...
 17
 18# Run tests with coverage
 19go test ./... -cover
 20```
 21
 22## Test packages
 23
 24### `internal/cli`: CLI parsing
 25
 26Tests for command-line argument handling using the `go-arg` library.
 27
 28| Test | Description |
 29|------|-------------|
 30| `TestFeedsFileParsing` | Verifies `--feeds-file` accepts valid file paths and rejects non-existent files |
 31| `TestPromptParsing` | Tests `--prompt` handling including empty strings and special characters |
 32| `TestAPIKeyParsing` | Verifies `--api-key` parsing and environment variable fallback |
 33| `TestModelParsing` | Tests `--model` with default value (`deepseek-chat`) and custom models |
 34| `TestBaseURLParsing` | Tests `--base-url` with various API endpoints |
 35| `TestArgsToViperMap` | Ensures CLI arguments convert correctly to Viper configuration |
 36| `TestParseKeywords` | Tests keyword parsing from comma-separated strings |
 37
 38Key features tested are argument validation and default value handling.
 39
 40---
 41
 42### `internal/config`: Configuration management
 43
 44Tests for the Viper-based configuration system.
 45
 46| Test | Description |
 47|------|-------------|
 48| `TestConfigParsingAlwaysPasses` | Ensures CLI argument parsing succeeds for valid inputs |
 49| `TestConfigLoadWithVariousSources` | Tests loading from config files and environment variables |
 50| `TestViperToConfigConversion` | Verifies conversion from Viper to Config struct |
 51| `TestBindCLIArgs` | Tests binding CLI arguments to Viper |
 52
 53**Configuration precedence** (highest precedence and lower):
 541. CLI arguments
 552. Environment variables (`LLM_AGGREGATOR_*`)
 563. Config file (`~/.config/llm_aggregator/config.toml`)
 574. Default values
 58
 59---
 60
 61### `internal/defaults`: Default values
 62
 63Tests for central default constants.
 64
 65| Test | Description |
 66|------|-------------|
 67| `TestDefaultValues` | Verifies all default constants are set correctly |
 68| `TestDefaultSystemPrompt` | Tests the default summarisation prompt |
 69| `TestDefaultValuesAreSensible` | Validates ranges (e.g., temperature 0-2, max articles > 0) |
 70| `TestDefaultBaseURLIsValid` | Ensures URL format is valid |
 71| `TestOutputFormatChoices` | Verifies supported output formats |
 72
 73**Default Values:**
 74| Setting | Default | Description |
 75|---------|---------|-------------|
 76| `DefaultModel` | `deepseek-chat` | LLM model to use |
 77| `DefaultBaseURL` | `https://api.deepseek.com` | API endpoint |
 78| `DefaultMaxTokens` | `4000` | Maximum tokens per request |
 79| `DefaultTemperature` | `0.7` | Sampling temperature (0-2) |
 80| `DefaultMaxArticlesPerFeed` | `10` | Articles per feed |
 81| `DefaultMaxDaysOld` | `7` | Maximum article age in days |
 82| `DefaultMaxTotalArticles` | `20` | Total articles limit |
 83| `DefaultOutput` | `text` | Output format |
 84| `DefaultIncludeArticles` | `false` | Include articles in output |
 85
 86---
 87
 88### `internal/aggregator`: Feed aggregation
 89
 90Tests for feed parsing, article extraction, and filtering.
 91
 92| Test | Description |
 93|------|-------------|
 94| `TestNewFeedAggregator` | Tests aggregator creation with various configurations |
 95| `TestNewFeedAggregatorWithProgress` | Tests with progress context |
 96| `TestParseFeedsFromFile` | Verifies parsing feeds file with comments |
 97| `TestParseFeedsFromFileEmpty` | Tests empty and comment-only files |
 98| `TestParseFeedsFromFileNotFound` | Verifies error handling for missing files |
 99| `TestArticleAgeFiltering` | Tests date-based article filtering |
100| `TestArticleSorting` | Tests date sorting (ascending/descending) |
101| `TestArticleLinkExtraction` | Verifies link extraction |
102| `TestArticleContentExtraction` | Tests content truncation |
103| `TestArticleToMap` | Tests article-to-map conversion |
104| `TestArticleStruct` | Tests Article struct methods |
105| `TestEmptyFeedsFile` | Tests handling of empty feeds file |
106
107**Mock data**
108
109```xml
110<?xml version="1.0" encoding="UTF-8"?>
111<rss version="2.0">
112  <channel>
113    <title>Test Feed</title>
114    <item>
115      <title>Article One</title>
116      <link>https://example.com/article1</link>
117      <pubDate>Wed, 15 Jan 2024 10:00:00 GMT</pubDate>
118    </item>
119  </channel>
120</rss>
121```
122
123---
124
125### `internal/output`: Summary formatting
126
127Tests for JSON, Markdown, and Text output formatting.
128
129| Test | Description |
130|------|-------------|
131| `TestNewFormatter` | Tests formatter creation with valid/invalid formats |
132| `TestFormatDataJSON` | Verifies JSON output structure |
133| `TestFormatDataMarkdown` | Tests Markdown formatting |
134| `TestFormatDataText` | Tests plain text formatting |
135| `TestFormatDataWithArticles` | Verifies articles appear in output |
136| `TestFormatDataMarkdownWithArticles` | Tests article formatting in Markdown |
137| `TestFormatDataDefaultValues` | Tests default values for missing fields |
138| `TestGetStringHelper` | Tests string extraction from map |
139| `TestGetIntHelper` | Tests integer extraction from map |
140| `TestCenterText` | Tests text centering |
141| `TestFormatArticleList` | Tests article list formatting |
142| `TestJSONIndentation` | Verifies proper JSON indentation |
143
144**Output Formats:**
145
146**JSON:**
147```json
148{
149  "title": "LLM Aggregator Summary",
150  "prompt": "Summarise the news",
151  "model": "deepseek-chat",
152  "articles_count": 5,
153  "timestamp": "2024-01-15T10:00:00Z",
154  "summary": "...",
155  "articles": [...]
156}
157```
158
159**Markdown:**
160```markdown
161# LLM Aggregator Summary
162
163## Metadata
164- **Prompt**: Summarise the news
165- **Model**: deepseek-chat
166- **Articles Analysed**: 5
167- **Generated**: 2024-01-15T10:00:00Z
168
169## Summary
170...
171
172## Articles Analysed
173### Article 1: Title
174**Source**: News Feed
175**Link**: [URL](URL)
176```
177
178**Text:**
179```
180================================================================
181                          Title
182================================================================
183METADATA
184----------------------------------------
185Prompt: Summarise the news
186Model: deepseek-chat
187...
188================================================================
189                       End of Summary
190================================================================
191```
192
193---
194
195### `internal/processor`: Content processing
196
197Tests for article filtering, sorting, and preparation for LLM summarisation.
198
199| Test | Description |
200|------|-------------|
201| `TestNewContentProcessor` | Tests processor creation |
202| `TestProcessArticles` | Tests overall article processing pipeline |
203| `TestFilterArticlesByIncludeKeywords` | Tests keyword inclusion filtering |
204| `TestFilterArticlesByExcludeKeywords` | Tests keyword exclusion filtering |
205| `TestFilterArticlesCaseInsensitive` | Verifies case-insensitive matching |
206| `TestSortArticlesByTitle` | Tests title sorting (A-Z, Z-A) |
207| `TestSortArticlesBySource` | Tests feed name sorting |
208| `TestPrepareForLLM` | Verifies article preparation for LLM |
209| `TestContentTruncation` | Tests content truncation |
210| `TestSetLogger` | Tests logger configuration |
211| `TestUnknownSortField` | Tests default sorting for unknown fields |
212| `TestMixedIncludeExclude` | Tests combined include/exclude filtering |
213| `TestEmptyKeywordLists` | Tests handling of empty keyword lists |
214
215**Filtering Logic:**
216
217```
218Original Articles
219220┌─────────────────────────────┐
221│  1. Check exclude keywords  │
222│     - If match → EXCLUDE    │
223│     - If no match → NEXT    │
224└─────────────────────────────┘
225226┌─────────────────────────────┐
227│  2. Check include keywords  │
228│     - If no filter → INCLUDE│
229│     - If match → INCLUDE    │
230│     - If no match → EXCLUDE │
231└─────────────────────────────┘
232233Filtered Articles
234235┌─────────────────────────────┐
236│  3. Sort (date/title/source) │
237└─────────────────────────────┘
238239┌─────────────────────────────┐
240│  4. Limit to maxTotalArticles│
241└─────────────────────────────┘
242243┌─────────────────────────────┐
244│  5. Truncate content        │
245└─────────────────────────────┘
246247Prepared Articles
248```
249
250**Sorting Options:**
251| Value | Behaviour |
252|-------|----------|
253| `date` | Sort by publication date (default) |
254| `title` | Sort alphabetically by title |
255| `source` | Sort by feed name |
256
257Keyword matching is case-insensitive.
258
259---
260
261## Test utilities
262
263### Helper functions
264
265| Package | Function | Purpose |
266|---------|----------|---------|
267| `aggregator` | `createTestArticles()` | Creates sample articles with dates |
268| `aggregator` | `writeTempFile()` | Creates temporary test files |
269| `processor` | `containsString()` | Checks if string exists in slice |
270| `output` | `getString()` | Extracts string from map with default |
271| `output` | `getInt()` | Extracts int from map with default |
272| `output` | `centerText()` | Centres text within width |
273
274### Mock data
275
276| Package | Data | Purpose |
277|---------|------|---------|
278| `aggregator` | `validRSSFeed` | Valid RSS XML for parsing tests |
279| `aggregator` | `atomFeed` | Atom XML for compatibility tests |
280| `aggregator` | `malformedRSSFeed` | Malformed XML for error handling |
281
282---
283
284## Coverage goals
285
286Current coverage status:
287
288| Package | Coverage | Notes |
289|---------|----------|-------|
290| `cli` | High | All argument types tested |
291| `config` | High | All config operations tested |
292| `defaults` | High | All defaults tested |
293| `aggregator` | Medium | Network calls require mocks |
294| `output` | High | All formatters tested |
295| `processor` | High | All filtering/sorting tested |
296| `llm` | Low | Requires API mocking |
297| `runtime` | Low | Integration tests only |
298| `tui` | None | Requires terminal interaction |
299
300---
301
302## Writing new tests
303
304### Test naming convention
305
306```go
307func Test<Component>_<Action>(t *testing.T) { ... }
308```
309
310Examples:
311- `TestNewFeedAggregator`: Test constructor
312- `TestFilterArticlesByExcludeKeywords`: Test filter with exclude
313- `TestFormatDataJSON`: Test JSON formatting
314
315### Table-driven tests
316
317Prefer table-driven tests for multiple test cases:
318
319```go
320func TestExample(t *testing.T) {
321    tests := []struct {
322        name     string
323        input    string
324        expected string
325    }{
326        {"empty string", "", ""},
327        {"single word", "hello", "hello"},
328        {"multiple words", "hello world", "hello world"},
329    }
330
331    for _, tt := range tests {
332        t.Run(tt.name, func(t *testing.T) {
333            result := Function(tt.input)
334            if result != tt.expected {
335                t.Errorf("got %q, want %q", result, tt.expected)
336            }
337        })
338    }
339}
340```
341
342### Using temporary files
343
344```go
345func TestWithTempFile(t *testing.T) {
346    tmpDir := t.TempDir()  // Go 1.15+
347    tmpFile := tmpDir + "/test.txt"
348    
349    if err := os.WriteFile(tmpFile, []byte("content"), 0644); err != nil {
350        t.Fatalf("Failed to create temp file: %v", err)
351    }
352    
353    // Test using tmpFile
354}
355```
356
357### Testing `nil` safety
358
359Always test `nil`/empty cases:
360
361```go
362t.Run("nil input", func(t *testing.T) {
363    result := ProcessArticles(nil, "date", false)
364    if len(result) != 0 {
365        t.Error("Expected empty result for nil input")
366    }
367})
368
369t.Run("empty slice", func(t *testing.T) {
370    result := ProcessArticles([]*Article{}, "date", false)
371    if len(result) != 0 {
372        t.Error("Expected empty result for empty slice")
373    }
374})
375```
376
377---
378
379## Continuous integration
380
381Tests should pass before committing:
382
383```bash
384# Verify all tests pass
385go test ./...
386
387# Verify with race detector
388go test ./... -race
389
390# Verify with coverage
391go test ./... -coverprofile=coverage.out
392go tool cover -html=coverage.out
393```