all repos — llm_aggregator @ 4316d19f105d3ded269a393c048b38d2e83d3141

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