orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package main
2
3import (
4 "strings"
5 "testing"
6 "time"
7)
8
9// row builds a dated at a fixed offset back from a fixed now, so the tests can
10// talk about order without carrying timestamps around.
11func row(source string, minsAgo int) dated {
12 base := time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC)
13 id := source + string(rune('a'+minsAgo))
14 return dated{
15 Headline: Headline{Title: id, URL: "https://example.test/" + id, Source: source},
16 at: base.Add(-time.Duration(minsAgo) * time.Minute),
17 }
18}
19
20func sources(out []Headline) string {
21 var b strings.Builder
22 for i, h := range out {
23 if i > 0 {
24 b.WriteString(" ")
25 }
26 b.WriteString(h.Source)
27 }
28 return b.String()
29}
30
31// The panel reads down newest to oldest, which is the whole shape Isaac asked
32// for, and the cap must not reorder anything on the way there.
33func TestPickReadsNewestFirst(t *testing.T) {
34 all := []dated{row("NPR", 5), row("BBC", 20), row("NPR", 40), row("BBC", 90)}
35
36 out := pick(all)
37 if got := sources(out); got != "NPR BBC NPR BBC" {
38 t.Errorf("want the input order back, got %v", got)
39 }
40}
41
42func TestPickFillsThePanel(t *testing.T) {
43 var all []dated
44 for i := 0; i < 40; i++ {
45 all = append(all, row("BBC", i))
46 all = append(all, row("NPR", i))
47 }
48
49 out := pick(all)
50 if len(out) != wireShown {
51 t.Fatalf("want %d rows, got %d", wireShown, len(out))
52 }
53}
54
55// BBC supplies two of the three feeds and posts more often, so without the cap
56// a busy afternoon is a column of one outlet.
57func TestPickCapsOneOutlet(t *testing.T) {
58 var all []dated
59 for i := 0; i < 20; i++ {
60 all = append(all, row("BBC", i))
61 }
62 for i := 0; i < 20; i++ {
63 all = append(all, row("NPR", i+30))
64 }
65
66 out := pick(all)
67 bbc := 0
68 for _, h := range out {
69 if h.Source == "BBC" {
70 bbc++
71 }
72 }
73 if bbc > wirePerSource {
74 t.Errorf("BBC took %d rows, cap is %d: %v", bbc, wirePerSource, sources(out))
75 }
76}
77
78// The cap is a preference, not a reason to serve a short panel.
79func TestPickFillsPastTheCapWhenItHasTo(t *testing.T) {
80 var all []dated
81 for i := 0; i < 14; i++ {
82 all = append(all, row("NPR", i))
83 }
84
85 out := pick(all)
86 if len(out) != wireShown {
87 t.Fatalf("want %d rows from a single outlet, got %d", wireShown, len(out))
88 }
89 // The fill has to keep the panel in time order rather than append what the
90 // cap passed over to the bottom.
91 // row names each headline in age order, so the panel is in time order when
92 // the titles come back ascending.
93 for i := 1; i < len(out); i++ {
94 if out[i-1].Title > out[i].Title {
95 t.Fatalf("rows are out of order: %v", sources(out))
96 }
97 }
98}
99
100func TestPickTakesWhatItHasWhenTheresLittle(t *testing.T) {
101 all := []dated{row("NPR", 1), row("BBC", 2)}
102
103 out := pick(all)
104 if len(out) != 2 {
105 t.Fatalf("want 2 rows, got %d", len(out))
106 }
107}
108
109func TestPromotionalKeepsRealNews(t *testing.T) {
110 keep := []string{
111 "Congress averts a government shutdown ahead of the midterms",
112 "Federal Reserve issues FOMC statement",
113 "Germany says Russia behind Leipzig airport drone attack",
114 }
115 for _, title := range keep {
116 if promotional(title) || sidebar(title) {
117 t.Errorf("filtered real news: %q", title)
118 }
119 }
120
121 drop := []string{
122 "Sign up for our daily briefing",
123 "Sponsored: the best cash back cards",
124 "Prime Day deals you can still get",
125 }
126 for _, title := range drop {
127 if !promotional(title) {
128 t.Errorf("kept an ad: %q", title)
129 }
130 }
131}
132
133// BBC mixes video and rolling live pages into its news feeds under a headline
134// that reads like a story.
135func TestSidebarDropsTheNonStories(t *testing.T) {
136 drop := []string{
137 "Watch: Defence calls for one juror to be dismissed",
138 "In pictures: the storm that flooded Toronto",
139 "Live updates: the Fed decision",
140 }
141 for _, title := range drop {
142 if !sidebar(title) {
143 t.Errorf("kept a non story: %q", title)
144 }
145 }
146}
147
148// NPR's feeds carry the same story under the same headline, and two outlets
149// carry it under headlines that differ by punctuation.
150func TestTitleKeyMatchesAcrossFeeds(t *testing.T) {
151 a := "Congress averts a government shutdown ahead of the midterms"
152 b := "Congress averts a government shutdown ahead of the midterms."
153 if titleKey(a) != titleKey(b) {
154 t.Errorf("same story read as two: %q vs %q", titleKey(a), titleKey(b))
155 }
156 if titleKey(a) == titleKey("Germany says Russia behind Leipzig drone attack") {
157 t.Error("two stories read as one")
158 }
159}
160
161func TestParseRSSTimeReadsBothFeedFormats(t *testing.T) {
162 for _, v := range []string{"Thu, 03 Sep 2026 22:31:23 GMT", "Thu, 03 Sep 2026 18:19:02 -0400"} {
163 got, err := parseRSSTime(v)
164 if err != nil {
165 t.Fatalf("pubDate %q did not parse: %v", v, err)
166 }
167 if got.Year() != 2026 || got.Month() != time.September || got.Day() != 3 {
168 t.Errorf("%q parsed to %v", v, got)
169 }
170 }
171}
172
173func TestClipDropsBBCVideoAndLive(t *testing.T) {
174 if !clip("https://www.bbc.co.uk/news/videos/ce30lqln299o?at_medium=RSS") {
175 t.Error("kept a video clip")
176 }
177 if !clip("https://www.bbc.co.uk/news/live/cx2g5nrpz4vt?at_medium=RSS") {
178 t.Error("kept a live page")
179 }
180 if clip("https://www.bbc.co.uk/news/articles/cj06q4ynpmjo?at_medium=RSS") {
181 t.Error("dropped a story")
182 }
183}
184
185// Two outlets word the same story differently, which is what the exact title
186// key cannot catch, and a duplicate row is the most obvious thing on a panel
187// of ten.
188func TestDedupeCatchesTheSameStoryTwice(t *testing.T) {
189 npr := "Leon Black defies subpoena to testify in Epstein inquiry and sues House panel"
190 bbc := "US billionaire Leon Black defies summons and sues Epstein panel"
191 if !sameStory(significant(npr), significant(bbc)) {
192 t.Errorf("read one story as two: %q and %q", npr, bbc)
193 }
194
195 // Same day, same president, different stories.
196 a := "Trump asks Supreme Court to lift block on USPS plan to restrict mail voting"
197 b := "Trump $1 coin makes him first living president on US currency in a century"
198 if sameStory(significant(a), significant(b)) {
199 t.Errorf("read two stories as one: %q and %q", a, b)
200 }
201}
202
203func TestDedupeKeepsTheNewerCopy(t *testing.T) {
204 newer := row("BBC", 5)
205 newer.Title = "US billionaire Leon Black defies summons and sues Epstein panel"
206 older := row("NPR", 60)
207 older.Title = "Leon Black defies subpoena to testify in Epstein inquiry and sues House panel"
208
209 out := dedupe([]dated{newer, older})
210 if len(out) != 1 {
211 t.Fatalf("want 1 row, got %d", len(out))
212 }
213 if out[0].Source != "BBC" {
214 t.Errorf("want the newer copy, got %q", out[0].Source)
215 }
216}
217
218// An outlet runs an explainer beside the story it explains, and the panel wants
219// the story.
220func TestSidebarDropsTheExplainer(t *testing.T) {
221 if !sidebar("Could Lindsay Clancy trial end in a mistrial? Here are the jury's options") {
222 t.Error("kept an explainer")
223 }
224 if sidebar("Trump asks Supreme Court to lift block on USPS plan") {
225 t.Error("dropped a story")
226 }
227}
228
229// One outlet writes charged where the other writes charges, and the stem is
230// what makes those the same word.
231func TestDedupeCatchesTheSameStoryWordedApart(t *testing.T) {
232 bbc := "ICE agent charged with lying about shooting Venezuelan man during crackdown"
233 npr := "ICE officer faces federal charges for lying over shooting of Venezuelan immigrant"
234 if !sameStory(significant(bbc), significant(npr)) {
235 t.Errorf("read one story as two: %q and %q", bbc, npr)
236 }
237
238 // Three angles on one death are three stories and the panel keeps them all.
239 a := "Tributes to Gloria Steinem are flooding in, from Hollywood to Capitol Hill"
240 b := "Feminist activist and journalist Gloria Steinem dies, aged 92"
241 if sameStory(significant(a), significant(b)) {
242 t.Errorf("read two stories as one: %q and %q", a, b)
243 }
244}