all repos — llm_aggregator @ 51cde19dd83544f497e4ce82d27d3524433979b3

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