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

3.1 KB · 105 lines · Go Raw History
  1package main
  2
  3import (
  4	"path/filepath"
  5	"strings"
  6	"testing"
  7)
  8
  9// A step goes out as it happens, so the list fills in while the answer is still
 10// being written rather than appearing whole at the end.
 11func TestAStepIsSentAsItHappens(t *testing.T) {
 12	var sent []Event
 13	tr := NewTrace(func(e Event) { sent = append(sent, e) })
 14
 15	tr.Add(Step{Kind: "tool", Label: "wikipedia"})
 16	tr.Add(Step{Kind: "answer", Label: "wrote the reply"})
 17
 18	if len(sent) != 2 {
 19		t.Fatalf("%d events sent, want 2", len(sent))
 20	}
 21	if sent[0].Kind != "step" || sent[0].Step == nil || sent[0].Step.Label != "wikipedia" {
 22		t.Errorf("first event is not the step: %+v", sent[0])
 23	}
 24	if len(tr.Steps()) != 2 {
 25		t.Errorf("%d steps kept, want 2", len(tr.Steps()))
 26	}
 27}
 28
 29// The trace is written next to every message, so a prompt of several thousand
 30// characters cannot go in whole.
 31func TestALongFieldIsClippedAndSaysSo(t *testing.T) {
 32	tr := NewTrace(nil)
 33	tr.Add(Step{Kind: "prompt", In: strings.Repeat("x", stepMax+500)})
 34
 35	got := tr.Steps()[0].In
 36	if len(got) > stepMax+80 {
 37		t.Errorf("field is %d characters, want it clipped near %d", len(got), stepMax)
 38	}
 39	if !strings.Contains(got, "more characters") {
 40		t.Errorf("a clipped field does not say it was clipped: %q", got[len(got)-40:])
 41	}
 42}
 43
 44// A turn that goes wrong must not write a megabyte of steps.
 45func TestTheStepCountIsCapped(t *testing.T) {
 46	tr := NewTrace(nil)
 47	for i := 0; i < stepsMax+25; i++ {
 48		tr.Add(Step{Kind: "model", Label: "round"})
 49	}
 50	if n := len(tr.Steps()); n != stepsMax {
 51		t.Errorf("%d steps kept, want the cap of %d", n, stepsMax)
 52	}
 53}
 54
 55// A nil trace is what the tests and any caller that does not want one pass, and
 56// it has to be usable rather than a panic waiting in the turn loop.
 57func TestANilTraceIsSafe(t *testing.T) {
 58	var tr *Trace
 59	tr.Add(Step{Kind: "model"})
 60	if tr.Steps() != nil {
 61		t.Error("a nil trace returned steps")
 62	}
 63}
 64
 65// The steps have to survive a reload, which means the column has to exist and
 66// be read back. A new column that went into one of the two table creates and
 67// not the other would lose them silently.
 68func TestStepsSurviveAStoreRoundTrip(t *testing.T) {
 69	store, err := OpenStore(filepath.Join(t.TempDir(), "chat.db"))
 70	if err != nil {
 71		t.Fatalf("open: %v", err)
 72	}
 73	defer store.Close()
 74
 75	id, err := store.NewConversation("test")
 76	if err != nil {
 77		t.Fatalf("new conversation: %v", err)
 78	}
 79	want := []Step{
 80		{Kind: "wikipedia", Label: "looked the subject up", In: "north korea", Out: "a country", MS: 18},
 81		{Kind: "gate", Label: "checked the draft", Bad: true},
 82	}
 83	if err := store.Append(id, Stored{Role: RoleAssistant, Content: "hi", Steps: want}); err != nil {
 84		t.Fatalf("append: %v", err)
 85	}
 86
 87	msgs, err := store.Messages(id)
 88	if err != nil {
 89		t.Fatalf("messages: %v", err)
 90	}
 91	if len(msgs) != 1 {
 92		t.Fatalf("%d messages, want 1", len(msgs))
 93	}
 94	got := msgs[0].Steps
 95	if len(got) != 2 {
 96		t.Fatalf("%d steps came back, want 2", len(got))
 97	}
 98	if got[0].In != "north korea" || got[0].MS != 18 {
 99		t.Errorf("the first step came back wrong: %+v", got[0])
100	}
101	if !got[1].Bad {
102		t.Error("the failed step came back as fine")
103	}
104}