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 "bytes"
5 "fmt"
6 "mime/multipart"
7 "strings"
8 "testing"
9)
10
11// headers runs the bytes through a real multipart round trip, since that is
12// what the handler hands readFiles and a hand built FileHeader has no body.
13func headers(t *testing.T, files map[string][]byte) []*multipart.FileHeader {
14 t.Helper()
15 var buf bytes.Buffer
16 w := multipart.NewWriter(&buf)
17 names := make([]string, 0, len(files))
18 for name := range files {
19 names = append(names, name)
20 }
21 for _, name := range names {
22 p, err := w.CreateFormFile("files", name)
23 if err != nil {
24 t.Fatal(err)
25 }
26 if _, err := p.Write(files[name]); err != nil {
27 t.Fatal(err)
28 }
29 }
30 if err := w.Close(); err != nil {
31 t.Fatal(err)
32 }
33 form, err := multipart.NewReader(&buf, w.Boundary()).ReadForm(1 << 20)
34 if err != nil {
35 t.Fatal(err)
36 }
37 t.Cleanup(func() { _ = form.RemoveAll() })
38 return form.File["files"]
39}
40
41func TestReadsATextFile(t *testing.T) {
42 parts := readFiles(headers(t, map[string][]byte{"notes.md": []byte("# Title\r\nbody\r\n")}))
43 if len(parts) != 1 {
44 t.Fatalf("got %d parts", len(parts))
45 }
46 p := parts[0]
47 if p.Err != "" {
48 t.Fatalf("unexpected error: %s", p.Err)
49 }
50 if p.Kind != "md" {
51 t.Errorf("kind = %q, want md", p.Kind)
52 }
53 if strings.Contains(p.Text, "\r") {
54 t.Error("carriage returns survived")
55 }
56 if p.Text != "# Title\nbody\n" {
57 t.Errorf("text = %q", p.Text)
58 }
59}
60
61// An unknown suffix is common on config and log files, and refusing by
62// extension would drop exactly the files worth asking about.
63func TestReadsAnUnknownExtension(t *testing.T) {
64 parts := readFiles(headers(t, map[string][]byte{"hosts.conf.bak": []byte("listen 8000\n")}))
65 if parts[0].Err != "" {
66 t.Fatalf("unexpected error: %s", parts[0].Err)
67 }
68 if parts[0].Text != "listen 8000\n" {
69 t.Errorf("text = %q", parts[0].Text)
70 }
71}
72
73func TestRefusesAnImageAndSaysWhy(t *testing.T) {
74 png := append([]byte("\x89PNG\r\n\x1a\n"), bytes.Repeat([]byte{0x00, 0x01}, 64)...)
75 parts := readFiles(headers(t, map[string][]byte{"shot.png": png}))
76 if parts[0].Err == "" {
77 t.Fatal("an image was accepted")
78 }
79 if !strings.Contains(parts[0].Err, "text only") {
80 t.Errorf("err = %q, want it to name the reason", parts[0].Err)
81 }
82 if parts[0].Kind != "png" {
83 t.Errorf("kind = %q, want png", parts[0].Kind)
84 }
85}
86
87// A .txt holding a binary payload is still binary, so the sniff has to beat
88// the extension in both directions.
89func TestRefusesBinaryNamedAsText(t *testing.T) {
90 parts := readFiles(headers(t, map[string][]byte{"payload.txt": {0x00, 0x01, 0x02, 0x03, 0x00}}))
91 if parts[0].Err == "" {
92 t.Fatal("a binary file was accepted")
93 }
94 if parts[0].Kind != "binary" {
95 t.Errorf("kind = %q, want binary", parts[0].Kind)
96 }
97}
98
99// One bad file must not lose the good ones, since a drop of five files where
100// one is a screenshot is the ordinary case.
101func TestOneBadFileDoesNotLoseTheRest(t *testing.T) {
102 parts := readFiles(headers(t, map[string][]byte{
103 "good.txt": []byte("hello"),
104 "bad.png": append([]byte("\x89PNG\r\n\x1a\n"), 0x00, 0x01),
105 }))
106 if len(parts) != 2 {
107 t.Fatalf("got %d parts", len(parts))
108 }
109 var ok, bad int
110 for _, p := range parts {
111 if p.Err == "" {
112 ok++
113 } else {
114 bad++
115 }
116 }
117 if ok != 1 || bad != 1 {
118 t.Errorf("ok = %d, bad = %d, want one of each", ok, bad)
119 }
120}
121
122func TestTruncatesAtTheFileCeiling(t *testing.T) {
123 big := bytes.Repeat([]byte("a"), maxFileChars+5000)
124 parts := readFiles(headers(t, map[string][]byte{"big.txt": big}))
125 if parts[0].Err != "" {
126 t.Fatalf("unexpected error: %s", parts[0].Err)
127 }
128 if len(parts[0].Text) > maxFileChars+200 {
129 t.Errorf("text is %d characters, want it capped near %d", len(parts[0].Text), maxFileChars)
130 }
131 if !strings.Contains(parts[0].Text, "truncated") {
132 t.Error("truncation was silent")
133 }
134}
135
136// The message goes last because that is where a small model reads most
137// carefully, and a file after the question got answered as if it were one.
138func TestComposeEndsOnTheMessage(t *testing.T) {
139 got := composeTurn("what is wrong with this", []filePart{
140 {Attachment: Attachment{Name: "a.go", Kind: "go", Size: 12}, Text: "package main"},
141 })
142 if !strings.HasSuffix(got, "what is wrong with this") {
143 t.Errorf("does not end on the message:\n%s", got)
144 }
145 if strings.Index(got, "package main") > strings.Index(got, "what is wrong with this") {
146 t.Error("the file came after the message")
147 }
148 if !strings.Contains(got, "a.go") {
149 t.Error("the file was not named")
150 }
151}
152
153// Files with no message is a real turn, and it needs an instruction or the
154// model is handed a wall of text and no question.
155func TestComposeSuppliesAMessageWhenThereIsNone(t *testing.T) {
156 got := composeTurn("", []filePart{
157 {Attachment: Attachment{Name: "a.txt", Kind: "txt", Size: 3}, Text: "abc"},
158 })
159 if !strings.Contains(got, "say what they are") {
160 t.Errorf("no default instruction:\n%s", got)
161 }
162}
163
164func TestComposeNamesAFileItCouldNotRead(t *testing.T) {
165 got := composeTurn("read this", []filePart{
166 {Attachment: Attachment{Name: "shot.png", Kind: "png", Size: 900, Err: "reads text only"}},
167 })
168 if !strings.Contains(got, "shot.png") || !strings.Contains(got, "not readable") {
169 t.Errorf("the unreadable file was not reported:\n%s", got)
170 }
171}
172
173func TestComposeLeavesAPlainTurnAlone(t *testing.T) {
174 if got := composeTurn("hello", nil); got != "hello" {
175 t.Errorf("got %q, want it untouched", got)
176 }
177}
178
179// The title is generated from this, so a file's contents reaching it is how a
180// conversation ends up named after line one of a csv.
181func TestTitleSeedFallsBackToTheNames(t *testing.T) {
182 seed := titleSeed("", []filePart{
183 {Attachment: Attachment{Name: "budget.csv"}, Text: "date,amount\n2026-01-01,12"},
184 })
185 if !strings.Contains(seed, "budget.csv") {
186 t.Errorf("seed = %q", seed)
187 }
188 if strings.Contains(seed, "2026-01-01") {
189 t.Error("the file's contents reached the title")
190 }
191 if got := titleSeed("what is this", nil); got != "what is this" {
192 t.Errorf("got %q, want the message", got)
193 }
194}
195
196func TestHumanSize(t *testing.T) {
197 for _, c := range []struct {
198 in int64
199 want string
200 }{{512, "512 B"}, {2048, "2.0 KB"}, {5 << 20, "5.0 MB"}} {
201 if got := humanSize(c.in); got != c.want {
202 t.Errorf("humanSize(%d) = %q, want %q", c.in, got, c.want)
203 }
204 }
205}
206
207// The failure that lost a whole turn: a tool call cut off by the token budget
208// arrives as unparseable JSON and llama.cpp refuses the request outright.
209func TestATruncatedToolCallIsRecognised(t *testing.T) {
210 real := fmt.Errorf("the model refused this turn: server_error: Failed to parse tool call arguments as JSON: " +
211 "[json.exception.parse_error.101] parse error at line 1, column 868: syntax error while parsing value - " +
212 "invalid string: missing closing quote")
213 if !isTruncatedToolCall(real) {
214 t.Error("the real refusal was not recognised")
215 }
216 for _, other := range []error{
217 nil,
218 fmt.Errorf("the model is not answering: connection refused"),
219 fmt.Errorf("the model answered 503 with no reason"),
220 fmt.Errorf("failed to parse the response body"),
221 } {
222 if isTruncatedToolCall(other) {
223 t.Errorf("wrongly matched: %v", other)
224 }
225 }
226}