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
7 "chat.bythewood.me/tools"
8)
9
10func testSources() []Source {
11 return collectSources([]tools.Result{
12 {Name: "web_search", Content: map[string]any{"results": []tools.SearchHit{
13 {Title: "Portsmouth arrival", URL: "https://www.bbc.co.uk/news/articles/x", Snippet: "A boat carrying 140 people landed at Portsmouth"},
14 {Title: "Crossings fall", URL: "https://example.org/stats?utm_source=ddg", Snippet: "crossings fell 43% over the year"},
15 }}},
16 {Name: "web_fetch", Content: map[string]any{
17 "url": "https://www.bbc.co.uk/news/articles/x#top",
18 "text": "Border Force intercepted the dinghy off Portsmouth on Saturday and brought 140 people ashore. Hampshire Police opened an investigation into assaults on officers.",
19 }},
20 {Name: "weather", Content: map[string]any{"temp": 61}},
21 {Name: "web_fetch", Err: "404", Content: map[string]any{"url": "https://example.com/gone"}},
22 })
23}
24
25func TestCollectSources(t *testing.T) {
26 srcs := testSources()
27 if len(srcs) != 2 {
28 t.Fatalf("want 2 sources, got %d: %+v", len(srcs), srcs)
29 }
30 // The fetched page is first because it is what the model actually read,
31 // and the fragment on it does not make it a second source.
32 if srcs[0].N != 1 || srcs[0].Site != "bbc.co.uk" {
33 t.Errorf("the fetched page should be [1], got %+v", srcs[0])
34 }
35 if !strings.Contains(srcs[0].Text, "Hampshire") {
36 t.Error("the fetched page kept the snippet instead of the page text")
37 }
38 if srcs[0].Title != "Portsmouth arrival" {
39 t.Errorf("the search hit's title was lost: %q", srcs[0].Title)
40 }
41 // A tracking parameter is not part of the address.
42 if strings.Contains(srcs[1].URL, "utm_source") {
43 t.Errorf("utm parameter survived: %s", srcs[1].URL)
44 }
45 // A tool with no page behind it and a failed fetch are not sources.
46 for _, s := range srcs {
47 if strings.Contains(s.URL, "gone") {
48 t.Error("a failed fetch was offered as a source")
49 }
50 }
51}
52
53func TestAttachKeepsAndRepairs(t *testing.T) {
54 srcs := testSources()
55
56 // A number the model wrote that names a real source is kept, and one that
57 // names nothing is dropped rather than shown as text.
58 got := attach("Border Force brought 140 people ashore at Portsmouth [1]. The rest is guesswork [9].", srcs)
59 if !strings.Contains(got, "Portsmouth.[1]") {
60 t.Errorf("the model's own citation was not kept: %q", got)
61 }
62 if strings.Contains(got, "[9]") {
63 t.Errorf("a citation with no source behind it survived: %q", got)
64 }
65
66 // A sentence carrying a source's own figures and names gets one even
67 // though the model wrote none.
68 got = attach("Hampshire Police opened an investigation into assaults on officers.", srcs)
69 if !strings.Contains(got, "[1]") {
70 t.Errorf("a sentence lifted from the page was left uncited: %q", got)
71 }
72
73 // A sentence about nothing in particular gets nothing, since a pill on an
74 // unsupported line reads as a check that passed.
75 got = attach("That is worth thinking about before you decide anything.", srcs)
76 if strings.Contains(got, "[") {
77 t.Errorf("an unsupported sentence was given a source: %q", got)
78 }
79}
80
81func TestAttachLeavesCodeAlone(t *testing.T) {
82 srcs := testSources()
83 src := "Read it like this:\n\n```go\nfmt.Println(rows[1])\n```\n\nThen use `cols[1]` after."
84 got := attach(src, srcs)
85 if !strings.Contains(got, "rows[1])") {
86 t.Errorf("an index inside a fence was rewritten: %q", got)
87 }
88 if !strings.Contains(got, "`cols[1]`") {
89 t.Errorf("an index inside a code span was rewritten: %q", got)
90 }
91}
92
93func TestAttachMovesTheMarkerToTheEnd(t *testing.T) {
94 srcs := testSources()
95 // The model puts it after the full stop about as often as before it, and a
96 // pill has to land in the same place either way.
97 for _, in := range []string{
98 "Border Force brought 140 people ashore at Portsmouth. [1] It was Saturday.",
99 "Border Force brought 140 people ashore at Portsmouth [1]. It was Saturday.",
100 } {
101 got := attach(in, srcs)
102 if !strings.Contains(got, "Portsmouth.[1]") {
103 t.Errorf("marker not normalised for %q, got %q", in, got)
104 }
105 if strings.Count(got, "[1]") != 1 {
106 t.Errorf("the marker was duplicated: %q", got)
107 }
108 }
109}
110
111func TestDropSourceList(t *testing.T) {
112 // What the model wrote under an answer before it had numbers, and every
113 // line of it was dead text rather than a link.
114 answer := "The crossings are down 43% on last year.\n\nSo the framing does not hold.\n\nbbc.co.uk/news/articles/crl60z17lyko\nindependent.co.uk/news/uk/home-news/portsmouth-b3045873.html"
115 got := dropSourceList(answer)
116 if strings.Contains(got, "bbc.co.uk") || strings.Contains(got, "independent") {
117 t.Errorf("the address dump survived: %q", got)
118 }
119 if !strings.Contains(got, "the framing does not hold") {
120 t.Errorf("the answer above it was eaten: %q", got)
121 }
122
123 // The heading form, and the same thing written on one line.
124 if got := dropSourceList("It is down.\n\nSources: https://a.org/x and https://b.org/y"); strings.Contains(got, "a.org") {
125 t.Errorf("a Sources: line survived: %q", got)
126 }
127
128 // A bulleted list of links in the middle of an answer is the answer, and
129 // only a Sources heading turns one at the end into a dump.
130 links := "Three places sell it:\n\n- example.org/parts/a\n- example.org/parts/b"
131 if got := dropSourceList(links); got != links {
132 t.Errorf("a list the reader asked for was dropped: %q", got)
133 }
134 withHeading := links + "\n\nSources\n\n- example.org/parts/a"
135 if got := dropSourceList(withHeading); !strings.HasSuffix(got, "parts/b") {
136 t.Errorf("a headed dump was not trimmed back to the answer: %q", got)
137 }
138
139 // An answer ending on a real sentence is untouched, including one that
140 // mentions a site by name.
141 for _, keep := range []string{
142 "It is down 43% and nobody expected that.",
143 "Check the Home Office quarterly table, which is the number that settles it.",
144 } {
145 if got := dropSourceList(keep); got != keep {
146 t.Errorf("prose was trimmed: %q became %q", keep, got)
147 }
148 }
149}
150
151func TestLinkBareAddresses(t *testing.T) {
152 got := linkBareAddresses("It is on bbc.co.uk/news/articles/x now.")
153 if !strings.Contains(got, "https://bbc.co.uk/news/articles/x") {
154 t.Errorf("a schemeless address was left dead: %q", got)
155 }
156 // Already a link, code, and a version number are all left alone.
157 for _, keep := range []string{
158 "See https://bbc.co.uk/news/articles/x for it.",
159 "Run `go/bin/thing` first.",
160 "The ratio is 1.2/3 either way.",
161 } {
162 if got := linkBareAddresses(keep); got != keep {
163 t.Errorf("%q was rewritten to %q", keep, got)
164 }
165 }
166}
167
168func TestLinkCitations(t *testing.T) {
169 srcs := testSources()
170 got := linkCitations("<p>Ashore at Portsmouth[1].</p>", srcs)
171 if !strings.Contains(got, `href="https://www.bbc.co.uk/news/articles/x"`) {
172 t.Errorf("the pill did not link to the source: %q", got)
173 }
174 // A number with no source behind it stays as text rather than becoming a
175 // link to nothing.
176 if got := linkCitations("<p>Something[7].</p>", srcs); strings.Contains(got, "<a") {
177 t.Errorf("an unknown number was linked: %q", got)
178 }
179 // Code is left alone, since an index is not a citation.
180 if got := linkCitations("<pre><code>rows[1]</code></pre>", srcs); strings.Contains(got, "<a") {
181 t.Errorf("an index inside code was linked: %q", got)
182 }
183 // A digit in an attribute is not a citation either.
184 if got := linkCitations(`<p data-x="[1]">hi</p>`, srcs); strings.Contains(got, "<a") {
185 t.Errorf("an attribute was rewritten: %q", got)
186 }
187}
188
189func TestCited(t *testing.T) {
190 srcs := testSources()
191 out := cited("One thing[2]. Another thing.", srcs)
192 if len(out) != 1 || out[0].N != 2 {
193 t.Errorf("want only source 2, got %+v", out)
194 }
195 if len(cited("Nothing cited here.", srcs)) != 0 {
196 t.Error("an answer citing nothing listed sources anyway")
197 }
198}
199
200func TestSentences(t *testing.T) {
201 got := sentences("It landed at 4am. Police opened a case. Nobody was charged.")
202 if len(got) != 3 {
203 t.Fatalf("want 3 sentences, got %d: %q", len(got), got)
204 }
205 // A decimal and an abbreviation are not sentence ends, and a code span
206 // carrying a full stop is stepped over.
207 if got := sentences("The figure is 43.5% of the total."); len(got) != 1 {
208 t.Errorf("a decimal split a sentence: %q", got)
209 }
210 if got := sentences("Call `fmt.Println` for it."); len(got) != 1 {
211 t.Errorf("a code span split a sentence: %q", got)
212 }
213}