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.1 KB · 202 lines · Go Raw History
  1package main
  2
  3import (
  4	"strings"
  5	"testing"
  6	"time"
  7)
  8
  9func day(y int, m time.Month, d int) time.Time {
 10	return time.Date(y, m, d, 12, 0, 0, 0, time.UTC)
 11}
 12
 13// The answer that started this: every fixture in it had already been played,
 14// every sentence in it was true, and none of it was what was asked for.
 15const playedFixtures = `On September 4, 2026, Liverpool FC played Ipswich Town in a match that ended in a draw [2].
 16
 17- Liverpool FC played Ipswich Town on September 4, 2026 [2].
 18- Liverpool FC played Newcastle United on August 23, 2026 [4].
 19- Liverpool FC played Brentford FC on May 24, 2026 [7].`
 20
 21func TestLatestDate(t *testing.T) {
 22	now := day(2026, time.September, 5)
 23	cases := []struct {
 24		text string
 25		want string
 26	}{
 27		{playedFixtures, "2026-09-04"},
 28		{"Liverpool play Fulham at Anfield on 12 September 2026.", "2026-09-12"},
 29		{"The next one is September 12, 2026, at 3pm.", "2026-09-12"},
 30		{"Published 2026-09-12 by the club.", "2026-09-12"},
 31		{"They play on 12 September, kick off at 12:30.", "2026-09-12"},
 32		{"The window closes in September 2026.", "2026-09-01"},
 33		{"No date anywhere in this sentence.", ""},
 34	}
 35	for _, c := range cases {
 36		got, ok := latestDate(c.text, now)
 37		if c.want == "" {
 38			if ok {
 39				t.Errorf("%q: found %s, wanted nothing", c.text, got.Format("2006-01-02"))
 40			}
 41			continue
 42		}
 43		if !ok {
 44			t.Errorf("%q: found no date, wanted %s", c.text, c.want)
 45			continue
 46		}
 47		if got.Format("2006-01-02") != c.want {
 48			t.Errorf("%q: got %s, want %s", c.text, got.Format("2006-01-02"), c.want)
 49		}
 50	}
 51}
 52
 53// A date with no year written in December is next January, not ten months ago.
 54func TestLatestDateRollsTheYear(t *testing.T) {
 55	got, ok := latestDate("They are back on 3 January.", day(2026, time.December, 20))
 56	if !ok || got.Format("2006-01-02") != "2027-01-03" {
 57		t.Errorf("got %s ok=%v, want 2027-01-03", got.Format("2006-01-02"), ok)
 58	}
 59}
 60
 61func TestNoFutureDate(t *testing.T) {
 62	now := day(2026, time.September, 5)
 63	if !noFutureDate(playedFixtures, now) {
 64		t.Error("an answer of played fixtures should not count as answering what is next")
 65	}
 66	if !noFutureDate("They have a game soon.", now) {
 67		t.Error("an answer with no date at all does not answer when")
 68	}
 69	// Today counts, since a match this evening is the next one.
 70	if noFutureDate("They play Fulham on 5 September 2026.", now) {
 71		t.Error("today is not in the past")
 72	}
 73	if noFutureDate("They played on 4 September and play Fulham on 12 September 2026.", now) {
 74		t.Error("one future date is enough, background about the last match is fine")
 75	}
 76}
 77
 78func TestPastOnly(t *testing.T) {
 79	now := day(2026, time.September, 5)
 80	got := pastOnly(playedFixtures, now)
 81	if len(got) != 1 || !strings.Contains(got[0], "4 September 2026") {
 82		t.Fatalf("want one warning naming the latest date, got %q", got)
 83	}
 84	if got := pastOnly("They play Fulham on 12 September 2026.", now); len(got) != 0 {
 85		t.Errorf("a dated future answer needs no warning, got %q", got)
 86	}
 87	if got := pastOnly("There is a game coming up.", now); len(got) != 1 {
 88		t.Errorf("an answer with no date should warn, got %q", got)
 89	}
 90	if got := pastOnly("", now); len(got) != 0 {
 91		t.Errorf("an empty answer is handled elsewhere, got %q", got)
 92	}
 93}
 94
 95func TestGuessShapeUpcoming(t *testing.T) {
 96	for _, q := range []string{
 97		"when is the next liverpool game",
 98		"when do liverpool play next",
 99		"upcoming premier league fixtures",
100		"when is the next spacex launch",
101		"gta 6 release date",
102	} {
103		if got := guessShape(q); got != ShapeUpcoming {
104			t.Errorf("%q: got %s, want upcoming", q, got)
105		}
106	}
107	// The other half of the same question still reads as news.
108	for _, q := range []string{"who won the liverpool game", "did liverpool win last night"} {
109		if got := guessShape(q); got != ShapeNews {
110			t.Errorf("%q: got %s, want news", q, got)
111		}
112	}
113}
114
115// A shape the planner can pick and the contract map does not hold falls back to
116// prose, which is the wrong format quietly.
117func TestEveryShapeHasAContract(t *testing.T) {
118	for _, s := range shapeEnum {
119		c, ok := contracts[s]
120		if !ok {
121			t.Errorf("%s is in the plan enum with no contract", s)
122			continue
123		}
124		if c.Shape != s || c.Instruction == "" || c.MaxTokens == 0 {
125			t.Errorf("%s has an incomplete contract: %+v", s, c)
126		}
127	}
128	// The shapes the planner is never offered still need a contract, since
129	// something else picks them.
130	for _, s := range offEnum {
131		if _, ok := contracts[s]; !ok {
132			t.Errorf("%s has no contract", s)
133		}
134	}
135	if want := len(shapeEnum) + len(offEnum); want != len(contracts) {
136		t.Errorf("%d shapes against %d contracts, so one of them is unreachable", want, len(contracts))
137	}
138}
139
140// A fixture list writes the month short, and the page that carries the cup tie
141// is not always the page the answer came off.
142func TestSoonerAcrossPassages(t *testing.T) {
143	now := day(2026, time.September, 5)
144	passages := []Passage{
145		{ID: 1, Text: "Next match\n\n12 Sept 2026, 14:00/Liverpool FC vs Fulham FC"},
146		{ID: 2, Text: "## Upcoming fixtures\n- Liverpool vs Atleti Wed, 9 Sept 2026 - 19:00 - Champions League"},
147	}
148	answer := "The next Liverpool game is on **12 September 2026** against Fulham FC at Anfield [1]."
149	got := sooner(answer, passages, now)
150	if len(got) != 1 || !strings.Contains(got[0], "9 September") {
151		t.Fatalf("want a warning naming the earlier date, got %q", got)
152	}
153	if got := sooner("The next one is 9 September 2026 [2].", passages, now); len(got) != 0 {
154		t.Errorf("leading with the soonest needs no warning, got %q", got)
155	}
156	// Nothing sooner in the passages, so nothing to say.
157	if got := sooner(answer, passages[:1], now); len(got) != 0 {
158		t.Errorf("got %q", got)
159	}
160	// A page stamping itself with today is not a match today.
161	stamped := []Passage{{ID: 1, Text: "**Last updated:** Sep 5, 2026\n\n- 12 Sept 2026, 14:00/Liverpool FC vs Fulham FC"}}
162	if got := sooner(answer, stamped, now); len(got) != 0 {
163		t.Errorf("a page stamp is not a fixture, got %q", got)
164	}
165}
166
167func TestShortMonths(t *testing.T) {
168	now := day(2026, time.September, 5)
169	for text, want := range map[string]string{
170		"Wed, 9 Sept 2026 - 19:00":     "2026-09-09",
171		"12 Sept 2026, 14:00":          "2026-09-12",
172		"Sat Nov 28, 2026 at Goodison": "2026-11-28",
173		"31 Feb 2027 is not a day":     "",
174	} {
175		got, ok := latestDate(text, now)
176		if want == "" {
177			if ok {
178				t.Errorf("%q: read %s off a date that does not exist", text, got.Format("2006-01-02"))
179			}
180			continue
181		}
182		if !ok || got.Format("2006-01-02") != want {
183			t.Errorf("%q: got %s ok=%v, want %s", text, got.Format("2006-01-02"), ok, want)
184		}
185	}
186}
187
188func TestScheduleAge(t *testing.T) {
189	now := day(2026, time.September, 5)
190	fresh := []Source{{Published: "2026-06-19"}, {Published: "2026-09-02"}}
191	if got := scheduleAge(fresh, now); len(got) != 0 {
192		t.Errorf("a page from three days ago is current enough, got %q", got)
193	}
194	old := []Source{{Published: "2026-06-19"}, {Published: "2026-07-05"}}
195	if got := scheduleAge(old, now); len(got) != 1 || !strings.Contains(got[0], "62 days ago") {
196		t.Errorf("want one warning naming the age, got %q", got)
197	}
198	if got := scheduleAge([]Source{{URL: "https://example.com"}}, now); len(got) != 1 {
199		t.Errorf("an undated page should say so, got %q", got)
200	}
201}