repos
/ orchard main

orchard

mirror

Every 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

7.6 KB · 221 lines · Go Raw History
  1package tools
  2
  3import (
  4	"testing"
  5	"time"
  6)
  7
  8func at(s string) time.Time {
  9	t, err := time.ParseInLocation("2006-01-02 15:04", s, newYork())
 10	if err != nil {
 11		panic(err)
 12	}
 13	return t
 14}
 15
 16// The windows are the whole point of this tool: if Isaac says today he means
 17// today, so each one is pinned against a fixed clock rather than trusted.
 18func TestNewsWindowsAreTakenLiterally(t *testing.T) {
 19	// A Monday afternoon, so "weekend" is the two days just gone.
 20	now := at("2026-09-07 16:20")
 21
 22	for _, tc := range []struct {
 23		window, from, to string
 24	}{
 25		{"today", "2026-09-07 00:00", "2026-09-07 16:20"},
 26		{"yesterday", "2026-09-06 00:00", "2026-09-07 00:00"},
 27		{"weekend", "2026-09-05 00:00", "2026-09-07 00:00"},
 28		// "over the weekend including today" has no answer in either of the
 29		// other two, and the plain weekend window excludes today by definition.
 30		{"weekend-and-today", "2026-09-05 00:00", "2026-09-07 16:20"},
 31		{"week", "2026-09-01 00:00", "2026-09-07 16:20"},
 32		{"month", "2026-08-09 00:00", "2026-09-07 16:20"},
 33	} {
 34		since, until, label := resolveWindow(now, tc.window)
 35		if !since.Equal(at(tc.from)) {
 36			t.Errorf("%s: from = %s, want %s", tc.window, since, at(tc.from))
 37		}
 38		if !until.Equal(at(tc.to)) {
 39			t.Errorf("%s: to = %s, want %s", tc.window, until, at(tc.to))
 40		}
 41		if label == "" {
 42			t.Errorf("%s: no label, so the answer cannot say which days it read", tc.window)
 43		}
 44	}
 45}
 46
 47// Asked on a Saturday, the weekend is the one being had and not the one before,
 48// and it cannot run past now.
 49func TestTheWeekendAskedForOnTheWeekendIsThisOne(t *testing.T) {
 50	now := at("2026-09-05 11:00")
 51	since, until, _ := resolveWindow(now, "weekend")
 52	if !since.Equal(at("2026-09-05 00:00")) {
 53		t.Errorf("from = %s, want Saturday morning", since)
 54	}
 55	if !until.Equal(now) {
 56		t.Errorf("to = %s, want now rather than a future Sunday night", until)
 57	}
 58}
 59
 60// An unknown word is today rather than an error, since the model picking
 61// something outside the enum should not cost the turn.
 62func TestAnUnknownWindowIsToday(t *testing.T) {
 63	now := at("2026-09-07 16:20")
 64	since, _, _ := resolveWindow(now, "fortnight")
 65	if !since.Equal(at("2026-09-07 00:00")) {
 66		t.Errorf("from = %s, want this morning", since)
 67	}
 68}
 69
 70func TestFeedTimesParse(t *testing.T) {
 71	for _, s := range []string{
 72		"Mon, 07 Sep 2026 15:04:05 +0000",
 73		"Mon, 7 Sep 2026 15:04:05 GMT",
 74		"2026-09-07T15:04:05Z",
 75		"2026-09-07T11:04:05-04:00",
 76	} {
 77		if _, ok := parseFeedTime(s); !ok {
 78			t.Errorf("did not parse %q", s)
 79		}
 80	}
 81	if _, ok := parseFeedTime("last tuesday"); ok {
 82		t.Error("parsed something that is not a date")
 83	}
 84}
 85
 86// A feed description arrives with markup often enough that leaving it in spends
 87// the model's window on span tags.
 88func TestSummariesLoseTheirMarkup(t *testing.T) {
 89	got := trimSummary(`<p>Five dead after a <a href="x">crash</a>.</p>`)
 90	if got != "Five dead after a crash." {
 91		t.Errorf("summary = %q", got)
 92	}
 93}
 94
 95// Two publishers carrying one story is worth knowing. One publisher's own item
 96// arriving twice is not.
 97func TestDuplicatesGoAndSeparatePublishersStay(t *testing.T) {
 98	in := []NewsItem{
 99		{Source: "BBC", Headline: "Plane crash at Miami", URL: "https://bbc/1"},
100		{Source: "BBC", Headline: "Plane crash at Miami", URL: "https://bbc/1"},
101		{Source: "NPR", Headline: "Plane crash at Miami", URL: "https://npr/1"},
102	}
103	if got := dedupeNews(in); len(got) != 2 {
104		t.Errorf("kept %d items, want the two publishers", len(got))
105	}
106}
107
108// On general news a scored post about timestamps must not outrank a fatal
109// crash, and on tech the score is exactly what should lead.
110func TestWhatLeadsDependsOnTheTopic(t *testing.T) {
111	desk := NewsItem{Source: "NPR", Headline: "Five dead in crash", rank: 0}
112	scored := NewsItem{Source: "Hacker News", Headline: "Timestamp conversion", Points: 900}
113
114	general := []NewsItem{scored, desk}
115	sortNews(general, false)
116	if general[0].Source != "NPR" {
117		t.Errorf("general led with %q", general[0].Source)
118	}
119
120	tech := []NewsItem{desk, scored}
121	sortNews(tech, true)
122	if tech[0].Source != "Hacker News" {
123		t.Errorf("tech led with %q", tech[0].Source)
124	}
125}
126
127// The bug that started this: a flat cap over everything, sorted desks first,
128// took 28 of 45 items and left all fourteen Hacker News stories and both
129// Lobsters ones on the floor. A section holding both has to split its slots.
130func TestASectionHoldingBothGivesTheAggregatorsSlots(t *testing.T) {
131	// A plain taker, so this tests the sharing and not the sorting.
132	take := func(items []NewsItem, slots int, _ bool) []NewsItem {
133		if len(items) > slots {
134			return items[:slots]
135		}
136		return items
137	}
138	desks := make([]NewsItem, 12)
139	for i := range desks {
140		desks[i] = NewsItem{Source: "BBC"}
141	}
142	scored := make([]NewsItem, 14)
143	for i := range scored {
144		scored[i] = NewsItem{Source: "Hacker News", Points: 400}
145	}
146
147	got := fillSection(section{slots: 9, aggregators: true,
148		feeds: []feed{{"BBC", "u"}}}, desks, scored, take)
149	if len(got) != 9 {
150		t.Fatalf("filled %d of 9 slots", len(got))
151	}
152	counts := map[string]int{}
153	for _, it := range got {
154		counts[it.Source]++
155	}
156	if counts["Hacker News"] == 0 {
157		t.Errorf("the aggregators were starved again: %v", counts)
158	}
159	if counts["BBC"] == 0 {
160		t.Errorf("the newsrooms were starved: %v", counts)
161	}
162}
163
164// A section with nothing from one side still fills up from the other rather
165// than coming back half empty.
166func TestASectionFillsUpWhenOneSideIsEmpty(t *testing.T) {
167	take := func(items []NewsItem, slots int, _ bool) []NewsItem {
168		if len(items) > slots {
169			return items[:slots]
170		}
171		return items
172	}
173	scored := make([]NewsItem, 10)
174	for i := range scored {
175		scored[i] = NewsItem{Source: "Hacker News", Points: 300}
176	}
177	got := fillSection(section{slots: 6, aggregators: true, feeds: []feed{{"BBC", "u"}}}, nil, scored, take)
178	if len(got) != 6 {
179		t.Errorf("filled %d of 6 slots with only aggregators available", len(got))
180	}
181}
182
183// The rundown carried the same NPR story twice on 2026-09-08 because the two
184// copies came off different feeds with different tracking parameters, and the
185// url was the only key that got looked at.
186func TestDedupeNewsIgnoresTrackingParameters(t *testing.T) {
187	in := []NewsItem{
188		{Source: "NPR Technology", Headline: "Voters are fed up with data centers", URL: "https://www.npr.org/2026/09/08/data-centers"},
189		{Source: "NPR Technology", Headline: "Voters are fed up with data centers", URL: "https://npr.org/2026/09/08/data-centers/?utm_source=rss"},
190		{Source: "BBC Technology", Headline: "Voters are fed up with data centers", URL: "https://bbc.co.uk/news/tech-1"},
191	}
192	out := dedupeNews(in)
193	if len(out) != 2 {
194		t.Fatalf("dedupe left %d items, want the NPR pair collapsed and the BBC one kept", len(out))
195	}
196	// Two publishers carrying one story is worth knowing, so the BBC copy stays.
197	if out[1].Source != "BBC Technology" {
198		t.Errorf("the second publisher was dropped: %#v", out)
199	}
200}
201
202// One story written up under two headlines is still one story, which an exact
203// key cannot see. The Navier-Stokes paper led the tech section twice.
204func TestSameStoryCatchesARewrittenHeadline(t *testing.T) {
205	a := headlineWords("On the Navier-Stokes Millennium Prize Problem")
206	b := headlineWords("Navier-Stokes Millennium Prize Problem, by Tristan Buckmaster")
207	if !sameStory(a, b) {
208		t.Error("two headlines about one paper were treated as two stories")
209	}
210
211	// And two genuinely different stories are not merged, which would lose one.
212	c := headlineWords("Mistral raises 3B euros for sovereign open-weight AI")
213	if sameStory(a, c) {
214		t.Error("two unrelated stories were merged")
215	}
216	// A short headline has too little in it to judge, so it is left alone.
217	if sameStory(headlineWords("Keep Our Servers Running"), headlineWords("Servers Running Hot")) {
218		t.Error("two short headlines were merged on a couple of shared words")
219	}
220}