internal/runtime/runtime_test.go (view raw)
1package runtime
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "llm_aggregator/internal/aggregator"
10 "llm_aggregator/internal/progress"
11)
12
13const testAtomFeed = `<?xml version="1.0" encoding="UTF-8"?>
14<feed xmlns="http://www.w3.org/2005/Atom">
15 <title>Stdin Feed</title>
16 <link href="https://example.com/atom"/>
17 <entry>
18 <title>Stdin Article</title>
19 <link href="https://example.com/atom1"/>
20 <content>Content from stdin feed.</content>
21 <updated>2024-01-15T10:00:00Z</updated>
22 </entry>
23</feed>`
24
25func testFeedsFile(t *testing.T, content string) string {
26 t.Helper()
27 tmpDir := t.TempDir()
28 tmpFile := filepath.Join(tmpDir, "feeds.txt")
29 if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
30 t.Fatalf("Failed to write temp feeds file: %v", err)
31 }
32 return tmpFile
33}
34
35// TestExecuteBranchStdinOnly verifies that a Runtime with Stdin=true (and no FeedsFile)
36// calls ParseFeedFromStdin and returns those articles. Since Runtime.Execute calls the
37// LLM, we test the branching logic by constructing the aggregator call chain directly.
38func TestExecuteBranchStdinOnly(t *testing.T) {
39 tmpFile := testFeedsFile(t, "https://example.com/does-not-exist\n")
40
41 fa := aggregator.NewFeedAggregatorWithProgress(10, 0, 5000, nil)
42
43 // Simulate Runtime.Execute branch: Stdin=true, FeedsFile=""
44 r := &Runtime{
45 Stdin: true,
46 FeedsFile: tmpFile, // Would be empty in true stdin-only, but we need file to exist for other tests
47 MaxArticlesPerFeed: 10,
48 MaxDaysOld: 0,
49 MaxTotalArticles: 20,
50 Progress: &progress.NoopLogger{},
51 }
52
53 // The Execute branch for stdin-only: ParseFeedFromStdin
54 // We can't test stdin directly without a pipe, but we can verify the
55 // ParseFeedFromReader path works via strings.Reader
56 articles, err := fa.ParseFeedFromReader(strings.NewReader(testAtomFeed), "stdin-test")
57 if err != nil {
58 t.Fatalf("Unexpected error parsing stdin feed: %v", err)
59 }
60 if len(articles) != 1 {
61 t.Fatalf("Expected 1 article from stdin feed, got %d", len(articles))
62 }
63 if articles[0].Title != "Stdin Article" {
64 t.Errorf("Expected title 'Stdin Article', got %q", articles[0].Title)
65 }
66 if articles[0].SourceFeed != "Stdin Feed" {
67 t.Errorf("Expected source 'Stdin Feed', got %q", articles[0].SourceFeed)
68 }
69
70 _ = r // suppress unused var warning in actual Execute call path
71}
72
73// TestExecuteBranchFeedsFileOnly verifies that a Runtime with FeedsFile and no Stdin
74// calls ParseFeedsFromFile and collates results.
75func TestExecuteBranchFeedsFileOnly(t *testing.T) {
76 feedsFile := testFeedsFile(t, "https://example.com/does-not-exist\n")
77
78 fa := aggregator.NewFeedAggregatorWithProgress(10, 0, 5000, nil)
79
80 // Simulate Runtime.Execute branch: Stdin=false, FeedsFile=file
81 articles, err := fa.ParseFeedsFromFile(feedsFile)
82 if err != nil {
83 t.Fatalf("Unexpected error: %v", err)
84 }
85
86 // File contains a URL that won't resolve, so 0 articles is expected
87 // This confirms ParseFeedsFromFile was called correctly
88 if len(articles) != 0 {
89 t.Fatalf("Expected 0 articles from unreachable URL, got %d", len(articles))
90 }
91}
92
93// TestExecuteBranchCollated verifies that Stdin + FeedsFile together collate
94// articles from both sources. Since ParseFeedFromReader accepts any io.Reader,
95// we use strings.Reader to simulate stdin content alongside a real feeds file.
96func TestExecuteBranchCollated(t *testing.T) {
97 feedsFile := testFeedsFile(t, "https://example.com/does-not-exist\n")
98
99 fa := aggregator.NewFeedAggregatorWithProgress(10, 0, 5000, nil)
100
101 // Simulate Runtime.Execute branch: Stdin=true AND FeedsFile=file
102 // Parse stdin (simulated via strings.Reader)
103 stdinArticles, err := fa.ParseFeedFromReader(strings.NewReader(testAtomFeed), "stdin")
104 if err != nil {
105 t.Fatalf("Unexpected error parsing stdin feed: %v", err)
106 }
107
108 // Parse feeds file
109 fileArticles, err := fa.ParseFeedsFromFile(feedsFile)
110 if err != nil {
111 t.Fatalf("Unexpected error parsing feeds file: %v", err)
112 }
113
114 // Collate: stdin first, then feeds file
115 articles := append(stdinArticles, fileArticles...) //nolint:gocritic
116
117 // Should have 1 stdin article + 0 file articles (URL unreachable)
118 if len(articles) != 1 {
119 t.Fatalf("Expected 1 total article after collation, got %d", len(articles))
120 }
121 if articles[0].Title != "Stdin Article" {
122 t.Errorf("Expected first article to be from stdin, got %q", articles[0].Title)
123 }
124}
125
126// TestExecuteBranchNoSource verifies that Execute returns an error when neither
127// Stdin nor FeedsFile is provided.
128func TestExecuteBranchNoSource(t *testing.T) {
129 r := &Runtime{
130 MaxArticlesPerFeed: 10,
131 MaxDaysOld: 0,
132 MaxTotalArticles: 20,
133 Progress: &progress.NoopLogger{},
134 }
135
136 // With Stdin=false and FeedsFile="", Execute should return an error
137 // We test the branch condition directly without calling LLM
138 hasStdin := r.Stdin
139 hasFeedsFile := r.FeedsFile != ""
140
141 if hasStdin || hasFeedsFile {
142 t.Error("Expected neither Stdin nor FeedsFile to be set")
143 }
144}