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

8.1 KB · 261 lines · Go Raw History
  1package main
  2
  3import (
  4	"path/filepath"
  5	"strings"
  6	"testing"
  7)
  8
  9func memStore(t *testing.T) *Store {
 10	t.Helper()
 11	s, err := OpenStore(filepath.Join(t.TempDir(), "chat.db"))
 12	if err != nil {
 13		t.Fatal(err)
 14	}
 15	t.Cleanup(func() { _ = s.Close() })
 16	return s
 17}
 18
 19func TestAFactIsStoredOnceHoweverOftenProposed(t *testing.T) {
 20	s := memStore(t)
 21	for i := 0; i < 3; i++ {
 22		if _, err := s.AddFact("Isaac drinks his coffee black"); err != nil {
 23			t.Fatal(err)
 24		}
 25	}
 26	facts, err := s.Facts()
 27	if err != nil {
 28		t.Fatal(err)
 29	}
 30	if len(facts) != 1 {
 31		t.Fatalf("got %d facts, want 1", len(facts))
 32	}
 33}
 34
 35// Retrieval is the whole value of this. A fact that does not surface when the
 36// question touches it may as well not be stored.
 37func TestRelevantFindsWhatTheQuestionTouches(t *testing.T) {
 38	s := memStore(t)
 39	for _, f := range []string{
 40		"Isaac drinks his coffee black",
 41		"Isaac prefers a hammock to a tent",
 42		"Isaac runs every site he owns from a desktop behind a Cloudflare tunnel",
 43		"Isaac camps most weekends through the autumn",
 44	} {
 45		if _, err := s.AddFact(f); err != nil {
 46			t.Fatal(err)
 47		}
 48	}
 49
 50	got := s.Relevant("what tent should I take camping this year", 3)
 51	if len(got) == 0 {
 52		t.Fatal("nothing matched a question about camping")
 53	}
 54	joined := ""
 55	for _, f := range got {
 56		joined += f.Text + " | "
 57	}
 58	if !strings.Contains(joined, "hammock") || !strings.Contains(joined, "camps") {
 59		t.Errorf("the two facts that matter did not come back:\n%s", joined)
 60	}
 61	if strings.Contains(joined, "coffee") {
 62		t.Errorf("an unrelated fact was pulled in:\n%s", joined)
 63	}
 64}
 65
 66// Without stopwords "my" and "the" match everything, so a question about
 67// anything drags in the whole table.
 68func TestRelevantIgnoresWordsThatMatchEverything(t *testing.T) {
 69	s := memStore(t)
 70	if _, err := s.AddFact("Isaac prefers the aisle seat"); err != nil {
 71		t.Fatal(err)
 72	}
 73	if got := s.Relevant("what is the weather", 5); len(got) != 0 {
 74		t.Errorf("matched on filler words: %+v", got)
 75	}
 76}
 77
 78func TestRelevantCountsUses(t *testing.T) {
 79	s := memStore(t)
 80	if _, err := s.AddFact("Isaac uses bun rather than npm"); err != nil {
 81		t.Fatal(err)
 82	}
 83	s.Relevant("should I use bun here", 3)
 84	facts, _ := s.Facts()
 85	if facts[0].Used != 1 {
 86		t.Errorf("used = %d, want 1", facts[0].Used)
 87	}
 88}
 89
 90// A model that ignores the rules once must not be able to write a credential
 91// into a file that is read into every future turn.
 92func TestCredentialsAreRefusedWhateverTheModelSays(t *testing.T) {
 93	for _, bad := range []string{
 94		"Isaac's password is hunter2",
 95		"The api key for the gateway is orch-EXAMPLEEXAMPLEEXAMPLEEXAMPLEexample00",
 96		"Isaac's token is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
 97		"His ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQ key",
 98	} {
 99		if !looksSecret(bad) {
100			t.Errorf("allowed through: %q", bad)
101		}
102	}
103	for _, fine := range []string{
104		"Isaac drinks his coffee black",
105		"Isaac prefers a hammock to a tent",
106		"Isaac runs Debian 13 trixie in his development container",
107	} {
108		if looksSecret(fine) {
109			t.Errorf("refused an ordinary fact: %q", fine)
110		}
111	}
112}
113
114func TestApplyChangesRefusesAnIdThatIsNotThere(t *testing.T) {
115	s := memStore(t)
116	id, err := s.AddFact("Isaac drinks his coffee black")
117	if err != nil {
118		t.Fatal(err)
119	}
120	existing, _ := s.Facts()
121	site := &site{store: s}
122
123	applied := site.applyChanges([]memChange{
124		{Op: "replace", ID: id + 999, Fact: "something else"},
125		{Op: "delete", ID: id + 998},
126	}, existing)
127	if len(applied) != 0 {
128		t.Errorf("acted on ids that do not exist: %+v", applied)
129	}
130	if facts, _ := s.Facts(); len(facts) != 1 {
131		t.Errorf("the real fact was disturbed: %+v", facts)
132	}
133}
134
135func TestApplyChangesReplacesRatherThanDuplicating(t *testing.T) {
136	s := memStore(t)
137	id, _ := s.AddFact("Isaac's car is a Subaru")
138	existing, _ := s.Facts()
139	site := &site{store: s}
140
141	site.applyChanges([]memChange{{Op: "replace", ID: id, Fact: "Isaac's car is a Toyota"}}, existing)
142
143	facts, _ := s.Facts()
144	if len(facts) != 1 {
145		t.Fatalf("got %d facts, want the one replaced", len(facts))
146	}
147	if facts[0].Text != "Isaac's car is a Toyota" {
148		t.Errorf("fact = %q", facts[0].Text)
149	}
150}
151
152// A small model wraps JSON in a fence or writes a sentence in front of it, and
153// neither is a reason to lose the edit.
154func TestParseChangesSurvivesTheUsualWrapping(t *testing.T) {
155	raw := "Sure, here is what should change:\n```json\n" +
156		`{"changes":[{"op":"add","fact":"Isaac drinks his coffee black"},{"op":"delete","id":4}]}` +
157		"\n```"
158	got := parseChanges(raw)
159	if len(got) != 2 {
160		t.Fatalf("got %d changes: %+v", len(got), got)
161	}
162	if got[0].Op != "add" || got[0].Fact != "Isaac drinks his coffee black" {
163		t.Errorf("first = %+v", got[0])
164	}
165	if got[1].Op != "delete" || got[1].ID != 4 {
166		t.Errorf("second = %+v", got[1])
167	}
168}
169
170func TestParseChangesDropsTheMalformed(t *testing.T) {
171	got := parseChanges(`{"changes":[{"op":"add"},{"op":"replace","fact":"no id"},{"op":"burn","id":1},{"op":"delete","id":0}]}`)
172	if len(got) != 0 {
173		t.Errorf("kept malformed changes: %+v", got)
174	}
175}
176
177// An empty block is left out entirely rather than saying it knows nothing,
178// which a model reads as an invitation to talk about not knowing things.
179func TestMemoryBlockIsEmptyWhenNothingMatched(t *testing.T) {
180	if got := memoryBlock(nil); got != "" {
181		t.Errorf("got %q, want nothing", got)
182	}
183	got := memoryBlock([]Fact{{Text: "Isaac drinks his coffee black"}})
184	if !strings.Contains(got, "coffee black") {
185		t.Errorf("the fact is missing:\n%s", got)
186	}
187	if !strings.Contains(got, "Do not list them back") {
188		t.Errorf("the instruction not to recite them is missing:\n%s", got)
189	}
190}
191
192func TestTidyFactCapsLength(t *testing.T) {
193	long := "Isaac " + strings.Repeat("x", maxFactChars+50)
194	if got := tidyFact(long); len(got) > maxFactChars {
195		t.Errorf("length = %d, want at most %d", len(got), maxFactChars)
196	}
197	if got := tidyFact("  - Isaac   drinks  coffee "); got != "Isaac drinks coffee" {
198		t.Errorf("got %q", got)
199	}
200}
201
202// The unique index only ever caught a fact proposed back word for word. On
203// 2026-09-08 one turn wrote two nearly identical facts about X post search and
204// left a third standing that contradicted both, because the model is asked to
205// replace rather than add and did not.
206func TestAddFactMergesARewording(t *testing.T) {
207	s := memStore(t)
208
209	first, err := s.AddFact("Isaac wants to build XCancel into the chat tooling in some way.")
210	if err != nil {
211		t.Fatal(err)
212	}
213	second, err := s.AddFact("Isaac is still looking for a way to integrate X post search into the chat tooling, " +
214		"but xcancel and Nitter are not the solutions since xcancel has no API.")
215	if err != nil {
216		t.Fatal(err)
217	}
218	if second != first {
219		t.Errorf("a rewording was stored as a new fact (%d then %d)", first, second)
220	}
221	facts, _ := s.Facts()
222	if len(facts) != 1 {
223		t.Fatalf("the table holds %d facts, want the one", len(facts))
224	}
225	// The newer wording is the one that stands, since it is the correction.
226	if !strings.Contains(facts[0].Text, "not the solutions") {
227		t.Errorf("the stored fact is the old wording: %q", facts[0].Text)
228	}
229}
230
231// A fact wholly contained in one already stored adds nothing, and replacing the
232// longer with the shorter would throw away what it knew.
233func TestAddFactKeepsTheFullerWording(t *testing.T) {
234	s := memStore(t)
235	full, _ := s.AddFact("Isaac camps and hikes in Yadkin Valley, North Carolina.")
236	again, err := s.AddFact("Isaac camps and hikes.")
237	if err != nil {
238		t.Fatal(err)
239	}
240	if again != full {
241		t.Errorf("the shorter fact was stored separately (%d then %d)", full, again)
242	}
243	facts, _ := s.Facts()
244	if len(facts) != 1 || !strings.Contains(facts[0].Text, "Yadkin") {
245		t.Errorf("the fuller wording was lost: %#v", facts)
246	}
247}
248
249// Two facts sharing only a name are not the same fact, and merging them would
250// lose one of them for good.
251func TestAddFactKeepsUnrelatedFactsApart(t *testing.T) {
252	s := memStore(t)
253	_, _ = s.AddFact("Isaac likes buttered chicken pizza.")
254	_, _ = s.AddFact("Isaac eats sausages but avoids ones containing nitrates or nitrites.")
255	_, _ = s.AddFact("Isaac regularly cooks sheet-pan meals of broccoli and potatoes with a single roasted protein.")
256	facts, _ := s.Facts()
257	if len(facts) != 3 {
258		t.Errorf("three unrelated facts collapsed to %d: %#v", len(facts), facts)
259	}
260}